+
+class RationalBasisEJA(FiniteDimensionalEJA):
+ r"""
+ Algebras whose supplied basis elements have all rational entries.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import BilinearFormEJA
+
+ EXAMPLES:
+
+ The supplied basis is orthonormalized by default::
+
+ sage: B = matrix(QQ, [[1, 0, 0], [0, 25, -32], [0, -32, 41]])
+ sage: J = BilinearFormEJA(B)
+ sage: J.matrix_basis()
+ (
+ [1] [ 0] [ 0]
+ [0] [1/5] [32/5]
+ [0], [ 0], [ 5]
+ )
+
+ """
+ def __init__(self,
+ basis,
+ jordan_product,
+ inner_product,
+ field=AA,
+ check_field=True,
+ **kwargs):
+
+ if check_field:
+ # Abuse the check_field parameter to check that the entries of
+ # out basis (in ambient coordinates) are in the field QQ.
+ # Use _all2list to get the vector coordinates of octonion
+ # entries and not the octonions themselves (which are not
+ # rational).
+ if not all( all(b_i in QQ for b_i in _all2list(b))
+ for b in basis ):
+ raise TypeError("basis not rational")
+
+ super().__init__(basis,
+ jordan_product,
+ inner_product,
+ field=field,
+ check_field=check_field,
+ **kwargs)
+
+ self._rational_algebra = None
+ if field is not QQ:
+ # There's no point in constructing the extra algebra if this
+ # one is already rational.
+ #
+ # Note: the same Jordan and inner-products work here,
+ # because they are necessarily defined with respect to
+ # ambient coordinates and not any particular basis.
+ self._rational_algebra = FiniteDimensionalEJA(
+ basis,
+ jordan_product,
+ inner_product,
+ field=QQ,
+ matrix_space=self.matrix_space(),
+ associative=self.is_associative(),
+ orthonormalize=False,
+ check_field=False,
+ check_axioms=False)
+
+ def rational_algebra(self):
+ # Using None as a flag here (rather than just assigning "self"
+ # to self._rational_algebra by default) feels a little bit
+ # more sane to me in a garbage-collected environment.
+ if self._rational_algebra is None:
+ return self
+ else:
+ return self._rational_algebra
+
+ @cached_method
+ def _charpoly_coefficients(self):
+ r"""
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (BilinearFormEJA,
+ ....: JordanSpinEJA)
+
+ EXAMPLES:
+
+ The base ring of the resulting polynomial coefficients is what
+ it should be, and not the rationals (unless the algebra was
+ already over the rationals)::
+
+ sage: J = JordanSpinEJA(3)
+ sage: J._charpoly_coefficients()
+ (X0^2 - X1^2 - X2^2, -2*X0)
+ sage: a0 = J._charpoly_coefficients()[0]
+ sage: J.base_ring()
+ Algebraic Real Field
+ sage: a0.base_ring()
+ Algebraic Real Field
+
+ """
+ if self.rational_algebra() is self:
+ # Bypass the hijinks if they won't benefit us.
+ return super()._charpoly_coefficients()
+
+ # Do the computation over the rationals.
+ a = ( a_i.change_ring(self.base_ring())
+ for a_i in self.rational_algebra()._charpoly_coefficients() )
+
+ # Convert our coordinate variables into deorthonormalized ones
+ # and substitute them into the deorthonormalized charpoly
+ # coefficients.
+ R = self.coordinate_polynomial_ring()
+ from sage.modules.free_module_element import vector
+ X = vector(R, R.gens())
+ BX = self._deortho_matrix*X
+
+ subs_dict = { X[i]: BX[i] for i in range(len(X)) }
+ return tuple( a_i.subs(subs_dict) for a_i in a )
+
+class ConcreteEJA(FiniteDimensionalEJA):
+ r"""
+ A class for the Euclidean Jordan algebras that we know by name.
+
+ These are the Jordan algebras whose basis, multiplication table,
+ rank, and so on are known a priori. More to the point, they are
+ the Euclidean Jordan algebras for which we are able to conjure up
+ a "random instance."
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import ConcreteEJA
+
+ TESTS:
+
+ Our basis is normalized with respect to the algebra's inner
+ product, unless we specify otherwise::
+
+ sage: J = ConcreteEJA.random_instance()
+ sage: all( b.norm() == 1 for b in J.gens() )
+ True
+
+ Since our basis is orthonormal with respect to the algebra's inner
+ product, and since we know that this algebra is an EJA, any
+ left-multiplication operator's matrix will be symmetric because
+ natural->EJA basis representation is an isometry and within the
+ EJA the operator is self-adjoint by the Jordan axiom::
+
+ sage: J = ConcreteEJA.random_instance()
+ sage: x = J.random_element()
+ sage: x.operator().is_self_adjoint()
+ True
+ """
+
+ @staticmethod
+ def _max_random_instance_dimension():
+ r"""
+ The maximum dimension of any random instance. Ten dimensions seems
+ to be about the point where everything takes a turn for the
+ worse. And dimension ten (but not nine) allows the 4-by-4 real
+ Hermitian matrices, the 2-by-2 quaternion Hermitian matrices,
+ and the 2-by-2 octonion Hermitian matrices.
+ """
+ return 10
+
+ @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 HermitianMatrixEJA(FiniteDimensionalEJA):
+ @staticmethod
+ def _denormalized_basis(A):
+ """
+ Returns a basis for the given Hermitian matrix space.
+
+ 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 HermitianMatrixEJA
+
+ TESTS::
+
+ sage: n = ZZ.random_element(1,5)
+ sage: A = MatrixSpace(QQ, n)
+ sage: B = HermitianMatrixEJA._denormalized_basis(A)
+ sage: all( M.is_hermitian() for M in B)
+ True
+
+ ::
+
+ sage: n = ZZ.random_element(1,5)
+ sage: A = ComplexMatrixAlgebra(n, scalars=QQ)
+ sage: B = HermitianMatrixEJA._denormalized_basis(A)
+ sage: all( M.is_hermitian() for M in B)
+ True
+
+ ::
+
+ sage: n = ZZ.random_element(1,5)
+ sage: A = QuaternionMatrixAlgebra(n, scalars=QQ)
+ sage: B = HermitianMatrixEJA._denormalized_basis(A)
+ sage: all( M.is_hermitian() for M in B )
+ True
+
+ ::
+
+ sage: n = ZZ.random_element(1,5)
+ sage: A = OctonionMatrixAlgebra(n, scalars=QQ)
+ sage: B = HermitianMatrixEJA._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(HermitianMatrixEJA, 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: 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: 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):
+ 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:
+ self.rational_algebra()._charpoly_coefficients.set_cache(a)
+
+
+
+class ComplexHermitianEJA(HermitianMatrixEJA, 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
+
+ TESTS:
+
+ The dimension of this algebra is `n^2`::
+
+ 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: 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):
+ 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:
+ 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(HermitianMatrixEJA, 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: 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: 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):
+ 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:
+ 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))
+
+ @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 OctonionHermitianEJA(HermitianMatrixEJA, RationalBasisEJA, ConcreteEJA):
+ r"""
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (FiniteDimensionalEJA,
+ ....: OctonionHermitianEJA)
+ sage: from mjo.hurwitz import Octonions, OctonionMatrixAlgebra
+
+ EXAMPLES:
+
+ The 3-by-3 algebra satisfies the axioms of an EJA::
+
+ sage: OctonionHermitianEJA(3, # long time
+ ....: field=QQ, # long time
+ ....: orthonormalize=False, # long time
+ ....: check_axioms=True) # long time
+ Euclidean Jordan algebra of dimension 27 over Rational Field
+
+ After a change-of-basis, the 2-by-2 algebra has the same
+ multiplication table as the ten-dimensional Jordan spin algebra::
+
+ sage: A = OctonionMatrixAlgebra(2,Octonions(QQ),QQ)
+ sage: b = OctonionHermitianEJA._denormalized_basis(A)
+ sage: basis = (b[0] + b[9],) + b[1:9] + (b[0] - b[9],)
+ sage: jp = OctonionHermitianEJA.jordan_product
+ sage: ip = OctonionHermitianEJA.trace_inner_product
+ sage: J = FiniteDimensionalEJA(basis,
+ ....: jp,
+ ....: ip,
+ ....: field=QQ,
+ ....: orthonormalize=False)
+ sage: J.multiplication_table()
+ +----++----+----+----+----+----+----+----+----+----+----+
+ | * || b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7 | b8 | b9 |
+ +====++====+====+====+====+====+====+====+====+====+====+
+ | b0 || b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7 | b8 | b9 |
+ +----++----+----+----+----+----+----+----+----+----+----+
+ | b1 || b1 | b0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
+ +----++----+----+----+----+----+----+----+----+----+----+
+ | b2 || b2 | 0 | b0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
+ +----++----+----+----+----+----+----+----+----+----+----+
+ | b3 || b3 | 0 | 0 | b0 | 0 | 0 | 0 | 0 | 0 | 0 |
+ +----++----+----+----+----+----+----+----+----+----+----+
+ | b4 || b4 | 0 | 0 | 0 | b0 | 0 | 0 | 0 | 0 | 0 |
+ +----++----+----+----+----+----+----+----+----+----+----+
+ | b5 || b5 | 0 | 0 | 0 | 0 | b0 | 0 | 0 | 0 | 0 |
+ +----++----+----+----+----+----+----+----+----+----+----+
+ | b6 || b6 | 0 | 0 | 0 | 0 | 0 | b0 | 0 | 0 | 0 |
+ +----++----+----+----+----+----+----+----+----+----+----+
+ | b7 || b7 | 0 | 0 | 0 | 0 | 0 | 0 | b0 | 0 | 0 |
+ +----++----+----+----+----+----+----+----+----+----+----+
+ | b8 || b8 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | b0 | 0 |
+ +----++----+----+----+----+----+----+----+----+----+----+
+ | b9 || b9 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | b0 |
+ +----++----+----+----+----+----+----+----+----+----+----+
+
+ TESTS:
+
+ We can actually construct the 27-dimensional Albert algebra,
+ and we get the right unit element if we recompute it::
+
+ sage: J = OctonionHermitianEJA(3, # long time
+ ....: field=QQ, # long time
+ ....: orthonormalize=False) # long time
+ sage: J.one.clear_cache() # long time
+ sage: J.one() # long time
+ b0 + b9 + b26
+ sage: J.one().to_matrix() # long time
+ +----+----+----+
+ | e0 | 0 | 0 |
+ +----+----+----+
+ | 0 | e0 | 0 |
+ +----+----+----+
+ | 0 | 0 | e0 |
+ +----+----+----+
+
+ The 2-by-2 algebra is isomorphic to the ten-dimensional Jordan
+ spin algebra, but just to be sure, we recompute its rank::
+
+ sage: J = OctonionHermitianEJA(2, # long time
+ ....: field=QQ, # long time
+ ....: orthonormalize=False) # long time
+ sage: J.rank.clear_cache() # long time
+ sage: J.rank() # long time
+ 2
+
+ """
+ @staticmethod
+ def _max_random_instance_size(max_dimension):
+ r"""
+ The maximum rank of a random OctonionHermitianEJA.
+ """
+ # There's certainly a formula for this, but with only four
+ # cases to worry about, I'm not that motivated to derive it.
+ if max_dimension >= 27:
+ return 3
+ elif max_dimension >= 10:
+ return 2
+ elif max_dimension >= 1:
+ return 1
+ else:
+ return 0
+
+ @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):
+ if n > 3:
+ # Otherwise we don't get an EJA.
+ raise ValueError("n cannot exceed 3")
+
+ # 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 OctonionMatrixAlgebra
+ A = OctonionMatrixAlgebra(n, scalars=field)
+ super().__init__(A, **kwargs)
+
+ from mjo.eja.eja_cache import octonion_hermitian_eja_coeffs
+ a = octonion_hermitian_eja_coeffs(self)
+ if a is not None:
+ self.rational_algebra()._charpoly_coefficients.set_cache(a)
+
+
+class AlbertEJA(OctonionHermitianEJA):
+ r"""
+ The Albert algebra is the algebra of three-by-three Hermitian
+ matrices whose entries are octonions.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import AlbertEJA
+
+ EXAMPLES::
+
+ sage: AlbertEJA(field=QQ, orthonormalize=False)
+ Euclidean Jordan algebra of dimension 27 over Rational Field
+ sage: AlbertEJA() # long time
+ Euclidean Jordan algebra of dimension 27 over Algebraic Real Field
+
+ """
+ def __init__(self, *args, **kwargs):
+ super().__init__(3, *args, **kwargs)