diff options
author | Yoctopuce dev <dev@yoctopuce.com> | 2025-06-06 14:55:21 +0200 |
---|---|---|
committer | Damien George <damien@micropython.org> | 2025-08-01 00:47:33 +1000 |
commit | dbbaa959c85c04dbbcde5908b5d0775b574e44e7 (patch) | |
tree | 050bd1670b061788d291c0d88af22a6aad722f64 /tests/float/float_format_accuracy.py | |
parent | e4e1c9f4132f839dac0291557d9b992f67577fd3 (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.py | 73 |
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)) |