circuitpython/tests/float/float_format_accuracy.py
Yoctopuce dev dbbaa959c8 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>
2025-08-01 00:47:33 +10:00

73 lines
2.4 KiB
Python

# 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))