diff --git a/src/sage/algebras/quatalg/quaternion_algebra.py b/src/sage/algebras/quatalg/quaternion_algebra.py index 2409db3840b..fd011fc358b 100644 --- a/src/sage/algebras/quatalg/quaternion_algebra.py +++ b/src/sage/algebras/quatalg/quaternion_algebra.py @@ -54,6 +54,7 @@ from sage.rings.rational_field import RationalField, QQ from sage.rings.infinity import infinity from sage.rings.number_field.number_field_base import NumberField +from sage.rings.qqbar import AA from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing from sage.rings.power_series_ring import PowerSeriesRing from sage.structure.category_object import normalize_names @@ -411,49 +412,79 @@ def is_commutative(self) -> bool: def is_division_algebra(self) -> bool: """ - Return ``True`` if the quaternion algebra is a division algebra (i.e. - every nonzero element in ``self`` is invertible), and ``False`` if the - quaternion algebra is isomorphic to the 2x2 matrix algebra. + Check whether this quaternion algebra is a division algebra, + i.e. whether every nonzero element in it is invertible. + + Currently only implemented for quaternion algebras + defined over a number field. EXAMPLES:: sage: QuaternionAlgebra(QQ,-5,-2).is_division_algebra() True - sage: QuaternionAlgebra(1).is_division_algebra() - False sage: QuaternionAlgebra(2,9).is_division_algebra() False + + By checking ramification, the method correctly recognizes division + quaternion algebras over a number field even if they have trivial + discriminant:: + + sage: K = QuadraticField(3) + sage: A = QuaternionAlgebra(K, -1, -1) + sage: A.discriminant() + Fractional ideal (1) + sage: A.is_division_algebra() + True + + The method is not implemented over arbitrary base rings yet:: + sage: QuaternionAlgebra(RR(2.),1).is_division_algebra() Traceback (most recent call last): ... - NotImplementedError: base field must be rational numbers + NotImplementedError: base ring must be rational numbers or a number field """ - if not isinstance(self.base_ring(), RationalField): - raise NotImplementedError("base field must be rational numbers") - return self.discriminant() != 1 + try: + return self.ramified_places(inf=True) != ([], []) + except ValueError: + raise NotImplementedError("base ring must be rational numbers or a number field") def is_matrix_ring(self) -> bool: """ - Return ``True`` if the quaternion algebra is isomorphic to the 2x2 - matrix ring, and ``False`` if ``self`` is a division algebra (i.e. - every nonzero element in ``self`` is invertible). + Check whether this quaternion algebra is isomorphic to the + 2x2 matrix ring over the base ring. + + Currently only implemented for quaternion algebras + defined over a number field. EXAMPLES:: sage: QuaternionAlgebra(QQ,-5,-2).is_matrix_ring() False - sage: QuaternionAlgebra(1).is_matrix_ring() - True sage: QuaternionAlgebra(2,9).is_matrix_ring() True + + By checking ramification, the method is able to recognize that + quaternion algebras (defined over a number field) with trivial + discriminant need not be matrix rings:: + + sage: K = QuadraticField(3) + sage: A = QuaternionAlgebra(K, -1, -1) + sage: A.discriminant() + Fractional ideal (1) + sage: A.is_matrix_ring() + False + + The method is not implemented over arbitrary base rings yet:: + sage: QuaternionAlgebra(RR(2.),1).is_matrix_ring() Traceback (most recent call last): ... - NotImplementedError: base field must be rational numbers + NotImplementedError: base ring must be rational numbers or a number field """ - if not isinstance(self.base_ring(), RationalField): - raise NotImplementedError("base field must be rational numbers") - return self.discriminant() == 1 + try: + return self.ramified_places(inf=True) == ([], []) + except ValueError: + raise NotImplementedError("base ring must be rational numbers or a number field") def is_exact(self) -> bool: """ @@ -1044,29 +1075,6 @@ def invariants(self): """ return self._a, self._b - def is_definite(self): - """ - Check whether the quaternion algebra ``self`` is definite, i.e. whether it ramifies at the - unique Archimedean place of its base field QQ. This is the case if and only if both - invariants of ``self`` are negative. - - EXAMPLES:: - - sage: QuaternionAlgebra(QQ,-5,-2).is_definite() - True - sage: QuaternionAlgebra(1).is_definite() - False - - sage: QuaternionAlgebra(RR(2.),1).is_definite() - Traceback (most recent call last): - ... - ValueError: base field must be rational numbers - """ - if not isinstance(self.base_ring(), RationalField): - raise ValueError("base field must be rational numbers") - a, b = self.invariants() - return a < 0 and b < 0 - def __eq__(self, other): """ Compare ``self`` and ``other``. @@ -1192,89 +1200,367 @@ def inner_product_matrix(self): a, b = self._a, self._b return diagonal_matrix(self.base_ring(), [2, -2*a, -2*b, 2*a*b]) - @cached_method - def discriminant(self): + def is_definite(self): + r""" + Check whether this quaternion algebra is definite. + + A quaternion algebra over `\QQ` is definite if it ramifies at the + unique real place of `\QQ`, which happens if and only if both of + its invariants are negative (see Exercise 2.4(c) in [Voi2021]_). + + EXAMPLES:: + + sage: QuaternionAlgebra(QQ,-5,-2).is_definite() + True + sage: QuaternionAlgebra(1).is_definite() + False + + The method does not make sense over an arbitrary base ring:: + + sage: QuaternionAlgebra(RR(2.), 1).is_definite() + Traceback (most recent call last): + ... + ValueError: base field must be rational numbers + """ + if not isinstance(self.base_ring(), RationalField): + raise ValueError("base field must be rational numbers") + a, b = self.invariants() + return a < 0 and b < 0 + + def is_totally_definite(self): """ - Return the discriminant of this quaternion algebra, i.e. the product of the finite - primes it ramifies at. + Check whether this quaternion algebra is totally definite. + + A quaternion algebra defined over a number field is + totally definite if it ramifies at all Archimedean + places of its base field. In particular, the base number + field has to be totally real (see 14.5.8 in [Voi2021]_). EXAMPLES:: - sage: QuaternionAlgebra(210,-22).discriminant() - 210 - sage: QuaternionAlgebra(19).discriminant() - 19 + sage: QuaternionAlgebra(QQ, -5, -2).is_totally_definite() + True + + sage: K = QuadraticField(3) + sage: QuaternionAlgebra(K, -1, -1).is_totally_definite() + True + + We can also use number field elements as invariants:: sage: x = polygen(ZZ, 'x') sage: F. = NumberField(x^2 - x - 1) - sage: B. = QuaternionAlgebra(F, 2*a, F(-1)) - sage: B.discriminant() - Fractional ideal (2) + sage: QuaternionAlgebra(F, 2*a, F(-1)).is_totally_definite() + False - sage: QuaternionAlgebra(QQ[sqrt(2)], 3, 19).discriminant() # needs sage.symbolic - Fractional ideal (1) + The method does not make sense over an arbitrary base ring:: + + sage: QuaternionAlgebra(RR(2.), 1).is_totally_definite() + Traceback (most recent call last): + ... + ValueError: base field must be rational numbers or a number field """ - if not isinstance(self.base_ring(), RationalField): - try: - F = self.base_ring() - return F.hilbert_conductor(self._a, self._b) - except NotImplementedError: - raise ValueError("base field must be rational numbers or number field") - else: - return ZZ.prod(self.ramified_primes()) + F = self.base_ring() + if isinstance(F, RationalField): + return self.is_definite() + + if F not in NumberFields(): + raise ValueError("base field must be rational numbers or a number field") + + # Since we need the list of real embeddings of the number field (instead + # of just the number of them), we avoid a call of the `is_totally_real()`- + # method by directly comparing the embedding list's length to the degree + E = F.embeddings(AA) + return len(E) == F.degree() and all(F.hilbert_symbol(self._a, self._b, e) == -1 + for e in E) @cached_method - def ramified_primes(self): + def ramified_places(self, inf=True): + r""" + Return the places of the base number field at which this + quaternion algebra ramifies. + + INPUT: + + - ``inf`` -- bool (default: ``True``) + + OUTPUT: + + The non-Archimedean (AKA finite) places at which this quaternion + algebra ramifies, given as + + - elements of `\ZZ` (sorted small to large) if the base field is `\QQ`, + + - integral fractional ideals of the base number field, otherwise. + + Additionally, if ``inf`` is set to ``True``, then the Archimedean + (AKA infinite) places at which the quaternion algebra ramifies are + also returned, given as + + - the embeddings of `\QQ` into `\RR` if the base field is `\QQ`, or + + - the embeddings of the base number field into the Algebraic Real Field. + + .. NOTE:: + + Any Archimedean place at which a quaternion algebra ramifies + has to be real (see 14.5.8 in [Voi2021]_). + + EXAMPLES:: + + sage: QuaternionAlgebra(210,-22).ramified_places() + ([2, 3, 5, 7], []) + + For a definite quaternion algebra we get ramification at the + unique infinite place of `\QQ`:: + + sage: QuaternionAlgebra(-1, -1).ramified_places() + ([2], + [Ring morphism: + From: Rational Field + To: Real Field with 53 bits of precision + Defn: 1 |--> 1.00000000000000]) + + Extending the base field can resolve all ramification:: + + sage: F = QuadraticField(-1) + sage: QuaternionAlgebra(F, -1, -1).ramified_places() + ([], []) + + Extending the base field can also resolve all ramification at finite + places while still leaving some ramification at infinite places:: + + sage: K = QuadraticField(3) + sage: QuaternionAlgebra(K, -1, -1).ramified_places() + ([], + [Ring morphism: + From: Number Field in a with defining polynomial x^2 - 3 with a = 1.732050807568878? + To: Algebraic Real Field + Defn: a |--> -1.732050807568878?, + Ring morphism: + From: Number Field in a with defining polynomial x^2 - 3 with a = 1.732050807568878? + To: Algebraic Real Field + Defn: a |--> 1.732050807568878?]) + + Extending the base field can also get rid of ramification at infinite + places while still leaving some ramification at finite places:: + + sage: L = QuadraticField(-15) + sage: QuaternionAlgebra(L, -1, -1).ramified_places() + ([Fractional ideal (2, 1/2*a + 1/2), Fractional ideal (2, 1/2*a - 1/2)], []) + + We can use number field elements as invariants as well:: + + sage: x = polygen(ZZ, 'x') + sage: F. = NumberField(x^2 - x - 1) + sage: QuaternionAlgebra(F, 2*a, F(-1)).ramified_places() + ([Fractional ideal (2)], + [Ring morphism: + From: Number Field in a with defining polynomial x^2 - x - 1 + To: Algebraic Real Field + Defn: a |--> -0.618033988749895?]) + + The method does not make sense over an arbitrary base ring:: + + sage: QuaternionAlgebra(RR(2.),1).ramified_places() + Traceback (most recent call last): + ... + ValueError: base field must be rational numbers or a number field """ - Return the (finite) primes that ramify in this rational quaternion algebra. + if not isinstance(inf, bool): + raise TypeError("inf must be a truth value") + + F = self.base_ring() + a = self._a + b = self._b + + # The initial choice of primes (for the base field QQ) respectively + # of prime ideals (in the number field case) to check ramification + # for is based on 12.4.12(a) in [Voi2021]_. + + # For efficiency (and to not convert QQ into a number field manually), + # we handle the case F = QQ first + if isinstance(F, RationalField): + ram_fin = sorted([p for p in set([2]).union( + prime_divisors(a.numerator()), prime_divisors(a.denominator()), + prime_divisors(b.numerator()), prime_divisors(b.denominator())) + if hilbert_symbol(a, b, p) == -1]) + + if not inf: + return ram_fin + + # The given quaternion algebra ramifies at the unique infinite place + # of QQ, by definition, if and only if it is definite + if self.is_definite(): + return ram_fin, QQ.places() + + return ram_fin, [] + + # At this point F needs to be a number field + # Note: Support for global function fields will be added in a future update + if F not in NumberFields(): + raise ValueError("base field must be rational numbers or a number field") + + # Over the number field F, first compute the finite ramified places + ram_fin = [p for p in set(F.primes_above(2)).union(F.primes_above(a), + F.primes_above(b)) if F.hilbert_symbol(a, b, p) == -1] + + if not inf: + return ram_fin + + # At this point the infinite ramified places also need to be computed + return ram_fin, [e for e in F.embeddings(AA) if F.hilbert_symbol(a, b, e) == -1] + + @cached_method + def ramified_primes(self): + r""" + Return the (finite) primes of the base number field at + which this quaternion algebra ramifies. OUTPUT: - The list of prime numbers at which ``self`` ramifies (given as integers), sorted by their - magnitude (small to large). + The list of finite primes at which this quaternion algebra ramifies, + given as + + - elements of `\ZZ` (sorted small to large) if the base field is `\QQ`, + + - integral fractional ideals of the base number field, otherwise. EXAMPLES:: - sage: QuaternionAlgebra(QQ, -1, -1).ramified_primes() + sage: QuaternionAlgebra(-58, -69).ramified_primes() + [3, 23, 29] + + Under field extensions, the number of ramified primes can increase + or decrease:: + + sage: K = QuadraticField(3) + sage: L = QuadraticField(-15) + sage: QuaternionAlgebra(-1, -1).ramified_primes() [2] + sage: QuaternionAlgebra(K, -1, -1).ramified_primes() + [] + sage: QuaternionAlgebra(L, -1, -1).ramified_primes() + [Fractional ideal (2, 1/2*a + 1/2), Fractional ideal (2, 1/2*a - 1/2)] - sage: QuaternionAlgebra(QQ, -58, -69).ramified_primes() - [3, 23, 29] + We can also use number field elements as invariants:: + + sage: x = polygen(ZZ, 'x') + sage: F. = NumberField(x^2 - x - 1) + sage: QuaternionAlgebra(F, 2*a, F(-1)).ramified_primes() + [Fractional ideal (2)] + + The method does not make sense over an arbitrary base ring:: + + sage: QuaternionAlgebra(RR(2.),1).ramified_primes() + Traceback (most recent call last): + ... + ValueError: base field must be rational numbers or a number field """ - if not isinstance(self.base_ring(), RationalField): - raise ValueError("base field must be the rational numbers") + return self.ramified_places(inf=False) - a, b = self._a, self._b - return sorted(p for p in {2}.union(prime_divisors(a.numerator()), - prime_divisors(a.denominator()), - prime_divisors(b.numerator()), - prime_divisors(b.denominator())) - if hilbert_symbol(self._a, self._b, p) == -1) + @cached_method + def discriminant(self): + r""" + Return the discriminant of this quaternion algebra. - def is_isomorphic(self, A) -> bool: + The discriminant of a quaternion algebra over a number field is the + product of the finite places at which the algebra ramifies. + + OUTPUT: + + The discriminant of this quaternion algebra, given as + + - an element of `\ZZ` if the algebra is defined over `\QQ`, + + - an integral fractional ideal of the base number field, otherwise. + + EXAMPLES:: + + sage: QuaternionAlgebra(210, -22).discriminant() + 210 + sage: QuaternionAlgebra(19).discriminant() + 19 + sage: QuaternionAlgebra(-1, -1).discriminant() + 2 + + Some examples over number fields:: + + sage: K = QuadraticField(3) + sage: L = QuadraticField(-15) + sage: QuaternionAlgebra(K, -1, -1).discriminant() + Fractional ideal (1) + sage: QuaternionAlgebra(L, -1, -1).discriminant() + Fractional ideal (2) + + We can also use number field elements as invariants:: + + sage: x = polygen(ZZ, 'x') + sage: F. = NumberField(x^2 - x - 1) + sage: QuaternionAlgebra(F, 2*a, F(-1)).discriminant() + Fractional ideal (2) + + The method does not make sense over an arbitrary base ring:: + + sage: QuaternionAlgebra(RR(2.),1).discriminant() + Traceback (most recent call last): + ... + ValueError: base field must be rational numbers or a number field """ - Return ``True`` if (and only if) ``self`` and ``A`` are isomorphic quaternion algebras over Q. + F = self.base_ring() + if isinstance(F, RationalField): + return ZZ.prod(self.ramified_places(inf=False)) + + return F.ideal(F.prod(self.ramified_places(inf=False))) + + def is_isomorphic(self, A) -> bool: + r""" + Check whether this quaternion algebra is isomorphic to ``A``. + + Currently only implemented for quaternion algebras defined over + a number field; based on Main Theorem 14.6.1 in [Voi2021]_, + noting that `\QQ` has a unique infinite place. INPUT: - - ``A`` -- a quaternion algebra defined over the rationals Q + - ``A`` -- a quaternion algebra defined over a number field EXAMPLES:: sage: B = QuaternionAlgebra(-46, -87) sage: A = QuaternionAlgebra(-58, -69) + sage: A == B + False sage: B.is_isomorphic(A) True - sage: A == B + + Checking ramification at both finite and infinite places, the method + correctly distinguishes isomorphism classes of quaternion algebras + that the discriminant can not distinguish:: + + sage: K = QuadraticField(3) + sage: A = QuaternionAlgebra(K, -1, -1) + sage: B = QuaternionAlgebra(K, 1, -1) + sage: A.discriminant() == B.discriminant() + True + sage: B.is_isomorphic(A) False """ if not isinstance(A, QuaternionAlgebra_ab): raise TypeError("A must be a quaternion algebra of the form (a,b)_K") - if self.base_ring() != QQ or A.base_ring() != QQ: - raise NotImplementedError("isomorphism check only implemented for rational quaternion algebras") + F = self.base_ring() + if F is not A.base_ring(): + raise ValueError("both quaternion algebras must be defined over the same ring") - return self.ramified_primes() == A.ramified_primes() + if isinstance(F, RationalField): + return self.ramified_places(inf=False) == A.ramified_places(inf=False) + + try: + ram_self = self.ramified_places(inf=True) + ram_A = A.ramified_places(inf=True) + return set(ram_self[0]) == set(ram_A[0]) and ram_self[1] == ram_A[1] + except ValueError: + raise NotImplementedError("base field must be rational numbers or a number field") def _magma_init_(self, magma): """