]> gitweb.michael.orlitzky.com - sage.d.git/blobdiff - mjo/eja/eja_algebra.py
eja: add more introductory documentation.
[sage.d.git] / mjo / eja / eja_algebra.py
index 7255533072acda6e0a92664b2d2439004a0beb95..8b37a83602ef9b00b5a14c55785c2163b44df32b 100644 (file)
@@ -52,6 +52,98 @@ the other algebras. Cartesian products of these are also supported
 using the usual ``cartesian_product()`` function; as a result, we
 support (up to isomorphism) all Euclidean Jordan algebras.
 
+At a minimum, the following are required to construct a Euclidean
+Jordan algebra:
+
+  * A basis of matrices, column vectors, or MatrixAlgebra elements
+  * A Jordan product defined on the basis
+  * Its inner product defined on the basis
+
+The real numbers form a Euclidean Jordan algebra when both the Jordan
+and inner products are the usual multiplication. We use this as our
+example, and demonstrate a few ways to construct an EJA.
+
+First, we can use one-by-one SageMath matrices with algebraic real
+entries to represent real numbers. We define the Jordan and inner
+products to be essentially real-number multiplication, with the only
+difference being that the Jordan product again returns a one-by-one
+matrix, whereas the inner product must return a scalar. Our basis for
+the one-by-one matrices is of course the set consisting of a single
+matrix with its sole entry non-zero::
+
+    sage: from mjo.eja.eja_algebra import FiniteDimensionalEJA
+    sage: jp = lambda X,Y: X*Y
+    sage: ip = lambda X,Y: X[0,0]*Y[0,0]
+    sage: b1 = matrix(AA, [[1]])
+    sage: J1 = FiniteDimensionalEJA((b1,), jp, ip)
+    sage: J1
+    Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
+
+In fact, any positive scalar multiple of that inner-product would work::
+
+    sage: ip2 = lambda X,Y: 16*ip(X,Y)
+    sage: J2 = FiniteDimensionalEJA((b1,), jp, ip2)
+    sage: J2
+    Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
+
+But beware that your basis will be orthonormalized _with respect to the
+given inner-product_ unless you pass ``orthonormalize=False`` to the
+constructor. For example::
+
+    sage: J3 = FiniteDimensionalEJA((b1,), jp, ip2, orthonormalize=False)
+    sage: J3
+    Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
+
+To see the difference, you can take the first and only basis element
+of the resulting algebra, and ask for it to be converted back into
+matrix form::
+
+    sage: J1.basis()[0].to_matrix()
+    [1]
+    sage: J2.basis()[0].to_matrix()
+    [1/4]
+    sage: J3.basis()[0].to_matrix()
+    [1]
+
+Since square roots are used in that process, the default scalar field
+that we use is the field of algebraic real numbers, ``AA``. You can
+also Use rational numbers, but only if you either pass
+``orthonormalize=False`` or know that orthonormalizing your basis
+won't stray beyond the rational numbers. The example above would
+have worked only because ``sqrt(16) == 4`` is rational.
+
+Another option for your basis is to use elemebts of a
+:class:`MatrixAlgebra`::
+
+    sage: from mjo.matrix_algebra import MatrixAlgebra
+    sage: A = MatrixAlgebra(1,AA,AA)
+    sage: J4 = FiniteDimensionalEJA(A.gens(), jp, ip)
+    sage: J4
+    Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
+    sage: J4.basis()[0].to_matrix()
+    +---+
+    | 1 |
+    +---+
+
+An easier way to view the entire EJA basis in its original (but
+perhaps orthonormalized) matrix form is to use the ``matrix_basis``
+method::
+
+    sage: J4.matrix_basis()
+    (+---+
+    | 1 |
+     +---+,)
+
+In particular, a :class:`MatrixAlgebra` is needed to work around the
+fact that matrices in SageMath must have entries in the same
+(commutative and associative) ring as its scalars. There are many
+Euclidean Jordan algebras whose elements are matrices that violate
+those assumptions. The complex, quaternion, and octonion Hermitian
+matrices all have entries in a ring (the complex numbers, quaternions,
+or octonions...) that differs from the algebra's scalar ring (the real
+numbers). Quaternions are also non-commutative; the octonions are
+neither commutative nor associative.
+
 SETUP::
 
     sage: from mjo.eja.eja_algebra import random_eja
