From 4da6b11053b3b8d25eec2ce51a9d1ceaae74260d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Oudompheng?= Date: Sun, 7 Sep 2025 10:15:04 +0200 Subject: [PATCH 1/6] More efficient conversion of fmpz to float --- src/flint/test/test_all.py | 4 ++++ src/flint/types/fmpz.pyx | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/flint/test/test_all.py b/src/flint/test/test_all.py index e82c8f3a..81575188 100644 --- a/src/flint/test/test_all.py +++ b/src/flint/test/test_all.py @@ -207,6 +207,10 @@ def test_fmpz(): assert math.ceil(f) == 2 assert type(math.ceil(f)) is flint.fmpz + # 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..ccc561b6 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 +import math from flint.flintlib.types.flint cimport FMPZ_REF, FMPZ_TMP, FMPZ_UNKNOWN, COEFF_IS_MPZ from flint.flintlib.functions.flint cimport flint_free @@ -105,7 +106,11 @@ cdef class fmpz(flint_scalar): return fmpz_get_intlong(self.val) def __float__(self): - return float(fmpz_get_intlong(self.val)) + cdef slong exp + # fmpz_get_d_2exp is always accurate + # math.ldexp handles overflow checks + cdef double d = fmpz_get_d_2exp(&exp, self.val) + return math.ldexp(d, exp) def __floor__(self): return self From b3656ca76e879f2e4f771f2b55333f0ddb0bb90d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Oudompheng?= Date: Sun, 7 Sep 2025 10:56:17 +0200 Subject: [PATCH 2/6] Add fast path for float conversion of numbers below 1023 bits --- src/flint/types/fmpz.pyx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/flint/types/fmpz.pyx b/src/flint/types/fmpz.pyx index ccc561b6..2eb3b2ec 100644 --- a/src/flint/types/fmpz.pyx +++ b/src/flint/types/fmpz.pyx @@ -106,6 +106,10 @@ cdef class fmpz(flint_scalar): return fmpz_get_intlong(self.val) def __float__(self): + if fmpz_bits(self.val) <= 1023: + # Known to be representable by a IEEE-754 double + return fmpz_get_d(self.val) + cdef slong exp # fmpz_get_d_2exp is always accurate # math.ldexp handles overflow checks From 9382a42dc47320e7642f9965b46b9f75059c1b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Oudompheng?= Date: Sun, 7 Sep 2025 16:18:23 +0200 Subject: [PATCH 3/6] Implement round-to-even compliant rounding --- src/flint/types/fmpz.pyx | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/src/flint/types/fmpz.pyx b/src/flint/types/fmpz.pyx index 2eb3b2ec..09da7a13 100644 --- a/src/flint/types/fmpz.pyx +++ b/src/flint/types/fmpz.pyx @@ -3,7 +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 -import math +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 @@ -106,15 +106,37 @@ cdef class fmpz(flint_scalar): return fmpz_get_intlong(self.val) def __float__(self): - if fmpz_bits(self.val) <= 1023: + cdef slong bits = fmpz_bits(self.val) + cdef long tz + cdef double d + cdef fmpz_struct xabs[1] + cdef int roundup = 0 + if bits <= 1023: # Known to be representable by a IEEE-754 double - return fmpz_get_d(self.val) - - cdef slong exp - # fmpz_get_d_2exp is always accurate - # math.ldexp handles overflow checks - cdef double d = fmpz_get_d_2exp(&exp, self.val) - return math.ldexp(d, exp) + 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) + tz = fmpz_val2(self.val) + if fmpz_sgn(self.val) == -1: + fmpz_init(xabs) + fmpz_abs(xabs, self.val) + if bits >= 54 and fmpz_tstbit(xabs, bits - 54) == 1 and \ + (tz < bits - 54 or (tz == bits - 54 and fmpz_tstbit(xabs, bits - 53) == 1)): + roundup = 1 + fmpz_clear(xabs) + else: + if bits >= 54 and fmpz_tstbit(self.val, bits - 54) == 1 and \ + (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): return self From 91dc079c69ecccb2ad50e35c236e0e8da7abd218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Oudompheng?= Date: Sun, 7 Sep 2025 18:04:17 +0200 Subject: [PATCH 4/6] Additional fmpz to float conversion tests --- src/flint/test/test_all.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/flint/test/test_all.py b/src/flint/test/test_all.py index 81575188..fa0ec927 100644 --- a/src/flint/test/test_all.py +++ b/src/flint/test/test_all.py @@ -207,6 +207,13 @@ 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) From 5896ada7c9bd881531b61aa9784b7573211c1388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Oudompheng?= Date: Sun, 7 Sep 2025 22:30:47 +0200 Subject: [PATCH 5/6] Improve conversion of small or negative fmpz to float --- src/flint/types/fmpz.pyx | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/flint/types/fmpz.pyx b/src/flint/types/fmpz.pyx index 09da7a13..2b8d4e3c 100644 --- a/src/flint/types/fmpz.pyx +++ b/src/flint/types/fmpz.pyx @@ -106,10 +106,11 @@ 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 fmpz_struct xabs[1] cdef int roundup = 0 if bits <= 1023: # Known to be representable by a IEEE-754 double @@ -119,18 +120,22 @@ cdef class fmpz(flint_scalar): # - 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) - tz = fmpz_val2(self.val) if fmpz_sgn(self.val) == -1: - fmpz_init(xabs) - fmpz_abs(xabs, self.val) - if bits >= 54 and fmpz_tstbit(xabs, bits - 54) == 1 and \ - (tz < bits - 54 or (tz == bits - 54 and fmpz_tstbit(xabs, bits - 53) == 1)): - roundup = 1 - fmpz_clear(xabs) + # 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 and \ - (tz < bits - 54 or (tz == bits - 54 and fmpz_tstbit(self.val, bits - 53) == 1)): - roundup = 1 + 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) From 6a5370c99b8d905d1522b163c584978c35f4fb76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Oudompheng?= Date: Sun, 7 Sep 2025 22:31:44 +0200 Subject: [PATCH 6/6] Update release notes --- README.md | 2 ++ 1 file changed, 2 insertions(+) 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)