diff --git a/README.md b/README.md index 6f6746be..7ecfc2ce 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,8 @@ Contributors (0.9.0): Changes (0.9.0): +- [gh-323](https://github.com/flintlib/python-flint/pull/323), + Faster conversion of `fmpz` to `float`. (RO) - [gh-322](https://github.com/flintlib/python-flint/pull/322), Add `mul_low` and `pow_trunc` methods to `fmpz_poly`, `fmpq_poly` and `nmod_poly`. (RO) diff --git a/src/flint/test/test_all.py b/src/flint/test/test_all.py index e82c8f3a..fa0ec927 100644 --- a/src/flint/test/test_all.py +++ b/src/flint/test/test_all.py @@ -207,6 +207,17 @@ def test_fmpz(): assert math.ceil(f) == 2 assert type(math.ceil(f)) is flint.fmpz + # Float conversion should follow Python rounding conventions + for mant in range(2**56 - 64, 2**56 + 64): + for shift in range(0, 900, 20): + for eps in range(-5, 5): + f = (mant << shift) + eps + assert float(f) == float(flint.fmpz(f)) + assert float(-f) == float(flint.fmpz(-f)) + # Test float conversion overflow + assert raises(lambda: float(flint.fmpz(2**1024)), OverflowError) + assert raises(lambda: float(flint.fmpz(-2**1024)), OverflowError) + assert flint.fmpz(2) != [] assert +flint.fmpz(0) == 0 assert +flint.fmpz(1) == 1 diff --git a/src/flint/types/fmpz.pyx b/src/flint/types/fmpz.pyx index 6f3cf677..2b8d4e3c 100644 --- a/src/flint/types/fmpz.pyx +++ b/src/flint/types/fmpz.pyx @@ -3,6 +3,7 @@ from flint.utils.typecheck cimport typecheck from flint.utils.conversion cimport chars_from_str from flint.utils.conversion cimport str_from_chars, _str_trunc cimport libc.stdlib +from libc.math cimport nextafter from flint.flintlib.types.flint cimport FMPZ_REF, FMPZ_TMP, FMPZ_UNKNOWN, COEFF_IS_MPZ from flint.flintlib.functions.flint cimport flint_free @@ -105,6 +106,41 @@ cdef class fmpz(flint_scalar): return fmpz_get_intlong(self.val) def __float__(self): + if not COEFF_IS_MPZ(self.val[0]): + return (self.val[0]) + cdef slong bits = fmpz_bits(self.val) + cdef long tz + cdef double d + cdef int roundup = 0 + if bits <= 1023: + # Known to be representable by a IEEE-754 double + d = fmpz_get_d(self.val) + # fmpz_get_d always rounds towards zero + # Sometimes we need to round away from zero: + # - if the 54-th most significant bit is 1 + # - if further bits are not zero + # - or if all further bits are zero but the 53-th bit is 1 (round-to-even convention) + if fmpz_sgn(self.val) == -1: + # tstbit behaves as if self was represented as 2's complement + # We look for patterns 0.10...0 or x.0xxxxxx + tz = fmpz_val2(self.val) + if bits >= 54: + if fmpz_tstbit(self.val, bits - 54) == 0: + if tz < bits - 54: + roundup = 1 + else: + if tz == bits - 54 and fmpz_tstbit(self.val, bits - 53) == 0: + roundup = 1 + else: + if bits >= 54 and fmpz_tstbit(self.val, bits - 54) == 1: + tz = fmpz_val2(self.val) + if tz < bits - 54 or (tz == bits - 54 and fmpz_tstbit(self.val, bits - 53) == 1): + roundup = 1 + if roundup: + # increase the mantissa of d by 1 + d = nextafter(d, 2 * d) + return d + return float(fmpz_get_intlong(self.val)) def __floor__(self):