@@ -104,6 +196,11 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
         product. This will be applied to ``basis`` to compute an
         inner-product table (basically a matrix) for this algebra.
 
+      - ``matrix_space`` -- the space that your matrix basis lives in,
+        or ``None`` (the default). So long as your basis does not have
+        length zero you can omit this. But in trivial algebras, it is
+        required.
+
       - ``field`` -- a subfield of the reals (default: ``AA``); the scalar
         field for the algebra.
 
@@ -128,7 +225,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
         sage: basis = tuple(b.superalgebra_element() for b in A.basis())
         sage: J.subalgebra(basis, orthonormalize=False).is_associative()
         True
-
     """
     Element = FiniteDimensionalEJAElement
 
@@ -137,6 +233,7 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
                  jordan_product,
                  inner_product,
                  field=AA,
+                 matrix_space=None,
                  orthonormalize=True,
                  associative=None,
                  cartesian_product=False,
@@ -236,8 +333,14 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
             basis = tuple(gram_schmidt(basis, inner_product))
 
         # Save the (possibly orthonormalized) matrix basis for
-        # later...
+        # later, as well as the space that its elements live in.
+        # In most cases we can deduce the matrix space, but when
+        # n == 0 (that is, there are no basis elements) we cannot.
         self._matrix_basis = basis
+        if matrix_space is None:
+            self._matrix_space = self._matrix_basis[0].parent()
+        else:
+            self._matrix_space = matrix_space
 
         # Now create the vector space for the algebra, which will have
         # its own set of non-ambient coordinates (in terms of the
@@ -1030,10 +1133,7 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
             the scalar ring Rational Field
 
         """
-        if self.is_trivial():
-            return MatrixSpace(self.base_ring(), 0)
-        else:
-            return self.matrix_basis()[0].parent()
+        return self._matrix_space
 
 
     @cached_method
@@ -1610,6 +1710,7 @@ class RationalBasisEJA(FiniteDimensionalEJA):
                                        jordan_product,
                                        inner_product,
                                        field=QQ,
+                                       matrix_space=self.matrix_space(),
                                        associative=self.is_associative(),
                                        orthonormalize=False,
                                        check_field=False,
@@ -1706,25 +1807,35 @@ class ConcreteEJA(FiniteDimensionalEJA):
     """
 
     @staticmethod
-    def _max_random_instance_size():
+    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. Unfortunately, the term "size" is ambiguous -- when
-        dealing with `R^n` under either the Hadamard or Jordan spin
-        product, the "size" refers to the dimension `n`. When dealing
-        with a matrix algebra (real symmetric or complex/quaternion
-        Hermitian), it refers to the size of the matrix, which is far
-        less than the dimension of the underlying vector space.
+        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, *args, **kwargs):
+    def random_instance(cls, max_dimension=None, *args, **kwargs):
         """
-        Return a random instance of this type of algebra.
+        Return a random instance of this type of algebra whose dimension
+        is less than or equal to ``max_dimension``. If the dimension bound
+        is omitted, then the ``_max_random_instance_dimension()`` is used
+        to get a suitable bound.
 
         This method should be implemented in each subclass.
         """
@@ -1737,7 +1848,7 @@ class ConcreteEJA(FiniteDimensionalEJA):
         return eja_class.random_instance(*args, **kwargs)
 
 
-class MatrixEJA:
+class MatrixEJA(FiniteDimensionalEJA):
     @staticmethod
     def _denormalized_basis(A):
         """
@@ -1878,8 +1989,23 @@ class MatrixEJA:
         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(RationalBasisEJA, ConcreteEJA, MatrixEJA):
+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
@@ -1913,8 +2039,8 @@ class RealSymmetricEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA):
     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: 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
