]> gitweb.michael.orlitzky.com - sage.d.git/blobdiff - mjo/eja/eja_algebra.py
eja: don't allow creation of invalid RationalBasisEJAs.
[sage.d.git] / mjo / eja / eja_algebra.py
index 3027075374a177ba5f72bbd690a2c07dad02d136..af4080b0807d6ac6848849f23edd237657896413 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
@@ -78,6 +170,17 @@ from mjo.eja.eja_element import FiniteDimensionalEJAElement
 from mjo.eja.eja_operator import FiniteDimensionalEJAOperator
 from mjo.eja.eja_utils import _all2list, _mat2vec
 
+def EuclideanJordanAlgebras(field):
+    r"""
+    The category of Euclidean Jordan algebras over ``field``, which
+    must be a subfield of the real numbers. For now this is just a
+    convenient wrapper around all of the other category axioms that
+    apply to all EJAs.
+    """
+    category = MagmaticAlgebras(field).FiniteDimensional()
+    category = category.WithBasis().Unital().Commutative()
+    return category
+
 class FiniteDimensionalEJA(CombinatorialFreeModule):
     r"""
     A finite-dimensional Euclidean Jordan algebra.
@@ -104,6 +207,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,18 +236,37 @@ 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
 
+    @staticmethod
+    def _check_input_field(field):
+        if not field.is_subring(RR):
+            # Note: this does return true for the real algebraic
+            # field, the rationals, and any quadratic field where
+            # we've specified a real embedding.
+            raise ValueError("scalar field is not real")
+
+    @staticmethod
+    def _check_input_axioms(basis, jordan_product, inner_product):
+        if not all( jordan_product(bi,bj) == jordan_product(bj,bi)
+                    for bi in basis
+                    for bj in basis ):
+            raise ValueError("Jordan product is not commutative")
+
+        if not all( inner_product(bi,bj) == inner_product(bj,bi)
+                    for bi in basis
+                    for bj in basis ):
+            raise ValueError("inner-product is not commutative")
+
     def __init__(self,
                  basis,
                  jordan_product,
                  inner_product,
                  field=AA,
+                 matrix_space=None,
                  orthonormalize=True,
                  associative=None,
-                 cartesian_product=False,
                  check_field=True,
                  check_axioms=True,
                  prefix="b"):
@@ -147,31 +274,20 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
         n = len(basis)
 
         if check_field:
-            if not field.is_subring(RR):
-                # Note: this does return true for the real algebraic
-                # field, the rationals, and any quadratic field where
-                # we've specified a real embedding.
-                raise ValueError("scalar field is not real")
+            self._check_input_field(field)
 
         if check_axioms:
             # Check commutativity of the Jordan and inner-products.
             # This has to be done before we build the multiplication
             # and inner-product tables/matrices, because we take
             # advantage of symmetry in the process.
-            if not all( jordan_product(bi,bj) == jordan_product(bj,bi)
-                        for bi in basis
-                        for bj in basis ):
-                raise ValueError("Jordan product is not commutative")
-
-            if not all( inner_product(bi,bj) == inner_product(bj,bi)
-                        for bi in basis
-                        for bj in basis ):
-                raise ValueError("inner-product is not commutative")
-
-
-        category = MagmaticAlgebras(field).FiniteDimensional()
-        category = category.WithBasis().Unital().Commutative()
+            self._check_input_axioms(basis, jordan_product, inner_product)
 
+        if n <= 1:
+            # All zero- and one-dimensional algebras are just the real
+            # numbers with (some positive multiples of) the usual
+            # multiplication as its Jordan and inner-product.
+            associative = True
         if associative is None:
             # We should figure it out. As with check_axioms, we have to do
             # this without the help of the _jordan_product_is_associative()
@@ -184,14 +300,11 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
                                for bj in basis
                                for bk in basis)
 
+        category = EuclideanJordanAlgebras(field)
+
         if associative:
             # Element subalgebras can take advantage of this.
             category = category.Associative()
-        if cartesian_product:
-            # Use join() here because otherwise we only get the
-            # "Cartesian product of..." and not the things themselves.
-            category = category.join([category,
-                                      category.CartesianProducts()])
 
         # Call the superclass constructor so that we can use its from_vector()
         # method to build our multiplication table.
@@ -207,7 +320,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
         # as well as a subspace W of V spanned by those (vectorized)
         # basis elements. The W-coordinates are the coefficients that
         # we see in things like x = 1*b1 + 2*b2.
-        vector_basis = basis
 
         degree = 0
         if n > 0:
@@ -217,9 +329,11 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
         # written out as "long vectors."
         V = VectorSpace(field, degree)
 
-        # The matrix that will hole the orthonormal -> unorthonormal
-        # coordinate transformation.
-        self._deortho_matrix = None
+        # The matrix that will hold the orthonormal -> unorthonormal
+        # coordinate transformation. Default to an identity matrix of
+        # the appropriate size to avoid special cases for None
+        # everywhere.
+        self._deortho_matrix = matrix.identity(field,n)
 
         if orthonormalize:
             # Save a copy of the un-orthonormalized basis for later.
@@ -231,30 +345,42 @@ 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
         # supplied basis).
         vector_basis = tuple( V(_all2list(b)) for b in basis )
-        W = V.span_of_basis( vector_basis, check=check_axioms)
+
+        # Save the span of our matrix basis (when written out as long
+        # vectors) because otherwise we'll have to reconstruct it
+        # every time we want to coerce a matrix into the algebra.
+        self._matrix_span = V.span_of_basis( vector_basis, check=check_axioms)
 
         if orthonormalize:
-            # Now "W" is the vector space of our algebra coordinates. The
-            # variables "X1", "X2",...  refer to the entries of vectors in
-            # W. Thus to convert back and forth between the orthonormal
-            # coordinates and the given ones, we need to stick the original
-            # basis in W.
+            # Now "self._matrix_span" is the vector space of our
+            # algebra coordinates. The variables "X1", "X2",...  refer
+            # to the entries of vectors in self._matrix_span. Thus to
+            # convert back and forth between the orthonormal
+            # coordinates and the given ones, we need to stick the
+            # original basis in self._matrix_span.
             U = V.span_of_basis( deortho_vector_basis, check=check_axioms)
-            self._deortho_matrix = matrix( U.coordinate_vector(q)
-                                           for q in vector_basis )
+            self._deortho_matrix = matrix.column( U.coordinate_vector(q)
+                                                  for q in vector_basis )
 
 
         # Now we actually compute the multiplication and inner-product
         # tables/matrices using the possibly-orthonormalized basis.
         self._inner_product_matrix = matrix.identity(field, n)
-        self._multiplication_table = [ [0 for j in range(i+1)]
+        zed = self.zero()
+        self._multiplication_table = [ [zed for j in range(i+1)]
                                        for i in range(n) ]
 
         # Note: the Jordan and inner-products are defined in terms
@@ -269,7 +395,7 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
                 # The jordan product returns a matrixy answer, so we
                 # have to convert it to the algebra coordinates.
                 elt = jordan_product(q_i, q_j)
-                elt = W.coordinate_vector(V(_all2list(elt)))
+                elt = self._matrix_span.coordinate_vector(V(_all2list(elt)))
                 self._multiplication_table[i][j] = self.from_vector(elt)
 
                 if not orthonormalize:
@@ -576,8 +702,8 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
 
     def _element_constructor_(self, elt):
         """
