From 0d41c9506cad17cea61086c407f0f9557ba3c103 Mon Sep 17 00:00:00 2001 From: Lorenz Panny Date: Sun, 28 Sep 2025 15:36:28 +0200 Subject: [PATCH 1/8] implement analytic class number formula (product form) for approximating class numbers of negative fundamental discriminants --- src/sage/rings/number_field/order.py | 77 ++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/sage/rings/number_field/order.py b/src/sage/rings/number_field/order.py index 7e2b95e3db5..dc2ee4028d8 100644 --- a/src/sage/rings/number_field/order.py +++ b/src/sage/rings/number_field/order.py @@ -119,6 +119,83 @@ def quadratic_order_class_number(disc): h = pari.qfbclassno(disc) return ZZ(h) +def quadratic_order_approximate_class_number(disc, *, bound=10**4): + r""" + Return *an approximation of* the class number of + the quadratic order of given discriminant. + + Currently only implemented for maximal orders + in imaginary-quadratic fields. + + EXAMPLES:: + + sage: from sage.rings.number_field.order import quadratic_order_approximate_class_number + sage: QuadraticField(-419).class_number() + 9 + sage: quadratic_order_approximate_class_number(-419) + 9.0... + + :: + + sage: from sage.rings.number_field.order import quadratic_order_approximate_class_number + sage: d = 100000000000031 + sage: QuadraticField(-d).class_number(proof=False) + 14414435 + sage: round(quadratic_order_approximate_class_number(-d)) + 144... + sage: round(quadratic_order_approximate_class_number(-d, bound=10**6)) + 1441... + + :: + + sage: from sage.rings.number_field.order import quadratic_order_approximate_class_number + sage: # Test it against the exact class number computed for the CSIDH-512 prime + sage: # Source: https://eprint.iacr.org/2019/498.pdf + sage: p = 4 * prod(primes(3,374)) * 587 - 1 + sage: hreal = 84884147409828091725676728670213067387206838101828807864190286991865870575397 + sage: assert not hreal * BQFClassGroup(-p).random_element() + sage: h = round(quadratic_order_approximate_class_number(-p, bound=10**3)); h + 8... + sage: RR(h / hreal) + 1.00... + sage: h = round(quadratic_order_approximate_class_number(-p, bound=10**4)); h + 84... + sage: RR(h / hreal) + 0.99... + sage: h = round(quadratic_order_approximate_class_number(-p, bound=10**5)); h + 848... + sage: RR(h / hreal) + 0.999... + sage: h = round(quadratic_order_approximate_class_number(-p, bound=10**6)); h # long time -- 2s + 84884... + sage: RR(h / hreal) # long time -- 2s + 1.00000... + + ALGORITHM: Finite approximation of the infinite product given by + the analytic class number formula, using primes up to ``bound``. + """ + disc = ZZ(disc) + if disc >= 0: + raise NotImplementedError('only imaginary-quadratic fields supported') + if not disc.is_fundamental_discriminant(): + raise NotImplementedError('only fundamental discriminants supported') + + from sage.rings.real_mpfr import RealField + from sage.arith.misc import primes, kronecker_symbol + from sage.symbolic.constants import pi + + w = 6 if disc == -3 else 4 if disc == -4 else 2 + RR = RealField(max(53, disc.bit_length())) # wild guess! + + # compute numerator and denominator separately for numerical stability + L1 = L2 = RR(1) + for ell in primes(bound): + L1 *= ell + L2 *= ell - kronecker_symbol(disc, ell) + L = L1 / L2 + + return RR(w * abs(disc).sqrt() * L / (2 * pi)) + class OrderFactory(UniqueFactory): r""" From f3ef13fe61e8c41e7536cd20868297c12456d2e6 Mon Sep 17 00:00:00 2001 From: Lorenz Panny <84067835+yyyyx4@users.noreply.github.com> Date: Thu, 2 Oct 2025 23:03:02 +0200 Subject: [PATCH 2/8] move Python comment into surrounding docstring Co-authored-by: user202729 <25191436+user202729@users.noreply.github.com> --- src/sage/rings/number_field/order.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/sage/rings/number_field/order.py b/src/sage/rings/number_field/order.py index dc2ee4028d8..fa3e7e82140 100644 --- a/src/sage/rings/number_field/order.py +++ b/src/sage/rings/number_field/order.py @@ -146,11 +146,9 @@ def quadratic_order_approximate_class_number(disc, *, bound=10**4): sage: round(quadratic_order_approximate_class_number(-d, bound=10**6)) 1441... - :: + Test it against the exact class number computed for the CSIDH-512 prime (source: https://eprint.iacr.org/2019/498.pdf):: sage: from sage.rings.number_field.order import quadratic_order_approximate_class_number - sage: # Test it against the exact class number computed for the CSIDH-512 prime - sage: # Source: https://eprint.iacr.org/2019/498.pdf sage: p = 4 * prod(primes(3,374)) * 587 - 1 sage: hreal = 84884147409828091725676728670213067387206838101828807864190286991865870575397 sage: assert not hreal * BQFClassGroup(-p).random_element() From bd5bba85ed6e3fbf06eb74da73a05466a9339b83 Mon Sep 17 00:00:00 2001 From: Lorenz Panny Date: Thu, 2 Oct 2025 23:11:39 +0200 Subject: [PATCH 3/8] speed is a stronger reason for this --- src/sage/rings/number_field/order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/rings/number_field/order.py b/src/sage/rings/number_field/order.py index fa3e7e82140..37f815f5d60 100644 --- a/src/sage/rings/number_field/order.py +++ b/src/sage/rings/number_field/order.py @@ -185,7 +185,7 @@ def quadratic_order_approximate_class_number(disc, *, bound=10**4): w = 6 if disc == -3 else 4 if disc == -4 else 2 RR = RealField(max(53, disc.bit_length())) # wild guess! - # compute numerator and denominator separately for numerical stability + # compute numerator and denominator separately for speed L1 = L2 = RR(1) for ell in primes(bound): L1 *= ell From 5c592beb4998fafa81a4d9adca7e14da6b068d0a Mon Sep 17 00:00:00 2001 From: Lorenz Panny Date: Wed, 8 Oct 2025 12:57:54 +0200 Subject: [PATCH 4/8] make linter happier --- src/sage/rings/number_field/order.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sage/rings/number_field/order.py b/src/sage/rings/number_field/order.py index 37f815f5d60..f70843848bb 100644 --- a/src/sage/rings/number_field/order.py +++ b/src/sage/rings/number_field/order.py @@ -119,6 +119,7 @@ def quadratic_order_class_number(disc): h = pari.qfbclassno(disc) return ZZ(h) + def quadratic_order_approximate_class_number(disc, *, bound=10**4): r""" Return *an approximation of* the class number of From af304115eb10627750fe1de2f9a78f739fec7c39 Mon Sep 17 00:00:00 2001 From: Lorenz Panny Date: Wed, 8 Oct 2025 13:00:30 +0200 Subject: [PATCH 5/8] hardcode current outputs in example --- src/sage/rings/number_field/order.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/sage/rings/number_field/order.py b/src/sage/rings/number_field/order.py index f70843848bb..d37c69f3e2a 100644 --- a/src/sage/rings/number_field/order.py +++ b/src/sage/rings/number_field/order.py @@ -143,9 +143,9 @@ def quadratic_order_approximate_class_number(disc, *, bound=10**4): sage: QuadraticField(-d).class_number(proof=False) 14414435 sage: round(quadratic_order_approximate_class_number(-d)) - 144... + 14407657 sage: round(quadratic_order_approximate_class_number(-d, bound=10**6)) - 1441... + 14413626 Test it against the exact class number computed for the CSIDH-512 prime (source: https://eprint.iacr.org/2019/498.pdf):: @@ -154,21 +154,21 @@ def quadratic_order_approximate_class_number(disc, *, bound=10**4): sage: hreal = 84884147409828091725676728670213067387206838101828807864190286991865870575397 sage: assert not hreal * BQFClassGroup(-p).random_element() sage: h = round(quadratic_order_approximate_class_number(-p, bound=10**3)); h - 8... + 85020334529027955331134025285584937708277525762952749243936367281020276898911 sage: RR(h / hreal) - 1.00... + 1.00160438813790 sage: h = round(quadratic_order_approximate_class_number(-p, bound=10**4)); h - 84... + 84396416322932187013685015858232028818143153418602750494380687212063263982976 sage: RR(h / hreal) - 0.99... + 0.994254155790231 sage: h = round(quadratic_order_approximate_class_number(-p, bound=10**5)); h - 848... + 84823787383264935642168065590216697209124465727576867303448690101681967969348 sage: RR(h / hreal) - 0.999... + 0.999288912848806 sage: h = round(quadratic_order_approximate_class_number(-p, bound=10**6)); h # long time -- 2s - 84884... + 84884627342209070883738394179700676127184729325091471467872632417652836154863 sage: RR(h / hreal) # long time -- 2s - 1.00000... + 1.00000565396951 ALGORITHM: Finite approximation of the infinite product given by the analytic class number formula, using primes up to ``bound``. From 24320dbab03456d13aa4c452e284e82f30f2f4ae Mon Sep 17 00:00:00 2001 From: Lorenz Panny Date: Wed, 8 Oct 2025 13:13:23 +0200 Subject: [PATCH 6/8] add tolerances to doctests --- src/sage/rings/number_field/order.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/sage/rings/number_field/order.py b/src/sage/rings/number_field/order.py index d37c69f3e2a..3cdeab6b16e 100644 --- a/src/sage/rings/number_field/order.py +++ b/src/sage/rings/number_field/order.py @@ -142,9 +142,9 @@ def quadratic_order_approximate_class_number(disc, *, bound=10**4): sage: d = 100000000000031 sage: QuadraticField(-d).class_number(proof=False) 14414435 - sage: round(quadratic_order_approximate_class_number(-d)) + sage: round(quadratic_order_approximate_class_number(-d)) # rel tol .01 14407657 - sage: round(quadratic_order_approximate_class_number(-d, bound=10**6)) + sage: round(quadratic_order_approximate_class_number(-d, bound=10**6)) # rel tol .01 14413626 Test it against the exact class number computed for the CSIDH-512 prime (source: https://eprint.iacr.org/2019/498.pdf):: @@ -153,21 +153,21 @@ def quadratic_order_approximate_class_number(disc, *, bound=10**4): sage: p = 4 * prod(primes(3,374)) * 587 - 1 sage: hreal = 84884147409828091725676728670213067387206838101828807864190286991865870575397 sage: assert not hreal * BQFClassGroup(-p).random_element() - sage: h = round(quadratic_order_approximate_class_number(-p, bound=10**3)); h + sage: h = round(quadratic_order_approximate_class_number(-p, bound=10**3)); h # rel tol .01 85020334529027955331134025285584937708277525762952749243936367281020276898911 - sage: RR(h / hreal) + sage: RR(h / hreal) # abs tol .01 1.00160438813790 - sage: h = round(quadratic_order_approximate_class_number(-p, bound=10**4)); h + sage: h = round(quadratic_order_approximate_class_number(-p, bound=10**4)); h # rel tol .01 84396416322932187013685015858232028818143153418602750494380687212063263982976 - sage: RR(h / hreal) + sage: RR(h / hreal) # abs tol .01 0.994254155790231 - sage: h = round(quadratic_order_approximate_class_number(-p, bound=10**5)); h + sage: h = round(quadratic_order_approximate_class_number(-p, bound=10**5)); h # rel tol .01 84823787383264935642168065590216697209124465727576867303448690101681967969348 - sage: RR(h / hreal) + sage: RR(h / hreal) # abs tol .01 0.999288912848806 - sage: h = round(quadratic_order_approximate_class_number(-p, bound=10**6)); h # long time -- 2s + sage: h = round(quadratic_order_approximate_class_number(-p, bound=10**6)); h # rel tol .01, long time (2s) 84884627342209070883738394179700676127184729325091471467872632417652836154863 - sage: RR(h / hreal) # long time -- 2s + sage: RR(h / hreal) # abs tol .01, long time (2s) 1.00000565396951 ALGORITHM: Finite approximation of the infinite product given by From c0c9b3abb55ac53e7b01a83d34bdd98f94be1ae1 Mon Sep 17 00:00:00 2001 From: Lorenz Panny Date: Wed, 8 Oct 2025 13:18:05 +0200 Subject: [PATCH 7/8] refer to new function in documentation of .class_number() --- src/sage/rings/number_field/order.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/sage/rings/number_field/order.py b/src/sage/rings/number_field/order.py index 3cdeab6b16e..93504e4a419 100644 --- a/src/sage/rings/number_field/order.py +++ b/src/sage/rings/number_field/order.py @@ -1167,6 +1167,15 @@ def class_number(self, proof=None): r""" Return the class number of this order. + .. NOTE:: + + For some applications (e.g., in algorithms for computing + class groups) it is required to merely *approximate* the + class number. The function + :func:`quadratic_order_approximate_class_number` + can be used to compute such an approximation (currently + restricted to maximal imaginary-quadratic orders). + EXAMPLES:: sage: ZZ[2^(1/3)].class_number() # needs sage.symbolic From 8bf00ec18770ed1212fbb467f3b9b9093950bc61 Mon Sep 17 00:00:00 2001 From: Lorenz Panny Date: Wed, 8 Oct 2025 14:15:33 +0200 Subject: [PATCH 8/8] update one more doctest --- src/sage/rings/number_field/order.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sage/rings/number_field/order.py b/src/sage/rings/number_field/order.py index 93504e4a419..025c8bd7a28 100644 --- a/src/sage/rings/number_field/order.py +++ b/src/sage/rings/number_field/order.py @@ -133,8 +133,8 @@ def quadratic_order_approximate_class_number(disc, *, bound=10**4): sage: from sage.rings.number_field.order import quadratic_order_approximate_class_number sage: QuadraticField(-419).class_number() 9 - sage: quadratic_order_approximate_class_number(-419) - 9.0... + sage: quadratic_order_approximate_class_number(-419) # rel tol .01 + 9.01653836091712 ::