@@ -1945,15 +2071,20 @@ class RealSymmetricEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA):
 
     """
     @staticmethod
-    def _max_random_instance_size():
-        return 4 # Dimension 10
+    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, **kwargs):
+    def random_instance(cls, max_dimension=None, **kwargs):
         """
         Return a random instance of this type of algebra.
         """
-        n = ZZ.random_element(cls._max_random_instance_size() + 1)
+        if max_dimension is None:
+            max_dimension = cls._max_random_instance_dimension()
+        max_size = cls._max_random_instance_size(max_dimension) + 1
+        n = ZZ.random_element(max_size)
         return cls(n, **kwargs)
 
     def __init__(self, n, field=AA, **kwargs):
@@ -1962,21 +2093,19 @@ class RealSymmetricEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA):
         if "check_axioms" not in kwargs: kwargs["check_axioms"] = False
 
         A = MatrixSpace(field, n)
-        super().__init__(self._denormalized_basis(A),
-                         self.jordan_product,
-                         self.trace_inner_product,
-                         field=field,
-                         **kwargs)
+        super().__init__(A, **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)
-        self.one.set_cache(self(A.one()))
+        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(RationalBasisEJA, ConcreteEJA, MatrixEJA):
+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,
@@ -1989,21 +2118,36 @@ class ComplexHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA):
 
     EXAMPLES:
 
-    In theory, our "field" can be any subfield of the reals::
+    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: 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
+        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: n_max = ComplexHermitianEJA._max_random_instance_size()
-        sage: n = ZZ.random_element(1, n_max)
+        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
@@ -2040,32 +2184,34 @@ class ComplexHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA):
 
         from mjo.hurwitz import ComplexMatrixAlgebra
         A = ComplexMatrixAlgebra(n, scalars=field)
-        super().__init__(self._denormalized_basis(A),
-                         self.jordan_product,
-                         self.trace_inner_product,
-                         field=field,
-                         **kwargs)
+        super().__init__(A, **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)
-        self.one.set_cache(self(A.one()))
+        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():
-        return 3 # Dimension 9
+    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, **kwargs):
+    def random_instance(cls, max_dimension=None, **kwargs):
         """
         Return a random instance of this type of algebra.
         """
-        n = ZZ.random_element(cls._max_random_instance_size() + 1)
+        if max_dimension is None:
+            max_dimension = cls._max_random_instance_dimension()
+        n = ZZ.random_element(cls._max_random_instance_size(max_dimension) + 1)
         return cls(n, **kwargs)
 
 
-class QuaternionHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA):
+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
@@ -2091,8 +2237,8 @@ class QuaternionHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA):
     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: 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
@@ -2129,35 +2275,38 @@ class QuaternionHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA):
 
         from mjo.hurwitz import QuaternionMatrixAlgebra
         A = QuaternionMatrixAlgebra(n, scalars=field)
-        super().__init__(self._denormalized_basis(A),
-                         self.jordan_product,
-                         self.trace_inner_product,
-                         field=field,
-                         **kwargs)
+        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)
 
-        # 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)
-        self.one.set_cache(self(A.one()))
 
 
     @staticmethod
-    def _max_random_instance_size():
+    def _max_random_instance_size(max_dimension):
         r"""
         The maximum rank of a random QuaternionHermitianEJA.
         """
-        return 2 # Dimension 6
+        # 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, **kwargs):
+    def random_instance(cls, max_dimension=None, **kwargs):
         """
         Return a random instance of this type of algebra.
         """
-        n = ZZ.random_element(cls._max_random_instance_size() + 1)
+        if max_dimension is None:
+            max_dimension = cls._max_random_instance_dimension()
+        n = ZZ.random_element(cls._max_random_instance_size(max_dimension) + 1)
         return cls(n, **kwargs)
 
-class OctonionHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA):
+class OctonionHermitianEJA(MatrixEJA, RationalBasisEJA, ConcreteEJA):
     r"""
     SETUP::
 
@@ -2245,18 +2394,29 @@ class OctonionHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA):
 
     """
     @staticmethod
-    def _max_random_instance_size():
+    def _max_random_instance_size(max_dimension):
         r"""
         The maximum rank of a random QuaternionHermitianEJA.
         """
-        return 1 # Dimension 1
+        # 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, **kwargs):
+    def random_instance(cls, max_dimension=None, **kwargs):
         """
         Return a random instance of this type of algebra.
         """
-        n = ZZ.random_element(cls._max_random_instance_size() + 1)
+        if max_dimension is None:
+            max_dimension = cls._max_random_instance_dimension()
+        n = ZZ.random_element(cls._max_random_instance_size(max_dimension) + 1)
         return cls(n, **kwargs)
 
     def __init__(self, n, field=AA, **kwargs):
@@ -2270,17 +2430,15 @@ class OctonionHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA):
 
         from mjo.hurwitz import OctonionMatrixAlgebra
         A = OctonionMatrixAlgebra(n, scalars=field)
-        super().__init__(self._denormalized_basis(A),
-                         self.jordan_product,
-                         self.trace_inner_product,
-                         field=field,
-                         **kwargs)
+        super().__init__(A, **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)
-        self.one.set_cache(self(A.one()))
+        from mjo.eja.eja_cache import octonion_hermitian_eja_coeffs
+        a = octonion_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)
 
 
 class AlbertEJA(OctonionHermitianEJA):
@@ -2345,13 +2503,14 @@ class HadamardEJA(RationalBasisEJA, ConcreteEJA):
         (r0, r1, r2)
     """
     def __init__(self, n, field=AA, **kwargs):
