From 60d683e3dc2ff03376fff875652a724958ed6131 Mon Sep 17 00:00:00 2001 From: Lorenz Panny Date: Mon, 13 Mar 2023 15:24:34 +0800 Subject: [PATCH 1/3] add class groups of binary quadratic forms --- .../en/reference/quadratic_forms/index.rst | 1 + src/sage/quadratic_forms/all.py | 2 + src/sage/quadratic_forms/binary_qf.py | 112 ++++ src/sage/quadratic_forms/bqf_class_group.py | 593 ++++++++++++++++++ 4 files changed, 708 insertions(+) create mode 100644 src/sage/quadratic_forms/bqf_class_group.py diff --git a/src/doc/en/reference/quadratic_forms/index.rst b/src/doc/en/reference/quadratic_forms/index.rst index 7169e7ac503..e553ecd1b82 100644 --- a/src/doc/en/reference/quadratic_forms/index.rst +++ b/src/doc/en/reference/quadratic_forms/index.rst @@ -6,6 +6,7 @@ Quadratic Forms sage/quadratic_forms/quadratic_form sage/quadratic_forms/binary_qf + sage/quadratic_forms/bqf_class_group sage/quadratic_forms/constructions sage/quadratic_forms/random_quadraticform sage/quadratic_forms/special_values diff --git a/src/sage/quadratic_forms/all.py b/src/sage/quadratic_forms/all.py index 3445705978a..81d395f375c 100644 --- a/src/sage/quadratic_forms/all.py +++ b/src/sage/quadratic_forms/all.py @@ -1,5 +1,7 @@ from .binary_qf import BinaryQF, BinaryQF_reduced_representatives +from .bqf_class_group import BQFClassGroup + from .ternary_qf import TernaryQF, find_all_ternary_qf_by_level_disc, find_a_ternary_qf_by_level_disc from .quadratic_form import QuadraticForm, DiagonalQuadraticForm, quadratic_form_from_invariants diff --git a/src/sage/quadratic_forms/binary_qf.py b/src/sage/quadratic_forms/binary_qf.py index 6516888e3ac..14a1f0049db 100755 --- a/src/sage/quadratic_forms/binary_qf.py +++ b/src/sage/quadratic_forms/binary_qf.py @@ -179,6 +179,101 @@ def _pari_init_(self): """ return 'Qfb(%s,%s,%s)' % (self._a, self._b, self._c) + @staticmethod + def principal(D): + r""" + Return the principal binary quadratic form of the given discriminant. + + EXAMPLES:: + + sage: BinaryQF.principal(8) + x^2 - 2*y^2 + sage: BinaryQF.principal(5) + x^2 + x*y - y^2 + sage: BinaryQF.principal(4) + x^2 - y^2 + sage: BinaryQF.principal(1) + x^2 + x*y + sage: BinaryQF.principal(-3) + x^2 + x*y + y^2 + sage: BinaryQF.principal(-4) + x^2 + y^2 + sage: BinaryQF.principal(-7) + x^2 + x*y + 2*y^2 + sage: BinaryQF.principal(-8) + x^2 + 2*y^2 + + TESTS: + + Some randomized testing:: + + sage: D = 1 + sage: while D.is_square(): + ....: D = choice((-4,+4)) * randrange(9999) + randrange(2) + sage: Q = BinaryQF.principal(D) + sage: Q.discriminant() == D # correct discriminant + True + sage: (Q*Q).is_equivalent(Q) # idempotent (hence identity) + True + """ + D = ZZ(D) + D4 = D % 4 + if D4 not in (0,1): + raise ValueError('discriminant must be congruent to 0 or 1 modulo 4') + return BinaryQF([1, D4, (D4-D)//4]) + + @staticmethod + def random(D): + r""" + Return a somewhat random primitive binary quadratic form of the + given discriminant. + + (In the case `D < 0`, only positive definite forms are returned.) + + .. NOTE:: + + No guarantees are being made about the distribution of forms + sampled by this function. + + EXAMPLES:: + + sage: BinaryQF.random(5) # random + 448219*x^2 - 597179*x*y + 198911*y^2 + sage: BinaryQF.random(-7) # random + 10007*x^2 + 10107*x*y + 2552*y^2 + + TESTS:: + + sage: D = choice((-4,+4)) * randrange(9999) + randrange(2) or 1 + sage: Q = BinaryQF.random(D) + sage: Q.discriminant() == D + True + sage: Q.is_primitive() + True + sage: Q.is_indefinite() or Q.is_positive_definite() + True + """ + D = ZZ(D) + D4 = D % 4 + if D4 not in (0,1): + raise ValueError('discriminant must be congruent to 0 or 1 modulo 4') + + from sage.misc.prandom import randrange + from sage.misc.misc_c import prod + from sage.matrix.special import random_matrix + B = (D.abs() or 1) * 99 # kind of arbitrary + while True: + b = randrange(D4, B, 2) + ac = (b**2 - D) // 4 + if ac: + break + a = prod(l**randrange(e+1) for l,e in ac.factor()) + a = a.prime_to_m_part(gcd([a, b, ac//a])) + c = ac // a + M = random_matrix(ZZ, 2, 2, 'unimodular') + M.rescale_row(0, (-1)**randrange(2)) + return BinaryQF([a, b, c]) * M + def __mul__(self, right): """ Gauss composition or right action by a 2x2 integer matrix. @@ -1706,6 +1801,23 @@ def solve_integer(self, n, *, algorithm="general"): sol = self.__pari__().qfbsolve(n, flag) return tuple(map(ZZ, sol)) if sol else None + def form_class(self): + r""" + Return the class of this form modulo equivalence. + + EXAMPLES:: + + sage: F = BinaryQF([3, -16, 161]) + sage: cl = F.form_class(); cl + Class of 3*x^2 + 2*x*y + 140*y^2 + sage: cl.parent() + Form Class Group of Discriminant -1676 + sage: cl.parent() is BQFClassGroup(-4*419) + True + """ + from sage.quadratic_forms.bqf_class_group import BQFClassGroup + return BQFClassGroup(self.discriminant())(self) + def BinaryQF_reduced_representatives(D, primitive_only=False, proper=True): r""" diff --git a/src/sage/quadratic_forms/bqf_class_group.py b/src/sage/quadratic_forms/bqf_class_group.py new file mode 100644 index 00000000000..58cd10468bf --- /dev/null +++ b/src/sage/quadratic_forms/bqf_class_group.py @@ -0,0 +1,593 @@ +r""" +Class groups of binary quadratic forms + +EXAMPLES: + +Constructing the class of a given binary quadratic form is straightforward:: + + sage: F1 = BinaryQF([22, 91, 99]) + sage: cl1 = F1.form_class(); cl1 + Class of 5*x^2 - 3*x*y + 22*y^2 + +Every class is represented by a reduced form in it:: + + sage: cl1.form() + 5*x^2 - 3*x*y + 22*y^2 + sage: cl1.form() == F1.reduced_form() + True + +Addition of form classes and derived operations are defined by composition +of binary quadratic forms:: + + sage: F2 = BinaryQF([4, 1, 27]) + sage: cl2 = F2.form_class(); cl2 + Class of 4*x^2 + x*y + 27*y^2 + sage: cl1 + cl2 + Class of 9*x^2 + x*y + 12*y^2 + sage: cl1 + cl2 == (F1 * F2).form_class() + True + sage: -cl1 + Class of 5*x^2 + 3*x*y + 22*y^2 + sage: cl1 - cl1 + Class of x^2 + x*y + 108*y^2 + +The form class group can be constructed as an explicit parent object:: + + sage: F1.discriminant() + -431 + sage: Cl = BQFClassGroup(-431); Cl + Form Class Group of Discriminant -431 + sage: cl1.parent() is Cl + True + sage: Cl(F1) == cl1 + True + +Structural properties of the form class group, such as the class number, +the group invariants, and element orders, can be computed:: + + sage: Cl.order() + 21 + sage: cl1 * Cl.order() == Cl.zero() + True + sage: cl2 * Cl.order() == Cl.zero() + True + sage: cl2.order() + 7 + sage: cl2 * cl2.order() == Cl.zero() + True + sage: Cl.abelian_group() + Additive abelian group isomorphic to Z/21 embedded in Form Class Group of Discriminant -431 + sage: Cl.gens() # random + [Class of 5*x^2 + 3*x*y + 22*y^2] + sage: Cl.gens()[0].order() + 21 + +AUTHORS: + +- Lorenz Panny (2023) +""" + +# **************************************************************************** +# Copyright (C) 2023 Lorenz Panny +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# https://www.gnu.org/licenses/ +# **************************************************************************** + +from sage.misc.cachefunc import cached_method + +from sage.structure.parent import Parent +from sage.structure.unique_representation import UniqueRepresentation +from sage.structure.element import AdditiveGroupElement + +from sage.misc.prandom import randrange +from sage.rings.integer_ring import ZZ +from sage.groups.generic import order_from_multiple, multiple +from sage.groups.additive_abelian.additive_abelian_wrapper import AdditiveAbelianGroupWrapper +from sage.quadratic_forms.binary_qf import BinaryQF + +from sage.libs.pari import pari + + +class BQFClassGroup(Parent, UniqueRepresentation): + r""" + This type represents the class group for a given discriminant `D`. + + - For `D < 0`, the group is the class group of *positive definite* + binary quadratic forms. The "full" form class group is the direct + sum of two isomorphic copies of this group (one for positive + definite forms and one for negative definite forms). + + - For `D > 0`, this functionality is currently not implemented. + + EXAMPLES:: + + sage: BQFClassGroup(-4) + Form Class Group of Discriminant -4 + sage: BQFClassGroup(-6) + Traceback (most recent call last): + ... + ValueError: not a discriminant + + The discriminant need not be fundamental:: + + sage: BQFClassGroup(-22^2) + Form Class Group of Discriminant -484 + """ + + def __init__(self, D, *, check=True): + r""" + Construct the class group for a given discriminant `D`. + + TESTS: + + Check that positive discriminants are rejected until code is + written for them:: + + sage: BQFClassGroup(101) + Traceback (most recent call last): + ... + NotImplementedError: positive discriminants are not yet supported + """ + self._disc = ZZ(D) + if check: + if not self._disc or self._disc % 4 not in (0,1): + raise ValueError('not a discriminant') + if self._disc > 0: + raise NotImplementedError('positive discriminants are not yet supported') + super().__init__() + + def _element_constructor_(self, F, *, check=True): + r""" + Construct an element of this form class group as a :class:`BQFClassGroup_element`. + + EXAMPLES:: + + sage: Cl = BQFClassGroup(-999) + sage: Cl(0) # indirect doctest + Class of x^2 + x*y + 250*y^2 + sage: Cl(BinaryQF([16, 5, 16])) # indirect doctest + Class of 16*x^2 + 5*x*y + 16*y^2 + """ + if isinstance(F, BQFClassGroup_element): + if check and F.parent() is not self: # class group is unique parent + raise ValueError('quadratic form has incorrect discriminant') + return F + if F == 0: + return self.zero() + if check and not isinstance(F, BinaryQF): + raise TypeError('not a binary quadratic form') + return BQFClassGroup_element(F, parent=self, check=check) + + def zero(self): + r""" + Return the neutral element of this group, i.e., the class of the + principal binary quadratic form of the respective discriminant. + + EXAMPLES:: + + sage: Cl = BQFClassGroup(-999) + sage: cl = Cl.zero(); cl + Class of x^2 + x*y + 250*y^2 + sage: cl + cl == cl + True + """ + return self(BinaryQF.principal(self._disc)) + + def random_element(self): + r""" + Return a somewhat random element of this form class group. + + ALGORITHM: :meth:`BinaryQF.random` + + .. NOTE:: + + No guarantees are being made about the distribution of classes + sampled by this function. Heuristically, however, it should be + fairly close to uniform. + + EXAMPLES:: + + sage: Cl = BQFClassGroup(-999); Cl + Form Class Group of Discriminant -999 + sage: cl = Cl.random_element(); cl # random + Class of 10*x^2 + x*y + 25*y^2 + sage: cl.form().discriminant() + -999 + """ + return self(BinaryQF.random(self._disc)) + + def __hash__(self): + r""" + Return a hash value for this form class group. + + EXAMPLES:: + + sage: hash(BQFClassGroup(-999)) # random + -4246560339810542104 + """ + return hash(('BQFClassGroup', self._disc)) + + def _repr_(self): + r""" + Return a string describing this form class group. + + EXAMPLES:: + + sage: BQFClassGroup(-999) # indirect doctest + Form Class Group of Discriminant -999 + """ + return f'Form Class Group of Discriminant {self._disc}' + + def discriminant(self): + r""" + Return the discriminant of the forms in this form class group. + + EXAMPLES:: + + sage: BQFClassGroup(-999).discriminant() + -999 + """ + return self._disc + + @cached_method + def order(self): + r""" + Return the order of this form class group (the *class number*). + + ALGORITHM: :pari:`qfbclassno` + + EXAMPLES:: + + sage: BQFClassGroup(-4).order() + 1 + sage: BQFClassGroup(-11).order() + 1 + sage: BQFClassGroup(-67).order() + 1 + sage: BQFClassGroup(-163).order() + 1 + sage: BQFClassGroup(-999).order() + 24 + sage: BQFClassGroup(-9999).order() + 88 + sage: BQFClassGroup(-99999).order() + 224 + """ + return ZZ(pari.qfbclassno(self._disc)) + + cardinality = order + + @cached_method + def abelian_group(self): + r""" + Return the structure of this form class group as an + :class:`AdditiveAbelianGroupWrapper` object. + + ALGORITHM: :pari:`quadclassunit` + + EXAMPLES:: + + sage: Cl = BQFClassGroup(-4*777) + sage: Cl.order() + 16 + sage: G = Cl.abelian_group(); G + Additive abelian group isomorphic to Z/4 + Z/2 + Z/2 embedded in Form Class Group of Discriminant -3108 + sage: G.gens() # random + (Class of 11*x^2 + 4*x*y + 71*y^2, + Class of 6*x^2 + 6*x*y + 131*y^2, + Class of 2*x^2 + 2*x*y + 389*y^2) + sage: [g.order() for g in G.gens()] + [4, 2, 2] + sage: G.discrete_log(Cl.random_element()) # random + (3, 0, 1) + """ + h, ords, gens, reg = pari.quadclassunit(self._disc) + ords = [ZZ(o) for o in ords] + gens = [BinaryQF(g) for g in gens] + return AdditiveAbelianGroupWrapper(self, gens, ords) + + def gens(self): + r""" + Return a generating set of this form class group. + + EXAMPLES:: + + sage: Cl = BQFClassGroup(-4*419) + sage: Cl.gens() + [Class of 3*x^2 + 2*x*y + 140*y^2] + + :: + + sage: Cl = BQFClassGroup(-4*777) + sage: Cl.gens() # random + [Class of 11*x^2 + 4*x*y + 71*y^2, + Class of 6*x^2 + 6*x*y + 131*y^2, + Class of 2*x^2 + 2*x*y + 389*y^2] + """ + return [g.element() for g in self.abelian_group().gens()] + + +class BQFClassGroup_element(AdditiveGroupElement): + r""" + This type represents elements of class groups of binary quadratic forms. + + Users should not need to construct objects of this type directly; it can + be accessed via either the :class:`BQFClassGroup` parent object or the + :meth:`~BinaryQF.form_class` method associated to binary quadratic forms. + + Currently only classes of positive definite forms are supported. + + EXAMPLES:: + + sage: F = BinaryQF([22, 91, 99]) + sage: F.form_class() # implicit doctest + Class of 5*x^2 - 3*x*y + 22*y^2 + + :: + + sage: Cl = BQFClassGroup(-4*419) + sage: Cl.zero() + Class of x^2 + 419*y^2 + sage: Cl.gens()[0] # implicit doctest + Class of 3*x^2 + 2*x*y + 140*y^2 + """ + + def __init__(self, F, parent, *, check=True, reduce=True): + r""" + Constructor for classes of binary quadratic forms. + + EXAMPLES:: + + sage: Cl = BQFClassGroup(-431) + sage: F = BinaryQF([22, 91, 99]) + sage: from sage.quadratic_forms.bqf_class_group import BQFClassGroup_element + sage: BQFClassGroup_element(F, parent=Cl) + Class of 5*x^2 - 3*x*y + 22*y^2 + """ + if check: + if not isinstance(F, BinaryQF): + raise TypeError('not a binary quadratic form') + if F.discriminant() != parent.discriminant(): + raise ValueError('given quadratic form has wrong discriminant') + if not F.is_primitive(): + raise ValueError('given quadratic form is not primitive') + if not F.is_positive_definite(): + raise NotImplemented('only positive definite forms are currently supported') + if reduce: + F = F.reduced_form() + self._form = F + super().__init__(parent=parent) + + def form(self): + r""" + Return a reduced quadratic form in this class. + + (For `D < 0`, each class contains a *unique* reduced form.) + + EXAMPLES:: + + sage: F = BinaryQF([3221, 2114, 350]) + sage: cl = F.form_class() + sage: cl.form() + 29*x^2 + 14*x*y + 350*y^2 + sage: cl.form() == F.reduced_form() + True + """ + return self._form + + def _neg_(self): + r""" + Return the inverse of this form class. + + The inverse class of a form `[a,b,c]` is represented by `[a,-b,c]`. + + EXAMPLES:: + + sage: F = BinaryQF([11,21,31]) + sage: cl = F.form_class(); cl + Class of 11*x^2 - x*y + 21*y^2 + sage: -cl # indirect doctest + Class of 11*x^2 + x*y + 21*y^2 + sage: cl + (-cl) == cl.parent().zero() # indirect doctest + True + """ + a,b,c = self._form + F = BinaryQF([a,-b,c]) + return BQFClassGroup_element(F, parent=self.parent()) + + def _add_(self, other): + r""" + Return the composition of two form classes. + + EXAMPLES:: + + sage: cl1 = BinaryQF([11,21,31]).form_class(); cl1 + Class of 11*x^2 - x*y + 21*y^2 + sage: cl2 = BinaryQF([7,55,141]).form_class(); cl2 + Class of 7*x^2 - x*y + 33*y^2 + sage: cl1 + cl2 # indirect doctest + Class of 3*x^2 + x*y + 77*y^2 + """ + F = self._form * other._form + return BQFClassGroup_element(F, parent=self.parent()) + + def _sub_(self, other): + r""" + Return the composition of a form class with the inverse of another. + + EXAMPLES:: + + sage: cl1 = BinaryQF([11,21,31]).form_class(); cl1 + Class of 11*x^2 - x*y + 21*y^2 + sage: cl2 = BinaryQF([7,55,141]).form_class(); cl2 + Class of 7*x^2 - x*y + 33*y^2 + sage: cl1 - cl2 # indirect doctest + Class of 9*x^2 - 7*x*y + 27*y^2 + sage: cl1 - cl2 == cl1 + (-cl2) # indirect doctest + True + """ + return self + (-other) + + def __mul__(self, other): + r""" + Return an integer multiple of this form class with respect to + repeated composition. + + ALGORITHM: :func:`multiple` + + EXAMPLES:: + + sage: F = BinaryQF([11,21,31]) + sage: cl = F.form_class(); cl + Class of 11*x^2 - x*y + 21*y^2 + sage: cl*0 == cl.parent().zero() # indirect doctest + True + sage: cl*1 == cl # indirect doctest + True + sage: cl*(-1) == -cl # indirect doctest + True + sage: cl*2 == cl + cl # indirect doctest + True + sage: cl*5 == cl + cl + cl + cl + cl # indirect doctest + True + sage: 5*cl == cl*5 # indirect doctest + True + sage: cl*(-5) == -(5*cl) # indirect doctest + True + """ + return multiple(self, other, operation='+') + + __rmul__ = __mul__ + + def __eq__(self, other): + r""" + Test two form classes for equality. + + EXAMPLES:: + + sage: F = BinaryQF([11,21,31]) + sage: cl = F.form_class(); cl + Class of 11*x^2 - x*y + 21*y^2 + sage: cl == cl # indirect doctest + True + sage: -cl == cl # indirect doctest + False + """ + # When generalizing to positive discriminants in the future, keep + # in mind that for indefinite forms there can be multiple reduced + # forms per class. This also affects the other comparison methods + # as well as hashing. + return self._form == other._form + + def __ne__(self, other): + r""" + Test two form classes for inequality. + + EXAMPLES:: + + sage: F = BinaryQF([11,21,31]) + sage: cl = F.form_class(); cl + Class of 11*x^2 - x*y + 21*y^2 + sage: cl != cl # indirect doctest + False + sage: -cl != cl # indirect doctest + True + """ + return self._form != other._form + + def __lt__(self, other): + r""" + Compare two form classes according to the lexicographic ordering + on their coefficient lists. + + EXAMPLES:: + + sage: cl1 = BinaryQF([7,55,141]).form_class(); cl1 + Class of 7*x^2 - x*y + 33*y^2 + sage: cl2 = BinaryQF([11,21,31]).form_class(); cl2 + Class of 11*x^2 - x*y + 21*y^2 + sage: cl1 < cl2 # indirect doctest + True + sage: cl1 > cl2 # indirect doctest + False + """ + return self._form < other._form + + def __bool__(self): + r""" + Return ``True`` if this form class is *not* the principal class + and ``False`` otherwise. + + EXAMPLES:: + + sage: cl = BinaryQF([11,21,31]).form_class() + sage: bool(cl) + True + sage: bool(0*cl) + False + """ + return self != self.parent().zero() + + def is_zero(self): + r""" + Return ``True`` if this form class is the principal class and + ``False`` otherwise. + + EXAMPLES:: + + sage: cl = BinaryQF([11,21,31]).form_class() + sage: cl.is_zero() + False + sage: (0*cl).is_zero() + True + """ + return not self + + def __repr__(self): + r""" + Return a string representation of this form class. + + EXAMPLES:: + + sage: F = BinaryQF([11,21,31]) + sage: F.form_class() # indirect doctest + Class of 11*x^2 - x*y + 21*y^2 + """ + return f'Class of {self._form}' + + def __hash__(self): + r""" + Return a hash value for this form class. + + EXAMPLES:: + + sage: cl = BinaryQF([11,21,31]).form_class() + sage: hash(cl) # random + -7760578299759721732 + """ + return hash(('BQFClassGroup_element', self._form)) + + @cached_method + def order(self): + r""" + Return the order of this form class in its class group. + + ALGORITHM: :meth:`BQFClassGroup.order` and :func:`order_from_multiple` + + EXAMPLES:: + + sage: cl = BinaryQF([11,21,31]).form_class() + sage: cl.order() + 10 + sage: (cl+cl).order() + 5 + sage: (cl+cl+cl).order() + 10 + sage: (5*cl).order() + 2 + """ + return order_from_multiple(self, self.parent().cardinality()) From 7c0a6c7e976fa5436c246842b20a9b5c5bbc5698 Mon Sep 17 00:00:00 2001 From: Lorenz Panny Date: Mon, 4 Sep 2023 19:35:26 +0200 Subject: [PATCH 2/3] remove BinaryQF.random() since its distribution was far from uniform on classes --- src/sage/quadratic_forms/binary_qf.py | 52 --------------------- src/sage/quadratic_forms/bqf_class_group.py | 25 ++++++++-- 2 files changed, 20 insertions(+), 57 deletions(-) diff --git a/src/sage/quadratic_forms/binary_qf.py b/src/sage/quadratic_forms/binary_qf.py index 14a1f0049db..ade5cf1561e 100755 --- a/src/sage/quadratic_forms/binary_qf.py +++ b/src/sage/quadratic_forms/binary_qf.py @@ -222,58 +222,6 @@ def principal(D): raise ValueError('discriminant must be congruent to 0 or 1 modulo 4') return BinaryQF([1, D4, (D4-D)//4]) - @staticmethod - def random(D): - r""" - Return a somewhat random primitive binary quadratic form of the - given discriminant. - - (In the case `D < 0`, only positive definite forms are returned.) - - .. NOTE:: - - No guarantees are being made about the distribution of forms - sampled by this function. - - EXAMPLES:: - - sage: BinaryQF.random(5) # random - 448219*x^2 - 597179*x*y + 198911*y^2 - sage: BinaryQF.random(-7) # random - 10007*x^2 + 10107*x*y + 2552*y^2 - - TESTS:: - - sage: D = choice((-4,+4)) * randrange(9999) + randrange(2) or 1 - sage: Q = BinaryQF.random(D) - sage: Q.discriminant() == D - True - sage: Q.is_primitive() - True - sage: Q.is_indefinite() or Q.is_positive_definite() - True - """ - D = ZZ(D) - D4 = D % 4 - if D4 not in (0,1): - raise ValueError('discriminant must be congruent to 0 or 1 modulo 4') - - from sage.misc.prandom import randrange - from sage.misc.misc_c import prod - from sage.matrix.special import random_matrix - B = (D.abs() or 1) * 99 # kind of arbitrary - while True: - b = randrange(D4, B, 2) - ac = (b**2 - D) // 4 - if ac: - break - a = prod(l**randrange(e+1) for l,e in ac.factor()) - a = a.prime_to_m_part(gcd([a, b, ac//a])) - c = ac // a - M = random_matrix(ZZ, 2, 2, 'unimodular') - M.rescale_row(0, (-1)**randrange(2)) - return BinaryQF([a, b, c]) * M - def __mul__(self, right): """ Gauss composition or right action by a 2x2 integer matrix. diff --git a/src/sage/quadratic_forms/bqf_class_group.py b/src/sage/quadratic_forms/bqf_class_group.py index 58cd10468bf..185a869b8b2 100644 --- a/src/sage/quadratic_forms/bqf_class_group.py +++ b/src/sage/quadratic_forms/bqf_class_group.py @@ -85,6 +85,8 @@ from sage.misc.prandom import randrange from sage.rings.integer_ring import ZZ +from sage.rings.finite_rings.integer_mod import Mod +from sage.arith.misc import random_prime from sage.groups.generic import order_from_multiple, multiple from sage.groups.additive_abelian.additive_abelian_wrapper import AdditiveAbelianGroupWrapper from sage.quadratic_forms.binary_qf import BinaryQF @@ -181,13 +183,17 @@ def random_element(self): r""" Return a somewhat random element of this form class group. - ALGORITHM: :meth:`BinaryQF.random` + ALGORITHM: + + Sample random odd primes `a` until `b^2 = D \pmod{4a}` has a + solution `b \in \ZZ` and set `c = (b^2-D)/(4a)`. Flip a coin + to choose the sign of `b`. Then return the class of `[a,b,c]`. .. NOTE:: - No guarantees are being made about the distribution of classes - sampled by this function. Heuristically, however, it should be - fairly close to uniform. + No strict guarantees are being made about the distribution of + classes sampled by this function. Heuristically, however, it + should be fairly close to uniform. EXAMPLES:: @@ -198,7 +204,16 @@ def random_element(self): sage: cl.form().discriminant() -999 """ - return self(BinaryQF.random(self._disc)) + B = self._disc.abs() * 100 + 9999 + while True: + a = random_prime(B, proof=False, lbound=3) + if self._disc.kronecker(a) == 1: + break + b = ZZ(Mod(self._disc, 4*a).sqrt()) + c = (b**2 - self._disc) // (4*a) + if randrange(2): + b = -b + return self(BinaryQF([a,b,c])) def __hash__(self): r""" From 4797a51839b2d9215ffb3a5d61d47fed4b6a9f0c Mon Sep 17 00:00:00 2001 From: Lorenz Panny Date: Fri, 3 Nov 2023 21:26:25 +0100 Subject: [PATCH 3/3] switch from qfbclassno() to quadclassunit() for large discriminants --- src/sage/quadratic_forms/bqf_class_group.py | 9 ++++++-- src/sage/rings/number_field/order.py | 25 ++++++++++++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/sage/quadratic_forms/bqf_class_group.py b/src/sage/quadratic_forms/bqf_class_group.py index 185a869b8b2..30c9bd7782a 100644 --- a/src/sage/quadratic_forms/bqf_class_group.py +++ b/src/sage/quadratic_forms/bqf_class_group.py @@ -253,7 +253,7 @@ def order(self): r""" Return the order of this form class group (the *class number*). - ALGORITHM: :pari:`qfbclassno` + ALGORITHM: :func:`sage.rings.number_field.order.quadratic_order_class_number`. EXAMPLES:: @@ -272,7 +272,12 @@ def order(self): sage: BQFClassGroup(-99999).order() 224 """ - return ZZ(pari.qfbclassno(self._disc)) + # Beware: If this code is ever generalized to positive + # discriminants, care must be taken to use the correct + # notion of class number. We may need the *narrow* class + # number here; see PARI's documentation for qfbclassno(). + from sage.rings.number_field.order import quadratic_order_class_number + return quadratic_order_class_number(self._disc) cardinality = order diff --git a/src/sage/rings/number_field/order.py b/src/sage/rings/number_field/order.py index f0b24b74630..857f33ffc4d 100644 --- a/src/sage/rings/number_field/order.py +++ b/src/sage/rings/number_field/order.py @@ -92,6 +92,29 @@ from sage.libs.pari.all import pari +def quadratic_order_class_number(disc): + r""" + Return the class number of the quadratic order of given discriminant. + + EXAMPLES:: + + sage: from sage.rings.number_field.order import quadratic_order_class_number + sage: quadratic_order_class_number(-419) + 9 + sage: quadratic_order_class_number(60) + 2 + + ALGORITHM: Either :pari:`qfbclassno` or :pari:`quadclassunit`, + depending on the size of the discriminant. + """ + # cutoffs from PARI documentation + if disc < -10**25 or disc > 10**10: + h = pari.quadclassunit(disc)[0] + else: + h = pari.qfbclassno(disc) + return ZZ(h) + + class OrderFactory(UniqueFactory): r""" Abstract base class for factories creating orders, such as @@ -1096,7 +1119,7 @@ def class_number(self, proof=None): K = self.number_field() if K.degree() != 2: raise NotImplementedError("computation of class numbers of non-maximal orders not in quadratic fields is not implemented") - return ZZ(pari.qfbclassno(self.discriminant())) + return quadratic_order_class_number(self.discriminant()) return self.number_field().class_number(proof=proof) def class_group(self, proof=None, names='c'):