Python float (64-bit) truncated to 32-bit
mmmvvvppp opened this issue · comments
Starting in 8.28 and up, we have noticed that python float
s 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 float
s to represent time and using Joiners.overlapping
in our constraints. Using float
s as a means to represent time seems to have a much more favorable performance than datetime
s. 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
optapy/jpyinterpreter/src/main/python/python_to_java_bytecode_translator.py
Lines 350 to 352 in 2be7988
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)
.
Fixed by #144