+        MS = MatrixSpace(field, n, 1)
+
         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) )
+                return MS( xi*yi for (xi,yi) in zip(x,y) )
 
             def inner_product(x,y):
                 return (x.T*y)[0,0]
@@ -2365,34 +2524,41 @@ class HadamardEJA(RationalBasisEJA, ConcreteEJA):
         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(field, n).basis() )
+        column_basis = tuple( MS(b) for b in FreeModule(field, n).basis() )
         super().__init__(column_basis,
                          jordan_product,
                          inner_product,
                          field=field,
+                         matrix_space=MS,
                          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()) )
+        self.one.set_cache( self.sum(self.gens()) )
 
     @staticmethod
-    def _max_random_instance_size():
+    def _max_random_instance_dimension():
         r"""
-        The maximum dimension of a random HadamardEJA.
+        There's no reason to go higher than five here. That's
+        enough to get the point across.
         """
         return 5
 
+    @staticmethod
+    def _max_random_instance_size(max_dimension):
+        r"""
+        The maximum size (=dimension) of a random HadamardEJA.
+        """
+        return max_dimension
+
     @classmethod
-    def random_instance(cls, **kwargs):
+    def random_instance(cls, max_dimension=None, **kwargs):
         """
         Return a random instance of this type of algebra.
         """
-        n = ZZ.random_element(cls._max_random_instance_size() + 1)
+        if max_dimension is None:
+            max_dimension = cls._max_random_instance_dimension()
+        n = ZZ.random_element(cls._max_random_instance_size(max_dimension) + 1)
         return cls(n, **kwargs)
 
 
@@ -2492,22 +2658,22 @@ class BilinearFormEJA(RationalBasisEJA, ConcreteEJA):
         # verify things, we'll skip the rest of the checks.
         if "check_axioms" not in kwargs: kwargs["check_axioms"] = False
 
+        n = B.nrows()
+        MS = MatrixSpace(field, n, 1)
+
         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())
+            return MS([z0] + zbar.list())
 
-        n = B.nrows()
-        column_basis = tuple( b.column()
-                              for b in FreeModule(field, n).basis() )
+        column_basis = tuple( MS(b) for b in FreeModule(field, n).basis() )
 
         # TODO: I haven't actually checked this, but it seems legit.
         associative = False
@@ -2518,6 +2684,7 @@ class BilinearFormEJA(RationalBasisEJA, ConcreteEJA):
                          jordan_product,
                          inner_product,
                          field=field,
+                         matrix_space=MS,
                          associative=associative,
                          **kwargs)
 
@@ -2525,25 +2692,34 @@ class BilinearFormEJA(RationalBasisEJA, ConcreteEJA):
         # 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():
+    def _max_random_instance_dimension():
         r"""
-        The maximum dimension of a random BilinearFormEJA.
+        There's no reason to go higher than five here. That's
+        enough to get the point across.
         """
         return 5
 
+    @staticmethod
+    def _max_random_instance_size(max_dimension):
+        r"""
+        The maximum size (=dimension) of a random BilinearFormEJA.
+        """
+        return max_dimension
+
     @classmethod
-    def random_instance(cls, **kwargs):
+    def random_instance(cls, max_dimension=None, **kwargs):
         """
         Return a random instance of this algebra.
         """
-        n = ZZ.random_element(cls._max_random_instance_size() + 1)
+        if max_dimension is None:
+            max_dimension = cls._max_random_instance_dimension()
+        n = ZZ.random_element(cls._max_random_instance_size(max_dimension) + 1)
         if n.is_zero():
             B = matrix.identity(ZZ, n)
             return cls(B, **kwargs)