-        Construct an element of this algebra from its vector or matrix
-        representation.
+        Construct an element of this algebra or a subalgebra from its
+        EJA element, vector, or matrix representation.
 
         This gets called only after the parent element _call_ method
         fails to find a coercion for the argument.
@@ -616,6 +742,16 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
             sage: J( (J1.matrix_basis()[1], J2.matrix_basis()[2]) )
             b1 + b5
 
+        Subalgebra elements are embedded into the superalgebra::
+
+            sage: J = JordanSpinEJA(3)
+            sage: J.one()
+            b0
+            sage: x = sum(J.gens())
+            sage: A = x.subalgebra_generated_by()
+            sage: J(A.one())
+            b0
+
         TESTS:
 
         Ensure that we can convert any element back and forth
@@ -640,6 +776,7 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
             Traceback (most recent call last):
             ...
             ValueError: not an element of this algebra
+
         """
         msg = "not an element of this algebra"
         if elt in self.base_ring():
@@ -649,13 +786,16 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
             # that the integer 3 belongs to the space of 2-by-2 matrices.
             raise ValueError(msg)
 
-        try:
-            # Try to convert a vector into a column-matrix...
+        if hasattr(elt, 'superalgebra_element'):
+            # Handle subalgebra elements
+            if elt.parent().superalgebra() == self:
+                return elt.superalgebra_element()
+
+        if hasattr(elt, 'sparse_vector'):
+            # Convert a vector into a column-matrix. We check for
+            # "sparse_vector" and not "column" because matrices also
+            # have a "column" method.
             elt = elt.column()
-        except (AttributeError, TypeError):
-            # and ignore failure, because we weren't really expecting
-            # a vector as an argument anyway.
-            pass
 
         if elt not in self.matrix_space():
             raise ValueError(msg)
@@ -672,15 +812,10 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
         # is that we're already converting everything to long vectors,
         # and that strategy works for tuples as well.
         #
-        # We pass check=False because the matrix basis is "guaranteed"
-        # to be linearly independent... right? Ha ha.
-        elt = _all2list(elt)
-        V = VectorSpace(self.base_ring(), len(elt))
-        W = V.span_of_basis( (V(_all2list(s)) for s in self.matrix_basis()),
-                             check=False)
+        elt = self._matrix_span.ambient_vector_space()(_all2list(elt))
 
         try:
-            coords = W.coordinate_vector(V(elt))
+            coords = self._matrix_span.coordinate_vector(elt)
         except ArithmeticError:  # vector is not in free module
             raise ValueError(msg)
 
@@ -1016,7 +1151,8 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
 
             sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
             sage: J.matrix_space()
-            Full MatrixSpace of 4 by 4 dense matrices over Rational Field
+            Module of 2 by 2 matrices with entries in Algebraic Field over
+            the scalar ring Rational Field
             sage: J = QuaternionHermitianEJA(1,field=QQ,orthonormalize=False)
             sage: J.matrix_space()
             Module of 1 by 1 matrices with entries in Quaternion
@@ -1024,10 +1160,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
@@ -1288,7 +1421,7 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
         # corresponding to trivial spaces (e.g. it returns only the
         # eigenspace corresponding to lambda=1 if you take the
         # decomposition relative to the identity element).
-        trivial = self.subalgebra(())
+        trivial = self.subalgebra((), check_axioms=False)
         J0 = trivial                          # eigenvalue zero
         J5 = VectorSpace(self.base_ring(), 0) # eigenvalue one-half
         J1 = trivial                          # eigenvalue one
@@ -1604,11 +1737,21 @@ class RationalBasisEJA(FiniteDimensionalEJA):
                                        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"""
