+ self.rank.set_cache(0)
+ self.one.set_cache( self.zero() )
+
+ @classmethod
+ def random_instance(cls, **kwargs):
+ # We don't take a "size" argument so the superclass method is
+ # inappropriate for us.
+ return cls(**kwargs)
+
+
+class CartesianProductEJA(FiniteDimensionalEJA):
+ r"""
+ The external (orthogonal) direct sum of two or more Euclidean
+ Jordan algebras. Every Euclidean Jordan algebra decomposes into an
+ orthogonal direct sum of simple Euclidean Jordan algebras which is
+ then isometric to a Cartesian product, so no generality is lost by
+ providing only this construction.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (random_eja,
+ ....: CartesianProductEJA,
+ ....: HadamardEJA,
+ ....: JordanSpinEJA,
+ ....: RealSymmetricEJA)
+
+ EXAMPLES:
+
+ The Jordan product is inherited from our factors and implemented by
+ our CombinatorialFreeModule Cartesian product superclass::
+
+ sage: set_random_seed()
+ sage: J1 = HadamardEJA(2)
+ sage: J2 = RealSymmetricEJA(2)
+ sage: J = cartesian_product([J1,J2])
+ sage: x,y = J.random_elements(2)
+ sage: x*y in J
+ True
+
+ The ability to retrieve the original factors is implemented by our
+ CombinatorialFreeModule Cartesian product superclass::
+
+ sage: J1 = HadamardEJA(2, field=QQ)
+ sage: J2 = JordanSpinEJA(3, field=QQ)
+ sage: J = cartesian_product([J1,J2])
+ sage: J.cartesian_factors()
+ (Euclidean Jordan algebra of dimension 2 over Rational Field,
+ Euclidean Jordan algebra of dimension 3 over Rational Field)
+
+ You can provide more than two factors::
+
+ sage: J1 = HadamardEJA(2)
+ sage: J2 = JordanSpinEJA(3)
+ sage: J3 = RealSymmetricEJA(3)
+ sage: cartesian_product([J1,J2,J3])
+ Euclidean Jordan algebra of dimension 2 over Algebraic Real
+ Field (+) Euclidean Jordan algebra of dimension 3 over Algebraic
+ Real Field (+) Euclidean Jordan algebra of dimension 6 over
+ Algebraic Real Field
+
+ Rank is additive on a Cartesian product::
+
+ sage: J1 = HadamardEJA(1)
+ sage: J2 = RealSymmetricEJA(2)
+ sage: J = cartesian_product([J1,J2])
+ sage: J1.rank.clear_cache()
+ sage: J2.rank.clear_cache()
+ sage: J.rank.clear_cache()
+ sage: J.rank()
+ 3
+ sage: J.rank() == J1.rank() + J2.rank()
+ True
+
+ The same rank computation works over the rationals, with whatever
+ basis you like::
+
+ sage: J1 = HadamardEJA(1, field=QQ, orthonormalize=False)
+ sage: J2 = RealSymmetricEJA(2, field=QQ, orthonormalize=False)
+ sage: J = cartesian_product([J1,J2])
+ sage: J1.rank.clear_cache()
+ sage: J2.rank.clear_cache()
+ sage: J.rank.clear_cache()
+ sage: J.rank()
+ 3
+ sage: J.rank() == J1.rank() + J2.rank()
+ True
+
+ The product algebra will be associative if and only if all of its
+ components are associative::
+
+ sage: J1 = HadamardEJA(2)
+ sage: J1.is_associative()
+ True
+ sage: J2 = HadamardEJA(3)
+ sage: J2.is_associative()
+ True
+ sage: J3 = RealSymmetricEJA(3)
+ sage: J3.is_associative()
+ False
+ sage: CP1 = cartesian_product([J1,J2])
+ sage: CP1.is_associative()
+ True
+ sage: CP2 = cartesian_product([J1,J3])
+ sage: CP2.is_associative()
+ False
+
+ Cartesian products of Cartesian products work::
+
+ sage: J1 = JordanSpinEJA(1)
+ sage: J2 = JordanSpinEJA(1)
+ sage: J3 = JordanSpinEJA(1)
+ sage: J = cartesian_product([J1,cartesian_product([J2,J3])])
+ sage: J.multiplication_table()
+ +----++----+----+----+
+ | * || b0 | b1 | b2 |
+ +====++====+====+====+
+ | b0 || b0 | 0 | 0 |
+ +----++----+----+----+
+ | b1 || 0 | b1 | 0 |
+ +----++----+----+----+
+ | b2 || 0 | 0 | b2 |
+ +----++----+----+----+
+ sage: HadamardEJA(3).multiplication_table()
+ +----++----+----+----+
+ | * || b0 | b1 | b2 |
+ +====++====+====+====+
+ | b0 || b0 | 0 | 0 |
+ +----++----+----+----+
+ | b1 || 0 | b1 | 0 |
+ +----++----+----+----+
+ | b2 || 0 | 0 | b2 |
+ +----++----+----+----+
+
+ TESTS:
+
+ All factors must share the same base field::
+
+ sage: J1 = HadamardEJA(2, field=QQ)
+ sage: J2 = RealSymmetricEJA(2)
+ sage: CartesianProductEJA((J1,J2))
+ Traceback (most recent call last):
+ ...
+ ValueError: all factors must share the same base field
+
+ The cached unit element is the same one that would be computed::
+
+ sage: set_random_seed() # long time
+ sage: J1 = random_eja() # long time
+ sage: J2 = random_eja() # long time
+ sage: J = cartesian_product([J1,J2]) # long time
+ sage: actual = J.one() # long time
+ sage: J.one.clear_cache() # long time
+ sage: expected = J.one() # long time
+ sage: actual == expected # long time
+ True
+
+ """
+ Element = FiniteDimensionalEJAElement
+
+
+ def __init__(self, factors, **kwargs):
+ m = len(factors)
+ if m == 0:
+ return TrivialEJA()
+
+ self._sets = factors
+
+ field = factors[0].base_ring()
+ if not all( J.base_ring() == field for J in factors ):
+ raise ValueError("all factors must share the same base field")
+
+ associative = all( f.is_associative() for f in factors )
+
+ # Compute my matrix space. This category isn't perfect, but
+ # is good enough for what we need to do.
+ MS_cat = MagmaticAlgebras(field).FiniteDimensional().WithBasis()
+ MS_cat = MS_cat.Unital().CartesianProducts()
+ MS_factors = tuple( J.matrix_space() for J in factors )
+ from sage.sets.cartesian_product import CartesianProduct
+ MS = CartesianProduct(MS_factors, MS_cat)
+
+ basis = []
+ zero = MS.zero()
+ for i in range(m):
+ for b in factors[i].matrix_basis():
+ z = list(zero)
+ z[i] = b
+ basis.append(z)
+
+ basis = tuple( MS(b) for b in basis )
+
+ # Define jordan/inner products that operate on that matrix_basis.
+ def jordan_product(x,y):
+ return MS(tuple(
+ (factors[i](x[i])*factors[i](y[i])).to_matrix()
+ for i in range(m)
+ ))
+
+ def inner_product(x, y):
+ return sum(
+ factors[i](x[i]).inner_product(factors[i](y[i]))
+ for i in range(m)
+ )
+
+ # There's no need to check the field since it already came
+ # from an EJA. Likewise the axioms are guaranteed to be
+ # satisfied, unless the guy writing this class sucks.
+ #
+ # If you want the basis to be orthonormalized, orthonormalize
+ # the factors.
+ FiniteDimensionalEJA.__init__(self,
+ basis,
+ jordan_product,
+ inner_product,
+ field=field,
+ matrix_space=MS,
+ orthonormalize=False,
+ associative=associative,
+ cartesian_product=True,
+ check_field=False,
+ check_axioms=False)
+
+ self.rank.set_cache(sum(J.rank() for J in factors))
+ ones = tuple(J.one().to_matrix() for J in factors)
+ self.one.set_cache(self(ones))
+
+ def cartesian_factors(self):
+ # Copy/pasted from CombinatorialFreeModule_CartesianProduct.
+ return self._sets
+
+ def cartesian_factor(self, i):
+ r"""
+ Return the ``i``th factor of this algebra.
+ """
+ return self._sets[i]
+
+ def _repr_(self):
+ # Copy/pasted from CombinatorialFreeModule_CartesianProduct.
+ from sage.categories.cartesian_product import cartesian_product
+ return cartesian_product.symbol.join("%s" % factor
+ for factor in self._sets)
+
+ def matrix_space(self):
+ r"""
+ Return the space that our matrix basis lives in as a Cartesian
+ product.
+
+ We don't simply use the ``cartesian_product()`` functor here
+ because it acts differently on SageMath MatrixSpaces and our
+ custom MatrixAlgebras, which are CombinatorialFreeModules. We
+ always want the result to be represented (and indexed) as
+ an ordered tuple.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
+ ....: HadamardEJA,
+ ....: OctonionHermitianEJA,
+ ....: RealSymmetricEJA)
+
+ EXAMPLES::
+
+ sage: J1 = HadamardEJA(1)
+ sage: J2 = RealSymmetricEJA(2)
+ sage: J = cartesian_product([J1,J2])
+ sage: J.matrix_space()
+ The Cartesian product of (Full MatrixSpace of 1 by 1 dense
+ matrices over Algebraic Real Field, Full MatrixSpace of 2
+ by 2 dense matrices over Algebraic Real Field)
+
+ ::
+
+ sage: J1 = ComplexHermitianEJA(1)
+ sage: J2 = ComplexHermitianEJA(1)
+ sage: J = cartesian_product([J1,J2])
+ sage: J.one().to_matrix()[0]
+ +---+
+ | 1 |
+ +---+
+ sage: J.one().to_matrix()[1]
+ +---+
+ | 1 |
+ +---+
+
+ ::
+
+ sage: J1 = OctonionHermitianEJA(1)
+ sage: J2 = OctonionHermitianEJA(1)
+ sage: J = cartesian_product([J1,J2])
+ sage: J.one().to_matrix()[0]
+ +----+
+ | e0 |
+ +----+
+ sage: J.one().to_matrix()[1]
+ +----+
+ | e0 |
+ +----+
+
+ """
+ return super().matrix_space()
+
+
+ @cached_method
+ def cartesian_projection(self, i):
+ r"""
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (random_eja,
+ ....: JordanSpinEJA,
+ ....: HadamardEJA,
+ ....: RealSymmetricEJA,
+ ....: ComplexHermitianEJA)
+
+ EXAMPLES:
+
+ The projection morphisms are Euclidean Jordan algebra
+ operators::
+
+ sage: J1 = HadamardEJA(2)
+ sage: J2 = RealSymmetricEJA(2)
+ sage: J = cartesian_product([J1,J2])
+ sage: J.cartesian_projection(0)
+ Linear operator between finite-dimensional Euclidean Jordan
+ algebras represented by the matrix:
+ [1 0 0 0 0]
+ [0 1 0 0 0]
+ Domain: Euclidean Jordan algebra of dimension 2 over Algebraic
+ Real Field (+) Euclidean Jordan algebra of dimension 3 over
+ Algebraic Real Field
+ Codomain: Euclidean Jordan algebra of dimension 2 over Algebraic
+ Real Field
+ sage: J.cartesian_projection(1)
+ Linear operator between finite-dimensional Euclidean Jordan
+ algebras represented by the matrix:
+ [0 0 1 0 0]
+ [0 0 0 1 0]
+ [0 0 0 0 1]
+ Domain: Euclidean Jordan algebra of dimension 2 over Algebraic
+ Real Field (+) Euclidean Jordan algebra of dimension 3 over
+ Algebraic Real Field
+ Codomain: Euclidean Jordan algebra of dimension 3 over Algebraic
+ Real Field
+
+ The projections work the way you'd expect on the vector
+ representation of an element::
+
+ sage: J1 = JordanSpinEJA(2)
+ sage: J2 = ComplexHermitianEJA(2)
+ sage: J = cartesian_product([J1,J2])
+ sage: pi_left = J.cartesian_projection(0)
+ sage: pi_right = J.cartesian_projection(1)
+ sage: pi_left(J.one()).to_vector()
+ (1, 0)
+ sage: pi_right(J.one()).to_vector()
+ (1, 0, 0, 1)
+ sage: J.one().to_vector()
+ (1, 0, 1, 0, 0, 1)
+
+ TESTS:
+
+ The answer never changes::
+
+ sage: set_random_seed()
+ sage: J1 = random_eja()
+ sage: J2 = random_eja()
+ sage: J = cartesian_product([J1,J2])
+ sage: P0 = J.cartesian_projection(0)
+ sage: P1 = J.cartesian_projection(0)
+ sage: P0 == P1
+ True
+
+ """
+ offset = sum( self.cartesian_factor(k).dimension()
+ for k in range(i) )
+ Ji = self.cartesian_factor(i)
+ Pi = self._module_morphism(lambda j: Ji.monomial(j - offset),
+ codomain=Ji)
+
+ return FiniteDimensionalEJAOperator(self,Ji,Pi.matrix())
+
+ @cached_method
+ def cartesian_embedding(self, i):
+ r"""
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (random_eja,
+ ....: JordanSpinEJA,
+ ....: HadamardEJA,
+ ....: RealSymmetricEJA)
+
+ EXAMPLES:
+
+ The embedding morphisms are Euclidean Jordan algebra
+ operators::
+
+ sage: J1 = HadamardEJA(2)
+ sage: J2 = RealSymmetricEJA(2)
+ sage: J = cartesian_product([J1,J2])
+ sage: J.cartesian_embedding(0)
+ Linear operator between finite-dimensional Euclidean Jordan
+ algebras represented by the matrix:
+ [1 0]
+ [0 1]
+ [0 0]
+ [0 0]
+ [0 0]
+ Domain: Euclidean Jordan algebra of dimension 2 over
+ Algebraic Real Field
+ Codomain: Euclidean Jordan algebra of dimension 2 over
+ Algebraic Real Field (+) Euclidean Jordan algebra of
+ dimension 3 over Algebraic Real Field
+ sage: J.cartesian_embedding(1)
+ Linear operator between finite-dimensional Euclidean Jordan
+ algebras represented by the matrix:
+ [0 0 0]
+ [0 0 0]
+ [1 0 0]
+ [0 1 0]
+ [0 0 1]
+ Domain: Euclidean Jordan algebra of dimension 3 over
+ Algebraic Real Field
+ Codomain: Euclidean Jordan algebra of dimension 2 over
+ Algebraic Real Field (+) Euclidean Jordan algebra of
+ dimension 3 over Algebraic Real Field
+
+ The embeddings work the way you'd expect on the vector
+ representation of an element::
+
+ sage: J1 = JordanSpinEJA(3)
+ sage: J2 = RealSymmetricEJA(2)
+ sage: J = cartesian_product([J1,J2])
+ sage: iota_left = J.cartesian_embedding(0)
+ sage: iota_right = J.cartesian_embedding(1)
+ sage: iota_left(J1.zero()) == J.zero()
+ True
+ sage: iota_right(J2.zero()) == J.zero()
+ True
+ sage: J1.one().to_vector()
+ (1, 0, 0)
+ sage: iota_left(J1.one()).to_vector()
+ (1, 0, 0, 0, 0, 0)
+ sage: J2.one().to_vector()
+ (1, 0, 1)
+ sage: iota_right(J2.one()).to_vector()
+ (0, 0, 0, 1, 0, 1)
+ sage: J.one().to_vector()
+ (1, 0, 0, 1, 0, 1)
+
+ TESTS:
+
+ The answer never changes::
+
+ sage: set_random_seed()
+ sage: J1 = random_eja()
+ sage: J2 = random_eja()
+ sage: J = cartesian_product([J1,J2])
+ sage: E0 = J.cartesian_embedding(0)
+ sage: E1 = J.cartesian_embedding(0)
+ sage: E0 == E1
+ True
+
+ Composing a projection with the corresponding inclusion should
+ produce the identity map, and mismatching them should produce
+ the zero map::
+
+ sage: set_random_seed()
+ sage: J1 = random_eja()
+ sage: J2 = random_eja()
+ sage: J = cartesian_product([J1,J2])
+ sage: iota_left = J.cartesian_embedding(0)
+ sage: iota_right = J.cartesian_embedding(1)
+ sage: pi_left = J.cartesian_projection(0)
+ sage: pi_right = J.cartesian_projection(1)
+ sage: pi_left*iota_left == J1.one().operator()
+ True
+ sage: pi_right*iota_right == J2.one().operator()
+ True
+ sage: (pi_left*iota_right).is_zero()
+ True
+ sage: (pi_right*iota_left).is_zero()
+ True
+
+ """
+ offset = sum( self.cartesian_factor(k).dimension()
+ for k in range(i) )
+ Ji = self.cartesian_factor(i)
+ Ei = Ji._module_morphism(lambda j: self.monomial(j + offset),
+ codomain=self)
+ return FiniteDimensionalEJAOperator(Ji,self,Ei.matrix())
+
+
+
+FiniteDimensionalEJA.CartesianProduct = CartesianProductEJA
+
+class RationalBasisCartesianProductEJA(CartesianProductEJA,
+ RationalBasisEJA):
+ r"""
+ A separate class for products of algebras for which we know a
+ rational basis.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (HadamardEJA,
+ ....: JordanSpinEJA,
+ ....: OctonionHermitianEJA,
+ ....: RealSymmetricEJA)
+
+ EXAMPLES:
+
+ This gives us fast characteristic polynomial computations in
+ product algebras, too::
+
+
+ sage: J1 = JordanSpinEJA(2)
+ sage: J2 = RealSymmetricEJA(3)
+ sage: J = cartesian_product([J1,J2])
+ sage: J.characteristic_polynomial_of().degree()
+ 5
+ sage: J.rank()
+ 5
+
+ TESTS:
+
+ The ``cartesian_product()`` function only uses the first factor to
+ decide where the result will live; thus we have to be careful to
+ check that all factors do indeed have a `_rational_algebra` member
+ before we try to access it::
+
+ sage: J1 = OctonionHermitianEJA(1) # no rational basis
+ sage: J2 = HadamardEJA(2)
+ sage: cartesian_product([J1,J2])
+ Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
+ (+) Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
+ sage: cartesian_product([J2,J1])
+ Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
+ (+) Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
+
+ """
+ def __init__(self, algebras, **kwargs):
+ CartesianProductEJA.__init__(self, algebras, **kwargs)
+
+ self._rational_algebra = None
+ if self.vector_space().base_field() is not QQ:
+ if all( hasattr(r, "_rational_algebra") for r in algebras ):
+ self._rational_algebra = cartesian_product([
+ r._rational_algebra for r in algebras
+ ])
+
+
+RationalBasisEJA.CartesianProduct = RationalBasisCartesianProductEJA
+
+def random_eja(*args, **kwargs):
+ J1 = ConcreteEJA.random_instance(*args, **kwargs)
+
+ # This might make Cartesian products appear roughly as often as
+ # any other ConcreteEJA.
+ if ZZ.random_element(len(ConcreteEJA.__subclasses__()) + 1) == 0:
+ # Use random_eja() again so we can get more than two factors.
+ J2 = random_eja(*args, **kwargs)
+ J = cartesian_product([J1,J2])
+ return J
+ else:
+ return J1