@@ -2626,21 +2802,16 @@ class JordanSpinEJA(BilinearFormEJA):
         # can pass in a field!
         super().__init__(B, *args, **kwargs)
 
-    @staticmethod
-    def _max_random_instance_size():
-        r"""
-        The maximum dimension of a random JordanSpinEJA.
-        """
-        return 5
-
     @classmethod
-    def random_instance(cls, **kwargs):
+    def random_instance(cls, max_dimension=None, **kwargs):
         """
         Return a random instance of this type of algebra.
 
         Needed here to override the implementation for ``BilinearFormEJA``.
         """
-        n = ZZ.random_element(cls._max_random_instance_size() + 1)
+        if max_dimension is None:
+            max_dimension = cls._max_random_instance_dimension()
+        n = ZZ.random_element(cls._max_random_instance_size(max_dimension) + 1)
         return cls(n, **kwargs)
 
 
@@ -2673,10 +2844,11 @@ class TrivialEJA(RationalBasisEJA, ConcreteEJA):
         0
 
     """
-    def __init__(self, **kwargs):
+    def __init__(self, field=AA, **kwargs):
         jordan_product = lambda x,y: x
-        inner_product = lambda x,y: 0
+        inner_product = lambda x,y: field.zero()
         basis = ()
+        MS = MatrixSpace(field,0)
 
         # New defaults for keyword arguments
         if "orthonormalize" not in kwargs: kwargs["orthonormalize"] = False
@@ -2686,6 +2858,8 @@ class TrivialEJA(RationalBasisEJA, ConcreteEJA):
                          jordan_product,
                          inner_product,
                          associative=True,
+                         field=field,
+                         matrix_space=MS,
                          **kwargs)
 
         # The rank is zero using my definition, namely the dimension of the
@@ -2863,7 +3037,14 @@ class CartesianProductEJA(FiniteDimensionalEJA):
 
         associative = all( f.is_associative() for f in factors )
 
-        MS = self.matrix_space()
+        # 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):
@@ -2898,15 +3079,16 @@ class CartesianProductEJA(FiniteDimensionalEJA):
                                       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))
-        self.rank.set_cache(sum(J.rank() for J in factors))
 
     def cartesian_factors(self):
         # Copy/pasted from CombinatorialFreeModule_CartesianProduct.
@@ -2981,16 +3163,7 @@ class CartesianProductEJA(FiniteDimensionalEJA):
             +----+
 
         """
-        scalars = self.cartesian_factor(0).base_ring()
-
-        # This category isn't perfect, but is good enough for what we
-        # need to do.
-        cat = MagmaticAlgebras(scalars).FiniteDimensional().WithBasis()
-        cat = cat.Unital().CartesianProducts()
-        factors = tuple( J.matrix_space() for J in self.cartesian_factors() )
-
-        from sage.sets.cartesian_product import CartesianProduct
-        return CartesianProduct(factors, cat)
+        return super().matrix_space()
 
 
     @cached_method
@@ -3242,15 +3415,23 @@ class RationalBasisCartesianProductEJA(CartesianProductEJA,
 
 RationalBasisEJA.CartesianProduct = RationalBasisCartesianProductEJA
 
-def random_eja(*args, **kwargs):
-    J1 = ConcreteEJA.random_instance(*args, **kwargs)
+def random_eja(max_dimension=None, *args, **kwargs):
+    # Use the ConcreteEJA default as the total upper bound (regardless
+    # of any whether or not any individual factors set a lower limit).
+    if max_dimension is None:
+        max_dimension = ConcreteEJA._max_random_instance_dimension()
+    J1 = ConcreteEJA.random_instance(max_dimension, *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:
+
+    # Roll the dice to see if we attempt a Cartesian product.
+    dice_roll = ZZ.random_element(len(ConcreteEJA.__subclasses__()) + 1)
+    new_max_dimension = max_dimension - J1.dimension()
+    if new_max_dimension == 0 or dice_roll != 0:
+        # If it's already as big as we're willing to tolerate, just
+        # return it and don't worry about Cartesian products.
         return J1
+    else:
+        # Use random_eja() again so we can get more than two factors
+        # if the sub-call also Decides on a cartesian product.
+        J2 = random_eja(new_max_dimension, *args, **kwargs)
+        return cartesian_product([J1,J2])