+ sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
+
+ TESTS::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,5)
+ sage: B = QuaternionHermitianEJA._denormalized_basis(n,ZZ)
+ sage: all( M.is_symmetric() for M in B )
+ True
+
+ """
+ Q = QuaternionAlgebra(QQ,-1,-1)
+ I,J,K = Q.gens()
+
+ # This is like the symmetric case, but we need to be careful:
+ #
+ # * We want conjugate-symmetry, not just symmetry.
+ # * The diagonal will (as a result) be real.
+ #
+ S = []
+ Eij = matrix.zero(Q,n)
+ for i in range(n):
+ for j in range(i+1):
+ # "build" E_ij
+ Eij[i,j] = 1
+ if i == j:
+ Sij = cls.real_embed(Eij)
+ S.append(Sij)
+ else:
+ # The second, third, and fourth ones have a minus
+ # because they're conjugated.
+ # Eij = Eij + Eij.transpose()
+ Eij[j,i] = 1
+ Sij_real = cls.real_embed(Eij)
+ S.append(Sij_real)
+ # Eij = I*(Eij - Eij.transpose())
+ Eij[i,j] = I
+ Eij[j,i] = -I
+ Sij_I = cls.real_embed(Eij)
+ S.append(Sij_I)
+ # Eij = J*(Eij - Eij.transpose())
+ Eij[i,j] = J
+ Eij[j,i] = -J
+ Sij_J = cls.real_embed(Eij)
+ S.append(Sij_J)
+ # Eij = K*(Eij - Eij.transpose())
+ Eij[i,j] = K
+ Eij[j,i] = -K
+ Sij_K = cls.real_embed(Eij)
+ S.append(Sij_K)
+ Eij[j,i] = 0
+ # "erase" E_ij
+ Eij[i,j] = 0
+
+ # Since we embedded the entries, we can drop back to the
+ # desired real "field" instead of the quaternion algebra "Q".
+ return tuple( s.change_ring(field) for s in S )
+
+
+ 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
+
+ associative = False
+ if n <= 1:
+ associative = True
+
+ super().__init__(self._denormalized_basis(n,field),
+ self.jordan_product,
+ self.trace_inner_product,
+ field=field,
+ associative=associative,
+ **kwargs)
+
+ # TODO: this could be factored out somehow, but is left here
+ # because the MatrixEJA is not presently a subclass of the
+ # FDEJA class that defines rank() and one().
+ self.rank.set_cache(n)
+ idV = matrix.identity(ZZ, self.dimension_over_reals()*n)
+ self.one.set_cache(self(idV))
+
+
+ @staticmethod
+ def _max_random_instance_size():
+ r"""
+ The maximum rank of a random QuaternionHermitianEJA.
+ """
+ return 2 # Dimension 6
+
+ @classmethod
+ def random_instance(cls, **kwargs):
+ """
+ Return a random instance of this type of algebra.
+ """
+ n = ZZ.random_element(cls._max_random_instance_size() + 1)
+ return cls(n, **kwargs)
+
+class OctonionHermitianEJA(ConcreteEJA, MatrixEJA):
+ r"""
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (FiniteDimensionalEJA,
+ ....: OctonionHermitianEJA)
+
+ 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: b = OctonionHermitianEJA._denormalized_basis(2,QQ)
+ 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():
+ r"""
+ The maximum rank of a random QuaternionHermitianEJA.
+ """
+ return 1 # Dimension 1
+
+ @classmethod
+ def random_instance(cls, **kwargs):
+ """
+ Return a random instance of this type of algebra.
+ """
+ n = ZZ.random_element(cls._max_random_instance_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
+
+ super().__init__(self._denormalized_basis(n,field),
+ self.jordan_product,
+ self.trace_inner_product,
+ field=field,
+ **kwargs)
+
+ # TODO: this could be factored out somehow, but is left here
+ # because the MatrixEJA is not presently a subclass of the
+ # FDEJA class that defines rank() and one().
+ self.rank.set_cache(n)
+ idV = self.matrix_space().one()
+ self.one.set_cache(self(idV))
+
+
+ @classmethod
+ def _denormalized_basis(cls, n, field):
+ """
+ Returns a basis for the space of octonion Hermitian n-by-n
+ matrices.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import OctonionHermitianEJA
+
+ EXAMPLES::
+
+ sage: B = OctonionHermitianEJA._denormalized_basis(3,QQ)
+ sage: all( M.is_hermitian() for M in B )
+ True
+ sage: len(B)
+ 27
+
+ """
+ from mjo.octonions import OctonionMatrixAlgebra
+ MS = OctonionMatrixAlgebra(n, scalars=field)
+ es = MS.entry_algebra().gens()
+
+ basis = []
+ for i in range(n):
+ for j in range(i+1):
+ if i == j:
+ E_ii = MS.monomial( (i,j,es[0]) )
+ basis.append(E_ii)
+ else:
+ for e in es:
+ E_ij = MS.monomial( (i,j,e) )
+ ec = e.conjugate()
+ # If the conjugate has a negative sign in front
+ # of it, (j,i,ec) won't be a monomial!
+ if (j,i,ec) in MS.indices():
+ E_ij += MS.monomial( (j,i,ec) )
+ else:
+ E_ij -= MS.monomial( (j,i,-ec) )
+ basis.append(E_ij)
+
+ return tuple( basis )
+
+ @staticmethod
+ def trace_inner_product(X,Y):
+ r"""
+ The octonions don't know that the reals are embedded in them,
+ so we have to take the e0 component ourselves.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import OctonionHermitianEJA