@@ -1633,25 +1776,15 @@ class RationalBasisEJA(FiniteDimensionalEJA):
             Algebraic Real Field
 
         """
-        if self._rational_algebra is None:
-            # There's no need to construct *another* algebra over the
-            # rationals if this one is already over the
-            # rationals. Likewise, if we never orthonormalized our
-            # basis, we might as well just use the given one.
+        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. The answer will be
         # the same, because all we've done is a change of basis.
         # Then, change back from QQ to our real base ring
         a = ( a_i.change_ring(self.base_ring())
-              for a_i in self._rational_algebra._charpoly_coefficients() )
-
-        if self._deortho_matrix is None:
-            # This can happen if our base ring was, say, AA and we
-            # chose not to (or didn't need to) orthonormalize. It's
-            # still faster to do the computations over QQ even if
-            # the numbers in the boxes stay the same.
-            return tuple(a)
+              for a_i in self.rational_algebra()._charpoly_coefficients() )
 
         # Otherwise, convert the coordinate variables back to the
         # deorthonormalized ones.
@@ -1700,27 +1833,62 @@ 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 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__())
@@ -1728,10 +1896,90 @@ class ConcreteEJA(FiniteDimensionalEJA):
         # These all bubble up to the RationalBasisEJA superclass
         # constructor, so any (kw)args valid there are also valid
         # here.
-        return eja_class.random_instance(*args, **kwargs)
+        return eja_class.random_instance(max_dimension, *args, **kwargs)
 
 
-class MatrixEJA:
+class MatrixEJA(FiniteDimensionalEJA):
+    @staticmethod
+    def _denormalized_basis(A):
+        """
+        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.hurwitz import (ComplexMatrixAlgebra,
+            ....:                          QuaternionMatrixAlgebra,
+            ....:                          OctonionMatrixAlgebra)
+            sage: from mjo.eja.eja_algebra import MatrixEJA
+
+        TESTS::
+
+            sage: set_random_seed()
+            sage: n = ZZ.random_element(1,5)
+            sage: A = MatrixSpace(QQ, n)
+            sage: B = MatrixEJA._denormalized_basis(A)
+            sage: all( M.is_hermitian() for M in  B)
+            True
+
+        ::
+
+            sage: set_random_seed()
+            sage: n = ZZ.random_element(1,5)
+            sage: A = ComplexMatrixAlgebra(n, scalars=QQ)
+            sage: B = MatrixEJA._denormalized_basis(A)
+            sage: all( M.is_hermitian() for M in  B)
+            True
+
+        ::
+
+            sage: set_random_seed()
+            sage: n = ZZ.random_element(1,5)
+            sage: A = QuaternionMatrixAlgebra(n, scalars=QQ)
+            sage: B = MatrixEJA._denormalized_basis(A)
+            sage: all( M.is_hermitian() for M in B )
+            True
+
+        ::
+
+            sage: set_random_seed()
+            sage: n = ZZ.random_element(1,5)
+            sage: A = OctonionMatrixAlgebra(n, scalars=QQ)
+            sage: B = MatrixEJA._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
@@ -1741,11 +1989,74 @@ class MatrixEJA:
         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
