diff options
author | Yoctopuce dev <dev@yoctopuce.com> | 2025-07-17 23:40:19 +0200 |
---|---|---|
committer | Damien George <damien@micropython.org> | 2025-07-24 11:07:30 +1000 |
commit | d6876e227318f974b94655ee7b74dc4995701660 (patch) | |
tree | f28d25ccc211a2b6a69d7adc48ef860ff396cb49 | |
parent | 59ee59901b9223697cf96f5859fcbf49fe9d21f4 (diff) |
py/obj: Fix REPR_C bias toward zero.
Current implementation of REPR_C works by clearing the two lower bits of
the mantissa to zero. As this happens after each floating point operation,
this tends to bias floating point numbers towards zero, causing decimals
like .9997 instead of rounded numbers. This is visible in test cases
involving repeated computations, such as `tests/misc/rge_sm.py` for
instance.
The suggested fix fills in the missing bits by copying the previous two
bits. Although this cannot recreate missing information, it fixes the bias
by inserting plausible values for the lost bits, at a relatively low cost.
Some float tests involving irrational numbers have to be softened in case
of REPR_C, as the 30 bits are not always enough to fulfill the expectations
of the original test, and the change may randomly affect the last digits.
Such cases have been made explicit by testing for REPR_C or by adding a
clear comment.
The perf_test fft code was also missing a call to round() before casting a
log_2 operation to int, which was causing a failure due to a last-decimal
change.
Signed-off-by: Yoctopuce dev <dev@yoctopuce.com>
-rw-r--r-- | py/obj.h | 4 | ||||
-rw-r--r-- | tests/float/cmath_fun.py | 3 | ||||
-rw-r--r-- | tests/float/math_fun_special.py | 5 | ||||
-rw-r--r-- | tests/float/string_format_fp30.py | 42 | ||||
-rw-r--r-- | tests/misc/rge_sm.py | 10 | ||||
-rw-r--r-- | tests/perf_bench/bm_fft.py | 2 | ||||
-rwxr-xr-x | tests/run-tests.py | 6 |
7 files changed, 22 insertions, 50 deletions
@@ -206,6 +206,10 @@ static inline mp_float_t mp_obj_float_get(mp_const_obj_t o) { mp_float_t f; mp_uint_t u; } num = {.u = ((mp_uint_t)o - 0x80800000u) & ~3u}; + // Rather than always truncating toward zero, which creates a strong + // bias, copy the two previous bits to fill in the two missing bits. + // This appears to be a pretty good heuristic. + num.u |= (num.u >> 2) & 3u; return num.f; } static inline mp_obj_t mp_obj_new_float(mp_float_t f) { diff --git a/tests/float/cmath_fun.py b/tests/float/cmath_fun.py index 39011733b..0037d7c65 100644 --- a/tests/float/cmath_fun.py +++ b/tests/float/cmath_fun.py @@ -51,6 +51,9 @@ for f_name, f, test_vals in functions: print("%.5g" % ret) elif type(ret) == tuple: print("%.5g %.5g" % ret) + elif f_name == "exp": + # exp amplifies REPR_C inaccuracies, so we need to check one digit less + print("complex(%.4g, %.4g)" % (real, ret.imag)) else: # some test (eg cmath.sqrt(-0.5)) disagree with CPython with tiny real part real = ret.real diff --git a/tests/float/math_fun_special.py b/tests/float/math_fun_special.py index e674ec8df..a747f73e9 100644 --- a/tests/float/math_fun_special.py +++ b/tests/float/math_fun_special.py @@ -43,10 +43,15 @@ functions = [ ("lgamma", lgamma, pos_test_values + [50.0, 100.0]), ] +is_REPR_C = float("1.0000001") == float("1.0") + for function_name, function, test_vals in functions: for value in test_vals: try: ans = "{:.4g}".format(function(value)) except ValueError as e: ans = str(e) + # a tiny error in REPR_C value for 1.5204998778 causes a wrong rounded value + if is_REPR_C and function_name == 'erfc' and ans == "1.521": + ans = "1.52" print("{}({:.4g}) = {}".format(function_name, value, ans)) diff --git a/tests/float/string_format_fp30.py b/tests/float/string_format_fp30.py deleted file mode 100644 index 5f0b213da..000000000 --- a/tests/float/string_format_fp30.py +++ /dev/null @@ -1,42 +0,0 @@ -def test(fmt, *args): - print("{:8s}".format(fmt) + ">" + fmt.format(*args) + "<") - - -test("{:10.4}", 123.456) -test("{:10.4e}", 123.456) -test("{:10.4e}", -123.456) -# test("{:10.4f}", 123.456) -# test("{:10.4f}", -123.456) -test("{:10.4g}", 123.456) -test("{:10.4g}", -123.456) -test("{:10.4n}", 123.456) -test("{:e}", 100) -test("{:f}", 200) -test("{:g}", 300) - -test("{:10.4E}", 123.456) -test("{:10.4E}", -123.456) -# test("{:10.4F}", 123.456) -# test("{:10.4F}", -123.456) -test("{:10.4G}", 123.456) -test("{:10.4G}", -123.456) - -test("{:06e}", float("inf")) -test("{:06e}", float("-inf")) -test("{:06e}", float("nan")) - -# The following fails right now -# test("{:10.1}", 0.0) - -print("%.0f" % (1.750000 % 0.08333333333)) -# Below isn't compatible with single-precision float -# print("%.1f" % (1.750000 % 0.08333333333)) -# print("%.2f" % (1.750000 % 0.08333333333)) -# print("%.12f" % (1.750000 % 0.08333333333)) - -# tests for errors in format string - -try: - "{:10.1b}".format(0.0) -except ValueError: - print("ValueError") diff --git a/tests/misc/rge_sm.py b/tests/misc/rge_sm.py index 5e071687c..f5b0910dd 100644 --- a/tests/misc/rge_sm.py +++ b/tests/misc/rge_sm.py @@ -119,6 +119,7 @@ def phaseDiagram(system, trajStart, trajPlot, h=0.1, tend=1.0, range=1.0): def singleTraj(system, trajStart, h=0.02, tend=1.0): + is_REPR_C = float("1.0000001") == float("1.0") tstart = 0.0 # compute the trajectory @@ -130,7 +131,14 @@ def singleTraj(system, trajStart, h=0.02, tend=1.0): for i in range(len(rk.Trajectory)): tr = rk.Trajectory[i] - print(" ".join(["{:.4f}".format(t) for t in tr])) + tr_str = " ".join(["{:.4f}".format(t) for t in tr]) + if is_REPR_C: + # allow two small deviations for REPR_C + if tr_str == "1.0000 0.3559 0.6485 1.1944 0.9271 0.1083": + tr_str = "1.0000 0.3559 0.6485 1.1944 0.9272 0.1083" + if tr_str == "16.0000 0.3894 0.5793 0.7017 0.5686 -0.0168": + tr_str = "16.0000 0.3894 0.5793 0.7017 0.5686 -0.0167" + print(tr_str) # phaseDiagram(sysSM, (lambda i, j: [0.354, 0.654, 1.278, 0.8 + 0.2 * i, 0.1 + 0.1 * j]), (lambda a: (a[4], a[5])), h=0.1, tend=math.log(10**17)) diff --git a/tests/perf_bench/bm_fft.py b/tests/perf_bench/bm_fft.py index 9a2d03d11..e35c1216c 100644 --- a/tests/perf_bench/bm_fft.py +++ b/tests/perf_bench/bm_fft.py @@ -15,7 +15,7 @@ def transform_radix2(vector, inverse): # Initialization n = len(vector) - levels = int(math.log(n) / math.log(2)) + levels = int(round(math.log(n) / math.log(2))) coef = (2 if inverse else -2) * cmath.pi / n exptable = [cmath.rect(1, i * coef) for i in range(n // 2)] vector = [vector[reverse(i, levels)] for i in range(n)] # Copy with bit-reversed permutation diff --git a/tests/run-tests.py b/tests/run-tests.py index 522027c1f..328d69f63 100755 --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -105,9 +105,6 @@ PC_PLATFORMS = ("darwin", "linux", "win32") # Tests to skip on specific targets. # These are tests that are difficult to detect that they should not be run on the given target. platform_tests_to_skip = { - "esp8266": ( - "misc/rge_sm.py", # incorrect values due to object representation C - ), "minimal": ( "basics/class_inplace_op.py", # all special methods not supported "basics/subclass_native_init.py", # native subclassing corner cases not support @@ -788,9 +785,6 @@ def run_tests(pyb, tests, args, result_dir, num_threads=1): skip_tests.add( "float/float2int_intbig.py" ) # requires fp32, there's float2int_fp30_intbig.py instead - skip_tests.add( - "float/string_format.py" - ) # requires fp32, there's string_format_fp30.py instead skip_tests.add("float/bytes_construct.py") # requires fp32 skip_tests.add("float/bytearray_construct.py") # requires fp32 skip_tests.add("float/float_format_ints_power10.py") # requires fp32 |