+ ::
+
+ sage: set_random_seed()
+ sage: J = QuaternionHermitianEJA.random_instance()
+ sage: x,y = J.random_elements(2)
+ sage: Xe = x.to_matrix()
+ sage: Ye = y.to_matrix()
+ sage: X = J.real_unembed(Xe)
+ sage: Y = J.real_unembed(Ye)
+ sage: expected = (X*Y).trace().coefficient_tuple()[0]
+ sage: actual = J.trace_inner_product(Xe,Ye)
+ sage: actual == expected
+ True
+
+ """
+ Xu = cls.real_unembed(X)
+ Yu = cls.real_unembed(Y)
+ tr = (Xu*Yu).trace()
+
+ try:
+ # Works in QQ, AA, RDF, et cetera.
+ return tr.real()
+ except AttributeError:
+ # A quaternion doesn't have a real() method, but does
+ # have coefficient_tuple() method that returns the
+ # coefficients of 1, i, j, and k -- in that order.
+ return tr.coefficient_tuple()[0]
+
+
+class RealMatrixEJA(MatrixEJA):
+ @staticmethod
+ def dimension_over_reals():
+ return 1
+
+
+class RealSymmetricEJA(ConcreteEJA, RealMatrixEJA):
+ """
+ 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: e0, e1, e2 = J.gens()
+ sage: e0*e0
+ e0
+ sage: e1*e1
+ 1/2*e0 + 1/2*e2
+ sage: e2*e2
+ e2
+
+ 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: n_max = RealSymmetricEJA._max_random_instance_size()
+ sage: n = ZZ.random_element(1, n_max)
+ 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
+
+ """
+ @classmethod
+ def _denormalized_basis(cls, n):
+ """
+ Return a basis for the space of real symmetric n-by-n matrices.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import RealSymmetricEJA
+
+ TESTS::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,5)
+ sage: B = RealSymmetricEJA._denormalized_basis(n)
+ sage: all( M.is_symmetric() for M in B)
+ True
+
+ """
+ # The basis of symmetric matrices, as matrices, in their R^(n-by-n)
+ # coordinates.
+ S = []
+ for i in range(n):
+ for j in range(i+1):
+ Eij = matrix(ZZ, n, lambda k,l: k==i and l==j)
+ if i == j:
+ Sij = Eij
+ else:
+ Sij = Eij + Eij.transpose()
+ S.append(Sij)
+ return tuple(S)
+
+
+ @staticmethod
+ def _max_random_instance_size():
+ return 4 # Dimension 10
+
+ @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, **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(RealSymmetricEJA, self).__init__(self._denormalized_basis(n),
+ self.jordan_product,
+ self.trace_inner_product,
+ **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))
+
+
+
+class ComplexMatrixEJA(MatrixEJA):
+ # A manual dictionary-cache for the complex_extension() method,
+ # since apparently @classmethods can't also be @cached_methods.
+ _complex_extension = {}
+
+ @classmethod
+ def complex_extension(cls,field):
+ r"""
+ The complex field that we embed/unembed, as an extension
+ of the given ``field``.
+ """
+ if field in cls._complex_extension:
+ return cls._complex_extension[field]
+
+ # Sage doesn't know how to adjoin the complex "i" (the root of
+ # x^2 + 1) to a field in a general way. Here, we just enumerate
+ # all of the cases that I have cared to support so far.
+ if field is AA:
+ # Sage doesn't know how to embed AA into QQbar, i.e. how
+ # to adjoin sqrt(-1) to AA.
+ F = QQbar
+ elif not field.is_exact():
+ # RDF or RR
+ F = field.complex_field()
+ else:
+ # Works for QQ and... maybe some other fields.
+ R = PolynomialRing(field, 'z')
+ z = R.gen()
+ F = field.extension(z**2 + 1, 'I', embedding=CLF(-1).sqrt())
+
+ cls._complex_extension[field] = F
+ return F
+
+ @staticmethod
+ def dimension_over_reals():
+ return 2
+
+ @classmethod
+ def real_embed(cls,M):
+ """
+ Embed the n-by-n complex matrix ``M`` into the space of real
+ matrices of size 2n-by-2n via the map the sends each entry `z = a +
+ bi` to the block matrix ``[[a,b],[-b,a]]``.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import ComplexMatrixEJA
+
+ EXAMPLES::
+
+ sage: F = QuadraticField(-1, 'I')
+ sage: x1 = F(4 - 2*i)
+ sage: x2 = F(1 + 2*i)
+ sage: x3 = F(-i)
+ sage: x4 = F(6)
+ sage: M = matrix(F,2,[[x1,x2],[x3,x4]])
+ sage: ComplexMatrixEJA.real_embed(M)
+ [ 4 -2| 1 2]
+ [ 2 4|-2 1]
+ [-----+-----]
+ [ 0 -1| 6 0]
+ [ 1 0| 0 6]
+
+ TESTS:
+
+ Embedding is a homomorphism (isomorphism, in fact)::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(3)
+ sage: F = QuadraticField(-1, 'I')
+ sage: X = random_matrix(F, n)
+ sage: Y = random_matrix(F, n)
+ sage: Xe = ComplexMatrixEJA.real_embed(X)
+ sage: Ye = ComplexMatrixEJA.real_embed(Y)
+ sage: XYe = ComplexMatrixEJA.real_embed(X*Y)
+ sage: Xe*Ye == XYe
+ True
+
+ """
+ super(ComplexMatrixEJA,cls).real_embed(M)
+ n = M.nrows()
+
+ # We don't need any adjoined elements...
+ field = M.base_ring().base_ring()
+
+ blocks = []
+ for z in M.list():
+ a = z.real()
+ b = z.imag()
+ blocks.append(matrix(field, 2, [ [ a, b],
+ [-b, a] ]))
+
+ return matrix.block(field, n, blocks)
+
+
+ @classmethod
+ def real_unembed(cls,M):
+ """
+ The inverse of _embed_complex_matrix().
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import ComplexMatrixEJA
+
+ EXAMPLES::
+
+ sage: A = matrix(QQ,[ [ 1, 2, 3, 4],
+ ....: [-2, 1, -4, 3],
+ ....: [ 9, 10, 11, 12],
+ ....: [-10, 9, -12, 11] ])
+ sage: ComplexMatrixEJA.real_unembed(A)
+ [ 2*I + 1 4*I + 3]
+ [ 10*I + 9 12*I + 11]
+
+ TESTS:
+
+ Unembedding is the inverse of embedding::
+
+ sage: set_random_seed()
+ sage: F = QuadraticField(-1, 'I')
+ sage: M = random_matrix(F, 3)
+ sage: Me = ComplexMatrixEJA.real_embed(M)
+ sage: ComplexMatrixEJA.real_unembed(Me) == M
+ True
+
+ """
+ super(ComplexMatrixEJA,cls).real_unembed(M)
+ n = ZZ(M.nrows())
+ d = cls.dimension_over_reals()
+ F = cls.complex_extension(M.base_ring())
+ i = F.gen()
+
+ # Go top-left to bottom-right (reading order), converting every
+ # 2-by-2 block we see to a single complex element.
+ elements = []
+ for k in range(n/d):
+ for j in range(n/d):
+ submat = M[d*k:d*k+d,d*j:d*j+d]
+ if submat[0,0] != submat[1,1]:
+ raise ValueError('bad on-diagonal submatrix')
+ if submat[0,1] != -submat[1,0]:
+ raise ValueError('bad off-diagonal submatrix')
+ z = submat[0,0] + submat[0,1]*i
+ elements.append(z)
+
+ return matrix(F, n/d, elements)
+
+
+class ComplexHermitianEJA(ConcreteEJA, ComplexMatrixEJA):
+ """
+ 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::
+
+ sage: ComplexHermitianEJA(2, field=RDF, check_axioms=True)
+ Euclidean Jordan algebra of dimension 4 over Real Double Field
+ sage: ComplexHermitianEJA(2, field=RR, check_axioms=True)
+ Euclidean Jordan algebra of dimension 4 over Real Field with
+ 53 bits of precision
+
+ TESTS:
+
+ The dimension of this algebra is `n^2`::
+
+ sage: set_random_seed()
+ sage: n_max = ComplexHermitianEJA._max_random_instance_size()
+ sage: n = ZZ.random_element(1, n_max)
+ 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
+
+ """
+
+ @classmethod
+ def _denormalized_basis(cls, n):
+ """
+ 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.eja.eja_algebra import ComplexHermitianEJA
+
+ TESTS::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,5)
+ sage: B = ComplexHermitianEJA._denormalized_basis(n)
+ sage: all( M.is_symmetric() for M in B)
+ True
+
+ """
+ field = ZZ
+ R = PolynomialRing(field, 'z')
+ z = R.gen()
+ F = field.extension(z**2 + 1, 'I')
+ I = F.gen(1)
+
+ # 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(F,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 one has a minus because it's conjugated.
+ Eij[j,i] = 1 # Eij = Eij + Eij.transpose()
+ Sij_real = cls.real_embed(Eij)
+ S.append(Sij_real)
+ # Eij = I*Eij - I*Eij.transpose()
+ Eij[i,j] = I
+ Eij[j,i] = -I
+ Sij_imag = cls.real_embed(Eij)
+ S.append(Sij_imag)
+ Eij[j,i] = 0
+ # "erase" E_ij
+ Eij[i,j] = 0
+
+ # Since we embedded these, we can drop back to the "field" that we
+ # started with instead of the complex extension "F".
+ return tuple( s.change_ring(field) for s in S )
+
+
+ def __init__(self, n, **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(ComplexHermitianEJA, self).__init__(self._denormalized_basis(n),
+ self.jordan_product,
+ self.trace_inner_product,
+ **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():
+ return 3 # Dimension 9
+
+ @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 QuaternionMatrixEJA(MatrixEJA):
+
+ # A manual dictionary-cache for the quaternion_extension() method,
+ # since apparently @classmethods can't also be @cached_methods.
+ _quaternion_extension = {}
+
+ @classmethod
+ def quaternion_extension(cls,field):
+ r"""
+ The quaternion field that we embed/unembed, as an extension
+ of the given ``field``.
+ """
+ if field in cls._quaternion_extension:
+ return cls._quaternion_extension[field]
+
+ Q = QuaternionAlgebra(field,-1,-1)
+
+ cls._quaternion_extension[field] = Q
+ return Q
+
+ @staticmethod
+ def dimension_over_reals():
+ return 4
+
+ @classmethod
+ def real_embed(cls,M):
+ """
+ Embed the n-by-n quaternion matrix ``M`` into the space of real
+ matrices of size 4n-by-4n by first sending each quaternion entry `z
+ = a + bi + cj + dk` to the block-complex matrix ``[[a + bi,
+ c+di],[-c + di, a-bi]]`, and then embedding those into a real
+ matrix.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import QuaternionMatrixEJA
+
+ EXAMPLES::
+
+ sage: Q = QuaternionAlgebra(QQ,-1,-1)
+ sage: i,j,k = Q.gens()
+ sage: x = 1 + 2*i + 3*j + 4*k
+ sage: M = matrix(Q, 1, [[x]])
+ sage: QuaternionMatrixEJA.real_embed(M)
+ [ 1 2 3 4]
+ [-2 1 -4 3]
+ [-3 4 1 -2]
+ [-4 -3 2 1]
+
+ Embedding is a homomorphism (isomorphism, in fact)::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(2)
+ sage: Q = QuaternionAlgebra(QQ,-1,-1)
+ sage: X = random_matrix(Q, n)
+ sage: Y = random_matrix(Q, n)
+ sage: Xe = QuaternionMatrixEJA.real_embed(X)
+ sage: Ye = QuaternionMatrixEJA.real_embed(Y)
+ sage: XYe = QuaternionMatrixEJA.real_embed(X*Y)
+ sage: Xe*Ye == XYe
+ True
+
+ """
+ super(QuaternionMatrixEJA,cls).real_embed(M)
+ quaternions = M.base_ring()
+ n = M.nrows()
+
+ F = QuadraticField(-1, 'I')
+ i = F.gen()
+
+ blocks = []
+ for z in M.list():
+ t = z.coefficient_tuple()
+ a = t[0]
+ b = t[1]
+ c = t[2]
+ d = t[3]
+ cplxM = matrix(F, 2, [[ a + b*i, c + d*i],
+ [-c + d*i, a - b*i]])
+ realM = ComplexMatrixEJA.real_embed(cplxM)
+ blocks.append(realM)
+
+ # We should have real entries by now, so use the realest field
+ # we've got for the return value.
+ return matrix.block(quaternions.base_ring(), n, blocks)
+
+
+
+ @classmethod
+ def real_unembed(cls,M):
+ """
+ The inverse of _embed_quaternion_matrix().
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import QuaternionMatrixEJA
+
+ EXAMPLES::
+
+ sage: M = matrix(QQ, [[ 1, 2, 3, 4],
+ ....: [-2, 1, -4, 3],
+ ....: [-3, 4, 1, -2],
+ ....: [-4, -3, 2, 1]])
+ sage: QuaternionMatrixEJA.real_unembed(M)
+ [1 + 2*i + 3*j + 4*k]
+
+ TESTS:
+
+ Unembedding is the inverse of embedding::
+
+ sage: set_random_seed()
+ sage: Q = QuaternionAlgebra(QQ, -1, -1)
+ sage: M = random_matrix(Q, 3)
+ sage: Me = QuaternionMatrixEJA.real_embed(M)
+ sage: QuaternionMatrixEJA.real_unembed(Me) == M
+ True
+
+ """
+ super(QuaternionMatrixEJA,cls).real_unembed(M)
+ n = ZZ(M.nrows())
+ d = cls.dimension_over_reals()
+
+ # Use the base ring of the matrix to ensure that its entries can be
+ # multiplied by elements of the quaternion algebra.
+ Q = cls.quaternion_extension(M.base_ring())
+ i,j,k = Q.gens()
+
+ # Go top-left to bottom-right (reading order), converting every
+ # 4-by-4 block we see to a 2-by-2 complex block, to a 1-by-1
+ # quaternion block.
+ elements = []
+ for l in range(n/d):
+ for m in range(n/d):
+ submat = ComplexMatrixEJA.real_unembed(
+ M[d*l:d*l+d,d*m:d*m+d] )
+ if submat[0,0] != submat[1,1].conjugate():
+ raise ValueError('bad on-diagonal submatrix')
+ if submat[0,1] != -submat[1,0].conjugate():
+ raise ValueError('bad off-diagonal submatrix')
+ z = submat[0,0].real()
+ z += submat[0,0].imag()*i
+ z += submat[0,1].real()*j
+ z += submat[0,1].imag()*k
+ elements.append(z)
+
+ return matrix(Q, n/d, elements)
+
+
+class QuaternionHermitianEJA(ConcreteEJA, QuaternionMatrixEJA):
+ 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: n_max = QuaternionHermitianEJA._max_random_instance_size()
+ sage: n = ZZ.random_element(1, n_max)
+ 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
+
+ """
+ @classmethod
+ def _denormalized_basis(cls, n):
+ """
+ Returns a basis for the space of quaternion 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.eja.eja_algebra import QuaternionHermitianEJA
+
+ TESTS::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,5)
+ sage: B = QuaternionHermitianEJA._denormalized_basis(n)
+ sage: all( M.is_symmetric() for M in B )
+ True
+
+ """
+ field = ZZ
+ 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 these, we can drop back to the "field" that we
+ # started with instead of the quaternion algebra "Q".
+ return tuple( s.change_ring(field) for s in S )
+
+
+ def __init__(self, n, **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(QuaternionHermitianEJA, self).__init__(self._denormalized_basis(n),
+ self.jordan_product,
+ self.trace_inner_product,
+ **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 HadamardEJA(ConcreteEJA):
+ """
+ Return the Euclidean Jordan Algebra corresponding to the set
+ `R^n` under the Hadamard product.
+
+ Note: this is nothing more than the Cartesian product of ``n``
+ copies of the spin algebra. Once Cartesian product algebras
+ are implemented, this can go.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import HadamardEJA
+
+ EXAMPLES:
+
+ This multiplication table can be verified by hand::
+
+ sage: J = HadamardEJA(3)
+ sage: e0,e1,e2 = J.gens()
+ sage: e0*e0
+ e0
+ sage: e0*e1
+ 0
+ sage: e0*e2
+ 0
+ sage: e1*e1
+ e1
+ sage: e1*e2
+ 0
+ sage: e2*e2
+ e2
+
+ TESTS:
+
+ We can change the generator prefix::
+
+ sage: HadamardEJA(3, prefix='r').gens()
+ (r0, r1, r2)
+
+ """
+ def __init__(self, n, **kwargs):
+ if n == 0:
+ jordan_product = lambda x,y: x
+ inner_product = lambda x,y: x
+ else:
+ def jordan_product(x,y):
+ P = x.parent()
+ return P( xi*yi for (xi,yi) in zip(x,y) )
+
+ def inner_product(x,y):
+ return (x.T*y)[0,0]
+
+ # New defaults for keyword arguments. Don't orthonormalize
+ # because our basis is already orthonormal with respect to our
+ # inner-product. Don't check the axioms, because we know this
+ # is a valid EJA... but do double-check if the user passes
+ # check_axioms=True. Note: we DON'T override the "check_field"
+ # default here, because the user can pass in a field!
+ if "orthonormalize" not in kwargs: kwargs["orthonormalize"] = False
+ if "check_axioms" not in kwargs: kwargs["check_axioms"] = False
+
+ column_basis = tuple( b.column() for b in FreeModule(ZZ, n).basis() )
+ super().__init__(column_basis,
+ jordan_product,
+ inner_product,
+ associative=True,
+ **kwargs)
+ self.rank.set_cache(n)
+
+ if n == 0:
+ self.one.set_cache( self.zero() )
+ else:
+ self.one.set_cache( sum(self.gens()) )
+
+ @staticmethod
+ def _max_random_instance_size():
+ r"""
+ The maximum dimension of a random HadamardEJA.
+ """
+ return 5
+
+ @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 BilinearFormEJA(ConcreteEJA):
+ r"""
+ The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
+ with the half-trace inner product and jordan product ``x*y =
+ (<Bx,y>,y_bar>, x0*y_bar + y0*x_bar)`` where `B = 1 \times B22` is
+ a symmetric positive-definite "bilinear form" matrix. Its
+ dimension is the size of `B`, and it has rank two in dimensions
+ larger than two. It reduces to the ``JordanSpinEJA`` when `B` is
+ the identity matrix of order ``n``.
+
+ We insist that the one-by-one upper-left identity block of `B` be
+ passed in as well so that we can be passed a matrix of size zero
+ to construct a trivial algebra.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (BilinearFormEJA,
+ ....: JordanSpinEJA)
+
+ EXAMPLES:
+
+ When no bilinear form is specified, the identity matrix is used,
+ and the resulting algebra is the Jordan spin algebra::
+
+ sage: B = matrix.identity(AA,3)
+ sage: J0 = BilinearFormEJA(B)
+ sage: J1 = JordanSpinEJA(3)
+ sage: J0.multiplication_table() == J0.multiplication_table()
+ True
+
+ An error is raised if the matrix `B` does not correspond to a
+ positive-definite bilinear form::
+
+ sage: B = matrix.random(QQ,2,3)
+ sage: J = BilinearFormEJA(B)
+ Traceback (most recent call last):
+ ...
+ ValueError: bilinear form is not positive-definite
+ sage: B = matrix.zero(QQ,3)
+ sage: J = BilinearFormEJA(B)
+ Traceback (most recent call last):
+ ...
+ ValueError: bilinear form is not positive-definite
+
+ TESTS:
+
+ We can create a zero-dimensional algebra::
+
+ sage: B = matrix.identity(AA,0)
+ sage: J = BilinearFormEJA(B)
+ sage: J.basis()
+ Finite family {}
+
+ We can check the multiplication condition given in the Jordan, von
+ Neumann, and Wigner paper (and also discussed on my "On the
+ symmetry..." paper). Note that this relies heavily on the standard
+ choice of basis, as does anything utilizing the bilinear form
+ matrix. We opt not to orthonormalize the basis, because if we
+ did, we would have to normalize the `s_{i}` in a similar manner::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(5)
+ sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular')
+ sage: B11 = matrix.identity(QQ,1)
+ sage: B22 = M.transpose()*M
+ sage: B = block_matrix(2,2,[ [B11,0 ],
+ ....: [0, B22 ] ])
+ sage: J = BilinearFormEJA(B, orthonormalize=False)
+ sage: eis = VectorSpace(M.base_ring(), M.ncols()).basis()
+ sage: V = J.vector_space()
+ sage: sis = [ J( V([0] + (M.inverse()*ei).list()).column() )
+ ....: for ei in eis ]
+ sage: actual = [ sis[i]*sis[j]
+ ....: for i in range(n-1)
+ ....: for j in range(n-1) ]
+ sage: expected = [ J.one() if i == j else J.zero()
+ ....: for i in range(n-1)
+ ....: for j in range(n-1) ]
+ sage: actual == expected
+ True
+
+ """
+ def __init__(self, B, **kwargs):
+ # The matrix "B" is supplied by the user in most cases,
+ # so it makes sense to check whether or not its positive-
+ # definite unless we are specifically asked not to...
+ if ("check_axioms" not in kwargs) or kwargs["check_axioms"]:
+ if not B.is_positive_definite():
+ raise ValueError("bilinear form is not positive-definite")
+
+ # However, all of the other data for this EJA is computed
+ # by us in manner that guarantees the axioms are
+ # satisfied. So, again, unless we are specifically asked to
+ # verify things, we'll skip the rest of the checks.
+ if "check_axioms" not in kwargs: kwargs["check_axioms"] = False
+
+ def inner_product(x,y):
+ return (y.T*B*x)[0,0]
+
+ def jordan_product(x,y):
+ P = x.parent()
+ x0 = x[0,0]
+ xbar = x[1:,0]
+ y0 = y[0,0]
+ ybar = y[1:,0]
+ z0 = inner_product(y,x)
+ zbar = y0*xbar + x0*ybar
+ return P([z0] + zbar.list())
+
+ n = B.nrows()
+ column_basis = tuple( b.column() for b in FreeModule(ZZ, n).basis() )
+ super(BilinearFormEJA, self).__init__(column_basis,
+ jordan_product,
+ inner_product,
+ **kwargs)
+
+ # The rank of this algebra is two, unless we're in a
+ # one-dimensional ambient space (because the rank is bounded
+ # by the ambient dimension).
+ self.rank.set_cache(min(n,2))
+
+ if n == 0:
+ self.one.set_cache( self.zero() )
+ else:
+ self.one.set_cache( self.monomial(0) )
+
+ @staticmethod
+ def _max_random_instance_size():
+ r"""
+ The maximum dimension of a random BilinearFormEJA.
+ """
+ return 5
+
+ @classmethod
+ def random_instance(cls, **kwargs):
+ """
+ Return a random instance of this algebra.
+ """
+ n = ZZ.random_element(cls._max_random_instance_size() + 1)
+ if n.is_zero():
+ B = matrix.identity(ZZ, n)
+ return cls(B, **kwargs)
+
+ B11 = matrix.identity(ZZ, 1)
+ M = matrix.random(ZZ, n-1)
+ I = matrix.identity(ZZ, n-1)
+ alpha = ZZ.zero()
+ while alpha.is_zero():
+ alpha = ZZ.random_element().abs()
+ B22 = M.transpose()*M + alpha*I
+
+ from sage.matrix.special import block_matrix
+ B = block_matrix(2,2, [ [B11, ZZ(0) ],
+ [ZZ(0), B22 ] ])
+
+ return cls(B, **kwargs)
+
+
+class JordanSpinEJA(BilinearFormEJA):
+ """
+ The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
+ with the usual inner product and jordan product ``x*y =
+ (<x,y>, x0*y_bar + y0*x_bar)``. It has dimension `n` over
+ the reals.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import JordanSpinEJA
+
+ EXAMPLES:
+
+ This multiplication table can be verified by hand::
+
+ sage: J = JordanSpinEJA(4)