+
         """
-        return (X*Y).trace().real()
+        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()
 
 
-class RealSymmetricEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA):
+    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(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
@@ -1779,8 +2090,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
@@ -1810,48 +2121,22 @@ class RealSymmetricEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA):
         Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
 
     """
-    @classmethod
-    def _denormalized_basis(cls, n, field):
-        """
-        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,ZZ)
-            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(field, 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
+    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, *args, **kwargs):
         """
         Return a random instance of this type of algebra.
         """
-        n = ZZ.random_element(cls._max_random_instance_size() + 1)
+        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):
@@ -1859,27 +2144,17 @@ class RealSymmetricEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA):
         # if the user passes check_axioms=True.
         if "check_axioms" not in kwargs: kwargs["check_axioms"] = False
 
-        associative = False
-        if n <= 1:
-            associative = True
+        A = MatrixSpace(field, n)
+        super().__init__(A, **kwargs)
 
-        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 = self.matrix_space().one()
-        self.one.set_cache(self(idV))
+        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(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,
@@ -1892,21 +2167,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
@@ -1936,108 +2226,40 @@ class ComplexHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA):
         Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
 
     """
-
-    @classmethod
-    def _denormalized_basis(cls, n, field):
-        """
-        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,QQ)
-            sage: all( M.is_hermitian() for M in  B)
-            True
-
-        """
-        from mjo.hurwitz import ComplexMatrixAlgebra
-        A = ComplexMatrixAlgebra(n, scalars=field)
-        es = A.entry_algebra_gens()
-
-        basis = []
-        for i in range(n):
-            for j in range(i+1):
-                if i == j:
-                    E_ii = A.monomial( (i,j,es[0]) )
-                    basis.append(E_ii)
-                else:
-                    for e in es:
-                        E_ij  = A.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 A.indices():
-                            E_ij += A.monomial( (j,i,ec) )
-                        else:
-                            E_ij -= A.monomial( (j,i,-ec) )
-                        basis.append(E_ij)
-
-        return tuple( basis )
-
-    @staticmethod
-    def trace_inner_product(X,Y):
-        r"""
-        SETUP::
-
-            sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
-
-        TESTS::
-
-            sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
-            sage: I = J.one().to_matrix()
-            sage: J.trace_inner_product(I, -I)
-            -2
-
-        """
-        return (X*Y).trace().real()
-
     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
