summaryrefslogtreecommitdiff
path: root/tests/float/float_format_accuracy.py
diff options
context:
space:
mode:
authorYoctopuce dev <dev@yoctopuce.com>2025-06-06 14:55:21 +0200
committerDamien George <damien@micropython.org>2025-08-01 00:47:33 +1000
commitdbbaa959c85c04dbbcde5908b5d0775b574e44e7 (patch)
tree050bd1670b061788d291c0d88af22a6aad722f64 /tests/float/float_format_accuracy.py
parente4e1c9f4132f839dac0291557d9b992f67577fd3 (diff)
py/formatfloat: Improve accuracy of float formatting code.
Following discussions in PR #16666, this commit updates the float formatting code to improve the `repr` reversibility, i.e. the percentage of valid floating point numbers that do parse back to the same number when formatted by `repr` (in CPython it's 100%). This new code offers a choice of 3 float conversion methods, depending on the desired tradeoff between code size and conversion precision: - BASIC method is the smallest code footprint - APPROX method uses an iterative method to approximate the exact representation, which is a bit slower but but does not have a big impact on code size. It provides `repr` reversibility on >99.8% of the cases in double precision, and on >98.5% in single precision (except with REPR_C, where reversibility is 100% as the last two bits are not taken into account). - EXACT method uses higher-precision floats during conversion, which provides perfect results but has a higher impact on code size. It is faster than APPROX method, and faster than the CPython equivalent implementation. It is however not available on all compilers when using FLOAT_IMPL_DOUBLE. Here is the table comparing the impact of the three conversion methods on code footprint on PYBV10 (using single-precision floats) and reversibility rate for both single-precision and double-precision floats. The table includes current situation as a baseline for the comparison: PYBV10 REPR_C FLOAT DOUBLE current = 364688 12.9% 27.6% 37.9% basic = 364812 85.6% 60.5% 85.7% approx = 365080 100.0% 98.5% 99.8% exact = 366408 100.0% 100.0% 100.0% Signed-off-by: Yoctopuce dev <dev@yoctopuce.com>
Diffstat (limited to 'tests/float/float_format_accuracy.py')
-rw-r--r--tests/float/float_format_accuracy.py73
1 files changed, 73 insertions, 0 deletions
diff --git a/tests/float/float_format_accuracy.py b/tests/float/float_format_accuracy.py
new file mode 100644
index 000000000..f9467f9c0
--- /dev/null
+++ b/tests/float/float_format_accuracy.py
@@ -0,0 +1,73 @@
+# Test accuracy of `repr` conversions.
+# This test also increases code coverage for corner cases.
+
+try:
+ import array, math, random
+except ImportError:
+ print("SKIP")
+ raise SystemExit
+
+# The largest errors come from seldom used very small numbers, near the
+# limit of the representation. So we keep them out of this test to keep
+# the max relative error display useful.
+if float("1e-100") == 0.0:
+ # single-precision
+ float_type = "f"
+ float_size = 4
+ # testing range
+ min_expo = -96 # i.e. not smaller than 1.0e-29
+ # Expected results (given >=50'000 samples):
+ # - MICROPY_FLTCONV_IMPL_EXACT: 100% exact conversions
+ # - MICROPY_FLTCONV_IMPL_APPROX: >=98.53% exact conversions, max relative error <= 1.01e-7
+ min_success = 0.980 # with only 1200 samples, the success rate is lower
+ max_rel_err = 1.1e-7
+ # REPR_C is typically used with FORMAT_IMPL_BASIC, which has a larger error
+ is_REPR_C = float("1.0000001") == float("1.0")
+ if is_REPR_C: # REPR_C
+ min_success = 0.83
+ max_rel_err = 5.75e-07
+else:
+ # double-precision
+ float_type = "d"
+ float_size = 8
+ # testing range
+ min_expo = -845 # i.e. not smaller than 1.0e-254
+ # Expected results (given >=200'000 samples):
+ # - MICROPY_FLTCONV_IMPL_EXACT: 100% exact conversions
+ # - MICROPY_FLTCONV_IMPL_APPROX: >=99.83% exact conversions, max relative error <= 2.7e-16
+ min_success = 0.997 # with only 1200 samples, the success rate is lower
+ max_rel_err = 2.7e-16
+
+
+# Deterministic pseudorandom generator. Designed to be uniform
+# on mantissa values and exponents, not on the represented number
+def pseudo_randfloat():
+ rnd_buff = bytearray(float_size)
+ for _ in range(float_size):
+ rnd_buff[_] = random.getrandbits(8)
+ return array.array(float_type, rnd_buff)[0]
+
+
+random.seed(42)
+stats = 0
+N = 1200
+max_err = 0
+for _ in range(N):
+ f = pseudo_randfloat()
+ while type(f) is not float or math.isinf(f) or math.isnan(f) or math.frexp(f)[1] <= min_expo:
+ f = pseudo_randfloat()
+
+ str_f = repr(f)
+ f2 = float(str_f)
+ if f2 == f:
+ stats += 1
+ else:
+ error = abs((f2 - f) / f)
+ if max_err < error:
+ max_err = error
+
+print(N, "values converted")
+if stats / N >= min_success and max_err <= max_rel_err:
+ print("float format accuracy OK")
+else:
+ print("FAILED: repr rate=%.3f%% max_err=%.3e" % (100 * stats / N, max_err))