+ @staticmethod
+ def _max_random_instance_size(max_dimension):
+ """
+ Return an integer "size" that is an upper bound on the size of
+ this algebra when it is used in a random test case. This size
+ (which can be passed to the algebra's constructor) is itself
+ based on the ``max_dimension`` parameter.
+
+ This method must be implemented in each subclass.
+ """
+ raise NotImplementedError
+
+ @classmethod
+ def random_instance(cls, max_dimension=None, *args, **kwargs):
+ """
+ Return a random instance of this type of algebra whose dimension
+ is less than or equal to the lesser of ``max_dimension`` and
+ the value returned by ``_max_random_instance_dimension()``. If
+ the dimension bound is omitted, then only the
+ ``_max_random_instance_dimension()`` is used as a bound.
+
+ This method should be implemented in each subclass.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import ConcreteEJA
+
+ TESTS:
+
+ Both the class bound and the ``max_dimension`` argument are upper
+ bounds on the dimension of the algebra returned::
+
+ sage: from sage.misc.prandom import choice
+ sage: eja_class = choice(ConcreteEJA.__subclasses__())
+ sage: class_max_d = eja_class._max_random_instance_dimension()
+ sage: J = eja_class.random_instance(max_dimension=20,
+ ....: field=QQ,
+ ....: orthonormalize=False)
+ sage: J.dimension() <= class_max_d
+ True
+ sage: J = eja_class.random_instance(max_dimension=2,
+ ....: field=QQ,
+ ....: orthonormalize=False)
+ sage: J.dimension() <= 2
+ True
+
+ """
+ from sage.misc.prandom import choice
+ eja_class = choice(cls.__subclasses__())
+
+ # These all bubble up to the RationalBasisEJA superclass
+ # constructor, so any (kw)args valid there are also valid
+ # here.
+ return eja_class.random_instance(max_dimension, *args, **kwargs)
+
+
+class MatrixEJA(FiniteDimensionalEJA):
+ @staticmethod
+ def _denormalized_basis(A):
+ """
+ Returns a basis for the space of complex Hermitian n-by-n matrices.
+
+ Why do we embed these? Basically, because all of numerical linear
+ algebra assumes that you're working with vectors consisting of `n`
+ entries from a field and scalars from the same field. There's no way
+ to tell SageMath that (for example) the vectors contain complex
+ numbers, while the scalar field is real.
+
+ SETUP::
+
+ sage: from mjo.hurwitz import (ComplexMatrixAlgebra,
+ ....: QuaternionMatrixAlgebra,
+ ....: OctonionMatrixAlgebra)
+ sage: from mjo.eja.eja_algebra import MatrixEJA
+
+ TESTS::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,5)
+ sage: A = MatrixSpace(QQ, n)
+ sage: B = MatrixEJA._denormalized_basis(A)
+ sage: all( M.is_hermitian() for M in B)
+ True
+
+ ::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,5)
+ sage: A = ComplexMatrixAlgebra(n, scalars=QQ)
+ sage: B = MatrixEJA._denormalized_basis(A)
+ sage: all( M.is_hermitian() for M in B)
+ True
+
+ ::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,5)
+ sage: A = QuaternionMatrixAlgebra(n, scalars=QQ)
+ sage: B = MatrixEJA._denormalized_basis(A)
+ sage: all( M.is_hermitian() for M in B )
+ True
+
+ ::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,5)
+ sage: A = OctonionMatrixAlgebra(n, scalars=QQ)
+ sage: B = MatrixEJA._denormalized_basis(A)
+ sage: all( M.is_hermitian() for M in B )
+ True
+
+ """
+ # These work for real MatrixSpace, whose monomials only have
+ # two coordinates (because the last one would always be "1").
+ es = A.base_ring().gens()
+ gen = lambda A,m: A.monomial(m[:2])
+
+ if hasattr(A, 'entry_algebra_gens'):
+ # We've got a MatrixAlgebra, and its monomials will have
+ # three coordinates.
+ es = A.entry_algebra_gens()
+ gen = lambda A,m: A.monomial(m)
+
+ basis = []
+ for i in range(A.nrows()):
+ for j in range(i+1):
+ if i == j:
+ E_ii = gen(A, (i,j,es[0]))
+ basis.append(E_ii)
+ else:
+ for e in es:
+ E_ij = gen(A, (i,j,e))
+ E_ij += E_ij.conjugate_transpose()
+ basis.append(E_ij)
+
+ return tuple( basis )
+
+ @staticmethod
+ def jordan_product(X,Y):
+ return (X*Y + Y*X)/2
+
+ @staticmethod
+ def trace_inner_product(X,Y):
+ r"""
+ A trace inner-product for matrices that aren't embedded in the
+ reals. It takes MATRICES as arguments, not EJA elements.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (RealSymmetricEJA,
+ ....: ComplexHermitianEJA,
+ ....: QuaternionHermitianEJA,
+ ....: OctonionHermitianEJA)
+
+ EXAMPLES::
+
+ sage: J = RealSymmetricEJA(2,field=QQ,orthonormalize=False)
+ sage: I = J.one().to_matrix()
+ sage: J.trace_inner_product(I, -I)
+ -2
+
+ ::
+
+ sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
+ sage: I = J.one().to_matrix()
+ sage: J.trace_inner_product(I, -I)
+ -2
+
+ ::
+
+ sage: J = QuaternionHermitianEJA(2,field=QQ,orthonormalize=False)
+ sage: I = J.one().to_matrix()
+ sage: J.trace_inner_product(I, -I)
+ -2
+
+ ::
+
+ sage: J = OctonionHermitianEJA(2,field=QQ,orthonormalize=False)
+ sage: I = J.one().to_matrix()
+ sage: J.trace_inner_product(I, -I)
+ -2
+
+ """
+ tr = (X*Y).trace()
+ if hasattr(tr, 'coefficient'):
+ # Works for octonions, and has to come first because they
+ # also have a "real()" method that doesn't return an
+ # element of the scalar ring.
+ return tr.coefficient(0)
+ elif hasattr(tr, 'coefficient_tuple'):
+ # Works for quaternions.
+ return tr.coefficient_tuple()[0]
+
+ # Works for real and complex numbers.
+ return tr.real()
+
+
+ def __init__(self, matrix_space, **kwargs):
+ # We know this is a valid EJA, but will double-check
+ # if the user passes check_axioms=True.
+ if "check_axioms" not in kwargs: kwargs["check_axioms"] = False
+
+
+ super().__init__(self._denormalized_basis(matrix_space),
+ self.jordan_product,
+ self.trace_inner_product,
+ field=matrix_space.base_ring(),
+ matrix_space=matrix_space,
+ **kwargs)
+
+ self.rank.set_cache(matrix_space.nrows())
+ self.one.set_cache( self(matrix_space.one()) )
+
+class RealSymmetricEJA(MatrixEJA, RationalBasisEJA, ConcreteEJA):
+ """
+ The rank-n simple EJA consisting of real symmetric n-by-n
+ matrices, the usual symmetric Jordan product, and the trace inner
+ product. It has dimension `(n^2 + n)/2` over the reals.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import RealSymmetricEJA
+
+ EXAMPLES::
+
+ sage: J = RealSymmetricEJA(2)
+ sage: b0, b1, b2 = J.gens()
+ sage: b0*b0
+ b0
+ sage: b1*b1
+ 1/2*b0 + 1/2*b2
+ sage: b2*b2
+ b2
+
+ In theory, our "field" can be any subfield of the reals::
+
+ sage: RealSymmetricEJA(2, field=RDF, check_axioms=True)
+ Euclidean Jordan algebra of dimension 3 over Real Double Field
+ sage: RealSymmetricEJA(2, field=RR, check_axioms=True)
+ Euclidean Jordan algebra of dimension 3 over Real Field with
+ 53 bits of precision
+
+ TESTS:
+
+ The dimension of this algebra is `(n^2 + n) / 2`::
+
+ sage: set_random_seed()
+ sage: d = RealSymmetricEJA._max_random_instance_dimension()
+ sage: n = RealSymmetricEJA._max_random_instance_size(d)
+ sage: J = RealSymmetricEJA(n)
+ sage: J.dimension() == (n^2 + n)/2
+ True
+
+ The Jordan multiplication is what we think it is::
+
+ sage: set_random_seed()
+ sage: J = RealSymmetricEJA.random_instance()
+ sage: x,y = J.random_elements(2)
+ sage: actual = (x*y).to_matrix()
+ sage: X = x.to_matrix()
+ sage: Y = y.to_matrix()
+ sage: expected = (X*Y + Y*X)/2
+ sage: actual == expected
+ True
+ sage: J(expected) == x*y
+ True
+
+ We can change the generator prefix::
+
+ sage: RealSymmetricEJA(3, prefix='q').gens()
+ (q0, q1, q2, q3, q4, q5)
+
+ We can construct the (trivial) algebra of rank zero::
+
+ sage: RealSymmetricEJA(0)
+ Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
+
+ """
+ @staticmethod
+ def _max_random_instance_size(max_dimension):
+ # Obtained by solving d = (n^2 + n)/2.
+ # The ZZ-int-ZZ thing is just "floor."
+ return ZZ(int(ZZ(8*max_dimension + 1).sqrt()/2 - 1/2))
+
+ @classmethod
+ def random_instance(cls, max_dimension=None, *args, **kwargs):
+ """
+ Return a random instance of this type of algebra.
+ """
+ class_max_d = cls._max_random_instance_dimension()
+ if (max_dimension is None or max_dimension > class_max_d):
+ max_dimension = class_max_d
+ max_size = cls._max_random_instance_size(max_dimension)
+ n = ZZ.random_element(max_size + 1)
+ return cls(n, **kwargs)
+
+ def __init__(self, n, field=AA, **kwargs):
+ # We know this is a valid EJA, but will double-check
+ # if the user passes check_axioms=True.
+ if "check_axioms" not in kwargs: kwargs["check_axioms"] = False
+
+ A = MatrixSpace(field, n)
+ super().__init__(A, **kwargs)
+
+ from mjo.eja.eja_cache import real_symmetric_eja_coeffs
+ a = real_symmetric_eja_coeffs(self)
+ if a is not None:
+ if self._rational_algebra is None:
+ self._charpoly_coefficients.set_cache(a)
+ else:
+ self._rational_algebra._charpoly_coefficients.set_cache(a)
+
+
+
+class ComplexHermitianEJA(MatrixEJA, RationalBasisEJA, ConcreteEJA):
+ """
+ The rank-n simple EJA consisting of complex Hermitian n-by-n
+ matrices over the real numbers, the usual symmetric Jordan product,
+ and the real-part-of-trace inner product. It has dimension `n^2` over
+ the reals.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
+
+ EXAMPLES:
+
+ In theory, our "field" can be any subfield of the reals, but we
+ can't use inexact real fields at the moment because SageMath
+ doesn't know how to convert their elements into complex numbers,
+ or even into algebraic reals::
+
+ sage: QQbar(RDF(1))
+ Traceback (most recent call last):
+ ...
+ TypeError: Illegal initializer for algebraic number
+ sage: AA(RR(1))
+ Traceback (most recent call last):
+ ...
+ TypeError: Illegal initializer for algebraic number
+
+ This causes the following error when we try to scale a matrix of
+ complex numbers by an inexact real number::
+
+ sage: ComplexHermitianEJA(2,field=RR)
+ Traceback (most recent call last):
+ ...
+ TypeError: Unable to coerce entries (=(1.00000000000000,
+ -0.000000000000000)) to coefficients in Algebraic Real Field
+
+ TESTS:
+
+ The dimension of this algebra is `n^2`::
+
+ sage: set_random_seed()
+ sage: d = ComplexHermitianEJA._max_random_instance_dimension()
+ sage: n = ComplexHermitianEJA._max_random_instance_size(d)
+ sage: J = ComplexHermitianEJA(n)
+ sage: J.dimension() == n^2
+ True
+
+ The Jordan multiplication is what we think it is::
+
+ sage: set_random_seed()
+ sage: J = ComplexHermitianEJA.random_instance()
+ sage: x,y = J.random_elements(2)
+ sage: actual = (x*y).to_matrix()
+ sage: X = x.to_matrix()
+ sage: Y = y.to_matrix()
+ sage: expected = (X*Y + Y*X)/2
+ sage: actual == expected
+ True
+ sage: J(expected) == x*y
+ True
+
+ We can change the generator prefix::
+
+ sage: ComplexHermitianEJA(2, prefix='z').gens()
+ (z0, z1, z2, z3)
+
+ We can construct the (trivial) algebra of rank zero::
+
+ sage: ComplexHermitianEJA(0)
+ Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
+
+ """
+ def __init__(self, n, field=AA, **kwargs):
+ # We know this is a valid EJA, but will double-check
+ # if the user passes check_axioms=True.
+ if "check_axioms" not in kwargs: kwargs["check_axioms"] = False
+
+ from mjo.hurwitz import ComplexMatrixAlgebra
+ A = ComplexMatrixAlgebra(n, scalars=field)
+ super().__init__(A, **kwargs)
+
+ from mjo.eja.eja_cache import complex_hermitian_eja_coeffs
+ a = complex_hermitian_eja_coeffs(self)
+ if a is not None:
+ if self._rational_algebra is None:
+ self._charpoly_coefficients.set_cache(a)
+ else:
+ self._rational_algebra._charpoly_coefficients.set_cache(a)
+
+ @staticmethod
+ def _max_random_instance_size(max_dimension):
+ # Obtained by solving d = n^2.
+ # The ZZ-int-ZZ thing is just "floor."
+ return ZZ(int(ZZ(max_dimension).sqrt()))
+
+ @classmethod
+ def random_instance(cls, max_dimension=None, *args, **kwargs):
+ """
+ Return a random instance of this type of algebra.
+ """
+ class_max_d = cls._max_random_instance_dimension()
+ if (max_dimension is None or max_dimension > class_max_d):
+ max_dimension = class_max_d
+ max_size = cls._max_random_instance_size(max_dimension)
+ n = ZZ.random_element(max_size + 1)
+ return cls(n, **kwargs)
+
+
+class QuaternionHermitianEJA(MatrixEJA, RationalBasisEJA, ConcreteEJA):
+ r"""
+ The rank-n simple EJA consisting of self-adjoint n-by-n quaternion
+ matrices, the usual symmetric Jordan product, and the
+ real-part-of-trace inner product. It has dimension `2n^2 - n` over
+ the reals.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
+
+ EXAMPLES:
+
+ In theory, our "field" can be any subfield of the reals::
+
+ sage: QuaternionHermitianEJA(2, field=RDF, check_axioms=True)
+ Euclidean Jordan algebra of dimension 6 over Real Double Field
+ sage: QuaternionHermitianEJA(2, field=RR, check_axioms=True)
+ Euclidean Jordan algebra of dimension 6 over Real Field with
+ 53 bits of precision
+
+ TESTS:
+
+ The dimension of this algebra is `2*n^2 - n`::
+
+ sage: set_random_seed()
+ sage: d = QuaternionHermitianEJA._max_random_instance_dimension()
+ sage: n = QuaternionHermitianEJA._max_random_instance_size(d)
+ sage: J = QuaternionHermitianEJA(n)
+ sage: J.dimension() == 2*(n^2) - n
+ True
+
+ The Jordan multiplication is what we think it is::
+
+ sage: set_random_seed()
+ sage: J = QuaternionHermitianEJA.random_instance()
+ sage: x,y = J.random_elements(2)
+ sage: actual = (x*y).to_matrix()
+ sage: X = x.to_matrix()
+ sage: Y = y.to_matrix()
+ sage: expected = (X*Y + Y*X)/2
+ sage: actual == expected
+ True
+ sage: J(expected) == x*y
+ True
+
+ We can change the generator prefix::
+
+ sage: QuaternionHermitianEJA(2, prefix='a').gens()
+ (a0, a1, a2, a3, a4, a5)
+
+ We can construct the (trivial) algebra of rank zero::
+
+ sage: QuaternionHermitianEJA(0)
+ Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
+
+ """
+ def __init__(self, n, field=AA, **kwargs):
+ # We know this is a valid EJA, but will double-check
+ # if the user passes check_axioms=True.
+ if "check_axioms" not in kwargs: kwargs["check_axioms"] = False
+
+ from mjo.hurwitz import QuaternionMatrixAlgebra
+ A = QuaternionMatrixAlgebra(n, scalars=field)
+ super().__init__(A, **kwargs)
+
+ from mjo.eja.eja_cache import quaternion_hermitian_eja_coeffs
+ a = quaternion_hermitian_eja_coeffs(self)
+ if a is not None:
+ if self._rational_algebra is None:
+ self._charpoly_coefficients.set_cache(a)
+ else:
+ self._rational_algebra._charpoly_coefficients.set_cache(a)
+
+
+
+ @staticmethod
+ def _max_random_instance_size(max_dimension):
+ r"""
+ The maximum rank of a random QuaternionHermitianEJA.
+ """
+ # Obtained by solving d = 2n^2 - n.
+ # The ZZ-int-ZZ thing is just "floor."
+ return ZZ(int(ZZ(8*max_dimension + 1).sqrt()/4 + 1/4))