+        from mjo.hurwitz import ComplexMatrixAlgebra
+        A = ComplexMatrixAlgebra(n, scalars=field)
+        super().__init__(A, **kwargs)
 
-        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 = self.matrix_space().one()
-        self.one.set_cache(self(idV))
+        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():
-        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, *args, **kwargs):
         """
         Return a random instance of this type of algebra.
         """
-        n = ZZ.random_element(cls._max_random_instance_size() + 1)
+        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(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
@@ -2063,8 +2285,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
@@ -2094,120 +2316,50 @@ class QuaternionHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA):
         Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
 
     """
-    @classmethod
-    def _denormalized_basis(cls, n, field):
-        """
-        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,QQ)
-            sage: all( M.is_hermitian() for M in B )
-            True
-
-        """
-        from mjo.hurwitz import QuaternionMatrixAlgebra
-        A = QuaternionMatrixAlgebra(n, scalars=field)
-        es = A.entry_algebra_gens()
-
-        basis = []
-        for i in range(n):
-            for j in range(i+1):
-                if i == j:
-                    E_ii = A.monomial( (i,j,es[0]) )
-                    basis.append(E_ii)
-                else:
-                    for e in es:
-                        E_ij  = A.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 A.indices():
-                            E_ij += A.monomial( (j,i,ec) )
-                        else:
-                            E_ij -= A.monomial( (j,i,-ec) )
-                        basis.append(E_ij)
-
-        return tuple( basis )
-
-
-    @staticmethod
-    def trace_inner_product(X,Y):
-        r"""
-        Overload the superclass method because the quaternions are weird
-        and we need to use ``coefficient_tuple()`` to get the realpart.
-
-        SETUP::
-
-            sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
-
-        TESTS::
-
-            sage: J = QuaternionHermitianEJA(2,field=QQ,orthonormalize=False)
-            sage: I = J.one().to_matrix()
-            sage: J.trace_inner_product(I, -I)
-            -2
-
-        """
-        return (X*Y).trace().coefficient_tuple()[0]
-
     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
+        from mjo.hurwitz import QuaternionMatrixAlgebra
+        A = QuaternionMatrixAlgebra(n, scalars=field)
+        super().__init__(A, **kwargs)
 
-        super().__init__(self._denormalized_basis(n,field),
-                         self.jordan_product,
-                         self.trace_inner_product,
-                         field=field,
-                         associative=associative,
-                         **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)
 
-        # 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))
 
 
     @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, *args, **kwargs):
         """
         Return a random instance of this type of algebra.
         """
-        n = ZZ.random_element(cls._max_random_instance_size() + 1)
+        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(RationalBasisEJA, ConcreteEJA, MatrixEJA):
+class OctonionHermitianEJA(MatrixEJA, RationalBasisEJA, ConcreteEJA):
     r"""
     SETUP::
 
         sage: from mjo.eja.eja_algebra import (FiniteDimensionalEJA,
         ....:                                  OctonionHermitianEJA)
