optapy / optapy

OptaPy is an AI constraint solver for Python to optimize planning and scheduling problems.

Home Page:https://www.optapy.org/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Python float (64-bit) truncated to 32-bit

mmmvvvppp opened this issue · comments

Starting in 8.28 and up, we have noticed that python floats with 64-bit precision are incorrectly truncated to 32-bit numbers when calling OptaPy. Our numbers with 7 whole digits and 10 decimals were rounded to increments of 0.25 (though this varies with the magnitude of the number).

Adding the following call to optapy/jpyinterpreter/tests/test_float.py::test_add can replicate the problem:

int_add_verifier.verify(2345678.2345678, 0, expected_result=2345678.2345678)

results in:

AssertionError: Typed and untyped translated bytecode did not return expected result (2345678.2345678) for arguments (2345678.2345678, 0); it returned (2345678.25) (typed) and (2345678.25) (untyped) instead.

User note: we've been using floats to represent time and using Joiners.overlapping in our constraints. Using floats as a means to represent time seems to have a much more favorable performance than datetimes. We discovered this bug during our 8.23->8.31 migration because our constraint was no longer producing penalties due to the rounding of numbers which no longer resulted in times overlapping but now being "adjacent".

Cause is probably

out = PythonFloat.valueOf(JFloat(value))
put_in_instance_map(instance_map, value, out)
return out
, which use JFloat (32-bit Java float) instead of JDouble (64-bit Java float). The PythonFloat class already use 64-bit precision (see ).

Indeed, replacing JFloat with JDouble does seem to result in the expected response but causes a test failure in tests/test_builtins.py::test_round

AssertionError: Typed and untyped translated bytecode did not return expected result (1.05) for arguments (1.055, 2); it returned (1.06) (typed) and (1.06) (untyped) instead.

The reason was BigDecimal.valueOf(double), which creates a BigDecimal from the canonical toString (so BigDecimal.valueOf(1.055) == BigDecimal("1.055"). But since 1.055 cannot be exactly represented as a 64-bit float, it not actually 1.055 but 1.05499999..., which rounds to 1.05 (and not 1.06 like 1.055). To fix, using new BigDecimal(double) (which uses all the digits of the double instead of the shortest real number that get mapped to the double) in place of BigDecimal.valueOf(double).