+        sage: from mjo.hurwitz import Octonions, OctonionMatrixAlgebra
 
     EXAMPLES:
 
@@ -2222,7 +2374,8 @@ class OctonionHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA):
     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: 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
@@ -2288,18 +2441,31 @@ 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, *args, **kwargs):
         """
         Return a random instance of this type of algebra.
         """
-        n = ZZ.random_element(cls._max_random_instance_size() + 1)
+        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):
@@ -2311,82 +2477,14 @@ class OctonionHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA):
         # 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.hurwitz import OctonionMatrixAlgebra
         A = OctonionMatrixAlgebra(n, scalars=field)
-        es = A.entry_algebra_gens()
+        super().__init__(A, **kwargs)
 
-        basis = []
-        for i in range(n):
-            for j in range(i+1):
-                if i == j:
-                    E_ii = A.monomial( (i,j,es[0]) )
-                    basis.append(E_ii)
-                else:
-                    for e in es:
-                        E_ij  = A.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 A.indices():
-                            E_ij += A.monomial( (j,i,ec) )
-                        else:
-                            E_ij -= A.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
-
-        TESTS::
-
-            sage: J = OctonionHermitianEJA(2,field=QQ,orthonormalize=False)
-            sage: I = J.one().to_matrix()
-            sage: J.trace_inner_product(I, -I)
-            -2
-
-        """
-        return (X*Y).trace().coefficient(0)
+        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):
@@ -2451,13 +2549,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]
@@ -2471,34 +2570,43 @@ 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, *args, **kwargs):
         """
         Return a random instance of this type of algebra.
         """
-        n = ZZ.random_element(cls._max_random_instance_size() + 1)
+        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)
 
 
@@ -2598,22 +2706,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
@@ -2624,6 +2732,7 @@ class BilinearFormEJA(RationalBasisEJA, ConcreteEJA):
                          jordan_product,
                          inner_product,
                          field=field,
+                         matrix_space=MS,
                          associative=associative,
                          **kwargs)
 
@@ -2631,25 +2740,37 @@ 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, *args, **kwargs):
         """
         Return a random instance of this algebra.
         """
-        n = ZZ.random_element(cls._max_random_instance_size() + 1)
+        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)
+
         if n.is_zero():
             B = matrix.identity(ZZ, n)
             return cls(B, **kwargs)
@@ -2660,6 +2781,7 @@ class BilinearFormEJA(RationalBasisEJA, ConcreteEJA):
         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
@@ -2732,21 +2854,18 @@ 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, *args, **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)
+        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)
 
 
@@ -2779,10 +2898,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
@@ -2792,6 +2912,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
@@ -2800,9 +2922,12 @@ class TrivialEJA(RationalBasisEJA, ConcreteEJA):
         self.one.set_cache( self.zero() )
 
     @classmethod
-    def random_instance(cls, **kwargs):
+    def random_instance(cls, max_dimension=None, *args, **kwargs):
         # We don't take a "size" argument so the superclass method is
-        # inappropriate for us.
+        # inappropriate for us. The ``max_dimension`` argument is
+        # included so that if this method is called generically with a
+        # ``max_dimension=<whatever>`` argument, we don't try to pass
+        # it on to the algebra constructor.
         return cls(**kwargs)
 
 
@@ -2818,6 +2943,7 @@ class CartesianProductEJA(FiniteDimensionalEJA):
 
         sage: from mjo.eja.eja_algebra import (random_eja,
         ....:                                  CartesianProductEJA,
+        ....:                                  ComplexHermitianEJA,
         ....:                                  HadamardEJA,
         ....:                                  JordanSpinEJA,
         ....:                                  RealSymmetricEJA)
@@ -2929,6 +3055,28 @@ class CartesianProductEJA(FiniteDimensionalEJA):
         | b2 || 0  | 0  | b2 |
         +----++----+----+----+
 
+    The "matrix space" of a Cartesian product always consists of
+    ordered pairs (or triples, or...) whose components are the
+    matrix spaces of its factors::
+
+            sage: J1 = HadamardEJA(2)
+            sage: J2 = ComplexHermitianEJA(2)
+            sage: J = cartesian_product([J1,J2])
+            sage: J.matrix_space()
+            The Cartesian product of (Full MatrixSpace of 2 by 1 dense
+            matrices over Algebraic Real Field, Module of 2 by 2 matrices
+            with entries in Algebraic Field over the scalar ring Algebraic
+            Real Field)
+            sage: J.one().to_matrix()[0]
+            [1]
+            [1]
+            sage: J.one().to_matrix()[1]
+            +---+---+
+            | 1 | 0 |
+            +---+---+
+            | 0 | 1 |
+            +---+---+
+
     TESTS:
 
     All factors must share the same base field::
@@ -2951,11 +3099,7 @@ class CartesianProductEJA(FiniteDimensionalEJA):
         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:
@@ -2967,52 +3111,97 @@ class CartesianProductEJA(FiniteDimensionalEJA):
         if not all( J.base_ring() == field for J in factors ):
             raise ValueError("all factors must share the same base field")
 
+        # Figure out the category to use.
         associative = all( f.is_associative() for f in factors )
+        category = EuclideanJordanAlgebras(field)
+        if associative: category = category.Associative()
+        category = category.join([category, category.CartesianProducts()])
+
+        # Compute my matrix space.  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. 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
+        self._matrix_space = CartesianProduct(MS_factors, MS_cat)
 
-        MS = self.matrix_space()
-        basis = []
-        zero = MS.zero()
+        self._matrix_basis = []
+        zero = self._matrix_space.zero()
         for i in range(m):
             for b in factors[i].matrix_basis():
                 z = list(zero)
                 z[i] = b
-                basis.append(z)
+                self._matrix_basis.append(z)
 
-        basis = tuple( MS(b) for b in basis )
+        self._matrix_basis = tuple( self._matrix_space(b)
+                                    for b in self._matrix_basis )
+        n = len(self._matrix_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)
-            )
+        # We already have what we need for the super-superclass constructor.
+        CombinatorialFreeModule.__init__(self,
+                                         field,
+                                         range(n),
+                                         prefix="b",
+                                         category=category,
+                                         bracket=False)
 
-        # 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,
-                                      orthonormalize=False,
-                                      associative=associative,
-                                      cartesian_product=True,
-                                      check_field=False,
-                                      check_axioms=False)
+        # Now create the vector space for the algebra, which will have
+        # its own set of non-ambient coordinates (in terms of the
+        # supplied basis).
+        degree = sum( f._matrix_span.ambient_vector_space().degree()
+                      for f in factors )
+        V = VectorSpace(field, degree)
+        vector_basis = tuple( V(_all2list(b)) for b in self._matrix_basis )
+
+        # Save the span of our matrix basis (when written out as long
+        # vectors) because otherwise we'll have to reconstruct it
+        # every time we want to coerce a matrix into the algebra.
+        self._matrix_span = V.span_of_basis( vector_basis, check=False)
+
+        # Since we don't (re)orthonormalize the basis, the FDEJA
+        # constructor is going to set self._deortho_matrix to the
+        # identity matrix. Here we set it to the correct value using
+        # the deortho matrices from our factors.
+        self._deortho_matrix = matrix.block_diagonal(
+            [J._deortho_matrix for J in factors]
+        )
+
+        self._inner_product_matrix = matrix.block_diagonal(
+            [J._inner_product_matrix for J in factors]
+        )
+        self._inner_product_matrix._cache = {'hermitian': True}
+        self._inner_product_matrix.set_immutable()
+
+        # Building the multiplication table is a bit more tricky
+        # because we have to embed the entries of the factors'
+        # multiplication tables into the product EJA.
+        zed = self.zero()
+        self._multiplication_table = [ [zed for j in range(i+1)]
+                                       for i in range(n) ]
 
+        # Keep track of an offset that tallies the dimensions of all
+        # previous factors. If the second factor is dim=2 and if the
+        # first one is dim=3, then we want to skip the first 3x3 block
+        # when copying the multiplication table for the second factor.
+        offset = 0
+        for f in range(m):
+            phi_f = self.cartesian_embedding(f)
+            factor_dim = factors[f].dimension()
+            for i in range(factor_dim):
+                for j in range(i+1):
+                    f_ij = factors[f]._multiplication_table[i][j]
+                    e = phi_f(f_ij)
+                    self._multiplication_table[offset+i][offset+j] = e
+            offset += factor_dim
+
+        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.
@@ -3030,72 +3219,6 @@ class CartesianProductEJA(FiniteDimensionalEJA):
         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 0]
-            [0 1]
-            sage: J.one().to_matrix()[1]
-            [1 0]
-            [0 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 |
-            +----+
-
-        """
-        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)
-
 
     @cached_method
     def cartesian_projection(self, i):
@@ -3297,9 +3420,9 @@ class RationalBasisCartesianProductEJA(CartesianProductEJA,
 
     SETUP::
 
-        sage: from mjo.eja.eja_algebra import (HadamardEJA,
+        sage: from mjo.eja.eja_algebra import (FiniteDimensionalEJA,
+        ....:                                  HadamardEJA,
         ....:                                  JordanSpinEJA,
-        ....:                                  OctonionHermitianEJA,
         ....:                                  RealSymmetricEJA)
 
     EXAMPLES:
@@ -3320,41 +3443,74 @@ class RationalBasisCartesianProductEJA(CartesianProductEJA,
 
     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
+    check that all factors do indeed have a ``rational_algebra()`` method
+    before we construct an algebra that claims to have a rational basis::
+
+        sage: J1 = HadamardEJA(2)
+        sage: jp = lambda X,Y: X*Y
+        sage: ip = lambda X,Y: X[0,0]*Y[0,0]
+        sage: b1 = matrix(QQ, [[1]])
+        sage: J2 = FiniteDimensionalEJA((b1,), jp, ip)
+        sage: cartesian_product([J2,J1]) # factor one not RationalBasisEJA
+        Euclidean Jordan algebra of dimension 1 over Algebraic Real
+        Field (+) Euclidean Jordan algebra of dimension 2 over Algebraic
+        Real Field
+        sage: cartesian_product([J1,J2]) # factor one is RationalBasisEJA
+        Traceback (most recent call last):
+        ...
+        ValueError: factor not a RationalBasisEJA
 
     """
     def __init__(self, algebras, **kwargs):
+        if not all( hasattr(r, "rational_algebra") for r in algebras ):
+            raise ValueError("factor not a RationalBasisEJA")
+
         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
-                ])
+    @cached_method
+    def rational_algebra(self):
+        if self.base_ring() is QQ:
+            return self
+
+        return cartesian_product([
+            r.rational_algebra() for r in self.cartesian_factors()
+        ])
 
 
 RationalBasisEJA.CartesianProduct = RationalBasisCartesianProductEJA
 
-def random_eja(*args, **kwargs):
-    J1 = ConcreteEJA.random_instance(*args, **kwargs)
+def random_eja(max_dimension=None, *args, **kwargs):
+    r"""
 
-    # 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:
+    SETUP::
+
+        sage: from mjo.eja.eja_algebra import random_eja
+
+    TESTS::
+
+        sage: set_random_seed()
+        sage: n = ZZ.random_element(1,5)
+        sage: J = random_eja(max_dimension=n, field=QQ, orthonormalize=False)
+        sage: J.dimension() <= n
+        True
+
+    """
+    # 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)
+
+
+    # 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])