]> gitweb.michael.orlitzky.com - sage.d.git/blobdiff - mjo/eja/eja_algebra.py
eja: fix tests and pre-cache ranks.
[sage.d.git] / mjo / eja / eja_algebra.py
index db1494681d054887781efc14acebd2f73653beb0..19db8b0ccefef297623d81721d4a774f99a9d3d1 100644 (file)
@@ -5,34 +5,63 @@ are used in optimization, and have some additional nice methods beyond
 what can be supported in a general Jordan Algebra.
 """
 
+from itertools import repeat
+
 from sage.algebras.quatalg.quaternion_algebra import QuaternionAlgebra
-from sage.categories.finite_dimensional_algebras_with_basis import FiniteDimensionalAlgebrasWithBasis
+from sage.categories.magmatic_algebras import MagmaticAlgebras
 from sage.combinat.free_module import CombinatorialFreeModule
 from sage.matrix.constructor import matrix
+from sage.matrix.matrix_space import MatrixSpace
 from sage.misc.cachefunc import cached_method
+from sage.misc.lazy_import import lazy_import
 from sage.misc.prandom import choice
-from sage.modules.free_module import VectorSpace
-from sage.rings.integer_ring import ZZ
-from sage.rings.number_field.number_field import QuadraticField
-from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
-from sage.rings.rational_field import QQ
-from sage.structure.element import is_Matrix
-
+from sage.misc.table import table
+from sage.modules.free_module import FreeModule, VectorSpace
+from sage.rings.all import (ZZ, QQ, AA, QQbar, RR, RLF, CLF,
+                            PolynomialRing,
+                            QuadraticField)
 from mjo.eja.eja_element import FiniteDimensionalEuclideanJordanAlgebraElement
+lazy_import('mjo.eja.eja_subalgebra',
+            'FiniteDimensionalEuclideanJordanSubalgebra')
 from mjo.eja.eja_utils import _mat2vec
 
 class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
+
+    def _coerce_map_from_base_ring(self):
+        """
+        Disable the map from the base ring into the algebra.
+
+        Performing a nonsense conversion like this automatically
+        is counterpedagogical. The fallback is to try the usual
+        element constructor, which should also fail.
+
+        SETUP::
+
+            sage: from mjo.eja.eja_algebra import random_eja
+
+        TESTS::
+
+            sage: set_random_seed()
+            sage: J = random_eja()
+            sage: J(1)
+            Traceback (most recent call last):
+            ...
+            ValueError: not a naturally-represented algebra element
+
+        """
+        return None
+
     def __init__(self,
                  field,
                  mult_table,
-                 rank,
                  prefix='e',
                  category=None,
-                 natural_basis=None):
+                 natural_basis=None,
+                 check=True):
         """
         SETUP::
 
-            sage: from mjo.eja.eja_algebra import random_eja
+            sage: from mjo.eja.eja_algebra import (JordanSpinEJA, random_eja)
 
         EXAMPLES:
 
@@ -40,17 +69,33 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
 
             sage: set_random_seed()
             sage: J = random_eja()
-            sage: x = J.random_element()
-            sage: y = J.random_element()
+            sage: x,y = J.random_elements(2)
             sage: x*y == y*x
             True
 
+        TESTS:
+
+        The ``field`` we're given must be real::
+
+            sage: JordanSpinEJA(2,QQbar)
+            Traceback (most recent call last):
+            ...
+            ValueError: field is not real
+
         """
-        self._rank = rank
+        if check:
+            if not field.is_subring(RR):
+                # Note: this does return true for the real algebraic
+                # field, and any quadratic field where we've specified
+                # a real embedding.
+                raise ValueError('field is not real')
+
         self._natural_basis = natural_basis
 
         if category is None:
-            category = FiniteDimensionalAlgebrasWithBasis(field).Unital()
+            category = MagmaticAlgebras(field).FiniteDimensional()
+            category = category.WithBasis().Unital()
+
         fda = super(FiniteDimensionalEuclideanJordanAlgebra, self)
         fda.__init__(field,
                      range(len(mult_table)),
@@ -64,8 +109,10 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
         # long run to have the multiplication table be in terms of
         # algebra elements. We do this after calling the superclass
         # constructor so that from_vector() knows what to do.
-        self._multiplication_table = [ map(lambda x: self.from_vector(x), ls)
-                                       for ls in mult_table ]
+        self._multiplication_table = [
+            list(map(lambda x: self.from_vector(x), ls))
+            for ls in mult_table
+        ]
 
 
     def _element_constructor_(self, elt):
@@ -79,7 +126,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
         SETUP::
 
             sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
-            ....:                                  RealCartesianProductEJA,
+            ....:                                  HadamardEJA,
             ....:                                  RealSymmetricEJA)
 
         EXAMPLES:
@@ -107,24 +154,40 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
         vector representations) back and forth faithfully::
 
             sage: set_random_seed()
-            sage: J = RealCartesianProductEJA(5)
+            sage: J = HadamardEJA.random_instance()
             sage: x = J.random_element()
             sage: J(x.to_vector().column()) == x
             True
-            sage: J = JordanSpinEJA(5)
+            sage: J = JordanSpinEJA.random_instance()
             sage: x = J.random_element()
             sage: J(x.to_vector().column()) == x
             True
 
         """
-        natural_basis = self.natural_basis()
-        if elt not in natural_basis[0].matrix_space():
-            raise ValueError("not a naturally-represented algebra element")
+        msg = "not a naturally-represented algebra element"
+        if elt == 0:
+            # The superclass implementation of random_element()
+            # needs to be able to coerce "0" into the algebra.
+            return self.zero()
+        elif elt in self.base_ring():
+            # Ensure that no base ring -> algebra coercion is performed
+            # by this method. There's some stupidity in sage that would
+            # otherwise propagate to this method; for example, sage thinks
+            # that the integer 3 belongs to the space of 2-by-2 matrices.
+            raise ValueError(msg)
 
-        # Thanks for nothing! Matrix spaces aren't vector
-        # spaces in Sage, so we have to figure out its
-        # natural-basis coordinates ourselves.
-        V = VectorSpace(elt.base_ring(), elt.nrows()*elt.ncols())
+        natural_basis = self.natural_basis()
+        basis_space = natural_basis[0].matrix_space()
+        if elt not in basis_space:
+            raise ValueError(msg)
+
+        # Thanks for nothing! Matrix spaces aren't vector spaces in
+        # Sage, so we have to figure out its natural-basis coordinates
+        # ourselves. We use the basis space's ring instead of the
+        # element's ring because the basis space might be an algebraic
+        # closure whereas the base ring of the 3-by-3 identity matrix
+        # could be QQ instead of QQbar.
+        V = VectorSpace(basis_space.base_ring(), elt.nrows()*elt.ncols())
         W = V.span_of_basis( _mat2vec(s) for s in natural_basis )
         coords =  W.coordinate_vector(_mat2vec(elt))
         return self.from_vector(coords)
@@ -142,14 +205,13 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
 
         Ensure that it says what we think it says::
 
-            sage: JordanSpinEJA(2, field=QQ)
-            Euclidean Jordan algebra of degree 2 over Rational Field
+            sage: JordanSpinEJA(2, field=AA)
+            Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
             sage: JordanSpinEJA(3, field=RDF)
-            Euclidean Jordan algebra of degree 3 over Real Double Field
+            Euclidean Jordan algebra of dimension 3 over Real Double Field
 
         """
-        # TODO: change this to say "dimension" and fix all the tests.
-        fmt = "Euclidean Jordan algebra of degree {} over {}"
+        fmt = "Euclidean Jordan algebra of dimension {} over {}"
         return fmt.format(self.dimension(), self.base_ring())
 
     def product_on_basis(self, i, j):
@@ -192,12 +254,18 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
         determinant).
         """
         z = self._a_regular_element()
-        V = self.vector_space()
-        V1 = V.span_of_basis( (z**k).to_vector() for k in range(self.rank()) )
+        # Don't use the parent vector space directly here in case this
+        # happens to be a subalgebra. In that case, we would be e.g.
+        # two-dimensional but span_of_basis() would expect three
+        # coordinates.
+        V = VectorSpace(self.base_ring(), self.vector_space().dimension())
+        basis = [ (z**k).to_vector() for k in range(self.rank()) ]
+        V1 = V.span_of_basis( basis )
         b =  (V1.basis() + V1.complement().basis())
         return V.span_of_basis(b)
 
 
+
     @cached_method
     def _charpoly_coeff(self, i):
         """
@@ -210,7 +278,10 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
         """
         (A_of_x, x, xr, detA) = self._charpoly_matrix_system()
         R = A_of_x.base_ring()
-        if i >= self.rank():
+
+        if i == self.rank():
+            return R.one()
+        if i > self.rank():
             # Guaranteed by theory
             return R.zero()
 
@@ -244,16 +315,40 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
         r = self.rank()
         n = self.dimension()
 
-        # Construct a new algebra over a multivariate polynomial ring...
+        # Turn my vector space into a module so that "vectors" can
+        # have multivatiate polynomial entries.
         names = tuple('X' + str(i) for i in range(1,n+1))
         R = PolynomialRing(self.base_ring(), names)
-        # Hack around the fact that our multiplication table is in terms of
-        # algebra elements but the constructor wants it in terms of vectors.
-        vmt = [ tuple(map(lambda x: x.to_vector(), ls))
-                for ls in self._multiplication_table ]
-        J = FiniteDimensionalEuclideanJordanAlgebra(R, tuple(vmt), r)
 
-        idmat = matrix.identity(J.base_ring(), n)
+        # Using change_ring() on the parent's vector space doesn't work
+        # here because, in a subalgebra, that vector space has a basis
+        # and change_ring() tries to bring the basis along with it. And
+        # that doesn't work unless the new ring is a PID, which it usually
+        # won't be.
+        V = FreeModule(R,n)
+
+        # Now let x = (X1,X2,...,Xn) be the vector whose entries are
+        # indeterminates...
+        x = V(names)
+
+        # And figure out the "left multiplication by x" matrix in
+        # that setting.
+        lmbx_cols = []
+        monomial_matrices = [ self.monomial(i).operator().matrix()
+                              for i in range(n) ] # don't recompute these!
+        for k in range(n):
+            ek = self.monomial(k).to_vector()
+            lmbx_cols.append(
+              sum( x[i]*(monomial_matrices[i]*ek)
+                   for i in range(n) ) )
+        Lx = matrix.column(R, lmbx_cols)
+
+        # Now we can compute powers of x "symbolically"
+        x_powers = [self.one().to_vector(), x]
+        for d in range(2, r+1):
+            x_powers.append( Lx*(x_powers[-1]) )
+
+        idmat = matrix.identity(R, n)
 
         W = self._charpoly_basis_space()
         W = W.change_ring(R.fraction_field())
@@ -273,18 +368,10 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
         # We want the middle equivalent thing in our matrix, but use
         # the first equivalent thing instead so that we can pass in
         # standard coordinates.
-        x = J.from_vector(W(R.gens()))
-
-        # Handle the zeroth power separately, because computing
-        # the unit element in J is mathematically suspect.
-        x0 = W.coordinate_vector(self.one().to_vector())
-        l1  = [ x0.column() ]
-        l1 += [ W.coordinate_vector((x**k).to_vector()).column()
-                for k in range(1,r) ]
-        l2 = [idmat.column(k-1).column() for k in range(r+1, n+1)]
-        A_of_x = matrix.block(R, 1, n, (l1 + l2))
-        xr = W.coordinate_vector((x**r).to_vector())
-        return (A_of_x, x, xr, A_of_x.det())
+        x_powers = [ W.coordinate_vector(xp) for xp in x_powers ]
+        l2 = [idmat.column(k-1) for k in range(r+1, n+1)]
+        A_of_x = matrix.column(R, n, (x_powers[:r] + l2))
+        return (A_of_x, x, x_powers[r], A_of_x.det())
 
 
     @cached_method
@@ -303,7 +390,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
 
         SETUP::
 
-            sage: from mjo.eja.eja_algebra import JordanSpinEJA
+            sage: from mjo.eja.eja_algebra import JordanSpinEJA, TrivialEJA
 
         EXAMPLES:
 
@@ -317,12 +404,22 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
             sage: p(*xvec)
             t^2 - 2*t + 1
 
+        By definition, the characteristic polynomial is a monic
+        degree-zero polynomial in a rank-zero algebra. Note that
+        Cayley-Hamilton is indeed satisfied since the polynomial
+        ``1`` evaluates to the identity element of the algebra on
+        any argument::
+
+            sage: J = TrivialEJA()
+            sage: J.characteristic_polynomial()
+            1
+
         """
         r = self.rank()
         n = self.dimension()
 
-        # The list of coefficient polynomials a_1, a_2, ..., a_n.
-        a = [ self._charpoly_coeff(i) for i in range(n) ]
+        # The list of coefficient polynomials a_0, a_1, a_2, ..., a_n.
+        a = [ self._charpoly_coeff(i) for i in range(r+1) ]
 
         # We go to a bit of trouble here to reorder the
         # indeterminates, so that it's easier to evaluate the
@@ -334,17 +431,6 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
         S = PolynomialRing(S, R.variable_names())
         t = S(t)
 
-        # Note: all entries past the rth should be zero. The
-        # coefficient of the highest power (x^r) is 1, but it doesn't
-        # appear in the solution vector which contains coefficients
-        # for the other powers (to make them sum to x^r).
-        if (r < n):
-            a[r] = 1 # corresponds to x^r
-        else:
-            # When the rank is equal to the dimension, trying to
-            # assign a[r] goes out-of-bounds.
-            a.append(1) # corresponds to x^r
-
         return sum( a[k]*(t**k) for k in range(len(a)) )
 
 
@@ -362,21 +448,80 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
 
         EXAMPLES:
 
-        The inner product must satisfy its axiom for this algebra to truly
-        be a Euclidean Jordan Algebra::
+        Our inner product is "associative," which means the following for
+        a symmetric bilinear form::
 
             sage: set_random_seed()
             sage: J = random_eja()
-            sage: x = J.random_element()
-            sage: y = J.random_element()
-            sage: z = J.random_element()
+            sage: x,y,z = J.random_elements(3)
             sage: (x*y).inner_product(z) == y.inner_product(x*z)
             True
 
         """
-        if (not x in self) or (not y in self):
-            raise TypeError("arguments must live in this algebra")
-        return x.trace_inner_product(y)
+        X = x.natural_representation()
+        Y = y.natural_representation()
+        return self.natural_inner_product(X,Y)
+
+
+    def is_trivial(self):
+        """
+        Return whether or not this algebra is trivial.
+
+        A trivial algebra contains only the zero element.
+
+        SETUP::
+
+            sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
+            ....:                                  TrivialEJA)
+
+        EXAMPLES::
+
+            sage: J = ComplexHermitianEJA(3)
+            sage: J.is_trivial()
+            False
+
+        ::
+
+            sage: J = TrivialEJA()
+            sage: J.is_trivial()
+            True
+
+        """
+        return self.dimension() == 0
+
+
+    def multiplication_table(self):
+        """
+        Return a visual representation of this algebra's multiplication
+        table (on basis elements).
+
+        SETUP::
+
+            sage: from mjo.eja.eja_algebra import JordanSpinEJA
+
+        EXAMPLES::
+
+            sage: J = JordanSpinEJA(4)
+            sage: J.multiplication_table()
+            +----++----+----+----+----+
+            | *  || e0 | e1 | e2 | e3 |
+            +====++====+====+====+====+
+            | e0 || e0 | e1 | e2 | e3 |
+            +----++----+----+----+----+
+            | e1 || e1 | e0 | 0  | 0  |
+            +----++----+----+----+----+
+            | e2 || e2 | 0  | e0 | 0  |
+            +----++----+----+----+----+
+            | e3 || e3 | 0  | 0  | e0 |
+            +----++----+----+----+----+
+
+        """
+        M = list(self._multiplication_table) # copy
+        for i in range(len(M)):
+            # M had better be "square"
+            M[i] = [self.monomial(i)] + M[i]
+        M = [["*"] + list(self.gens())] + M
+        return table(M, header_row=True, header_column=True, frame=True)
 
 
     def natural_basis(self):
@@ -404,8 +549,8 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
             Finite family {0: e0, 1: e1, 2: e2}
             sage: J.natural_basis()
             (
-            [1 0]  [0 1]  [0 0]
-            [0 0], [1 0], [0 1]
+            [1 0]  [                  0 0.7071067811865475?]  [0 0]
+            [0 0], [0.7071067811865475?                   0], [0 1]
             )
 
         ::
@@ -421,11 +566,37 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
 
         """
         if self._natural_basis is None:
-            return tuple( b.to_vector().column() for b in self.basis() )
+            M = self.natural_basis_space()
+            return tuple( M(b.to_vector()) for b in self.basis() )
         else:
             return self._natural_basis
 
 
+    def natural_basis_space(self):
+        """
+        Return the matrix space in which this algebra's natural basis
+        elements live.
+        """
+        if self._natural_basis is None or len(self._natural_basis) == 0:
+            return MatrixSpace(self.base_ring(), self.dimension(), 1)
+        else:
+            return self._natural_basis[0].matrix_space()
+
+
+    @staticmethod
+    def natural_inner_product(X,Y):
+        """
+        Compute the inner product of two naturally-represented elements.
+
+        For example in the real symmetric matrix EJA, this will compute
+        the trace inner-product of two n-by-n symmetric matrices. The
+        default should work for the real cartesian product EJA, the
+        Jordan spin EJA, and the real symmetric matrices. The others
+        will have to be overridden.
+        """
+        return (X.conjugate_transpose()*Y).trace()
+
+
     @cached_method
     def one(self):
         """
@@ -433,16 +604,16 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
 
         SETUP::
 
-            sage: from mjo.eja.eja_algebra import (RealCartesianProductEJA,
+            sage: from mjo.eja.eja_algebra import (HadamardEJA,
             ....:                                  random_eja)
 
         EXAMPLES::
 
-            sage: J = RealCartesianProductEJA(5)
+            sage: J = HadamardEJA(5)
             sage: J.one()
             e0 + e1 + e2 + e3 + e4
 
-        TESTS::
+        TESTS:
 
         The identity element acts like the identity::
 
@@ -487,20 +658,157 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
         return self.linear_combination(zip(self.gens(), coeffs))
 
 
+    def peirce_decomposition(self, c):
+        """
+        The Peirce decomposition of this algebra relative to the
+        idempotent ``c``.
+
+        In the future, this can be extended to a complete system of
+        orthogonal idempotents.
+
+        INPUT:
+
+          - ``c`` -- an idempotent of this algebra.
+
+        OUTPUT:
+
+        A triple (J0, J5, J1) containing two subalgebras and one subspace
+        of this algebra,
+
+          - ``J0`` -- the algebra on the eigenspace of ``c.operator()``
+            corresponding to the eigenvalue zero.
+
+          - ``J5`` -- the eigenspace (NOT a subalgebra) of ``c.operator()``
+            corresponding to the eigenvalue one-half.
+
+          - ``J1`` -- the algebra on the eigenspace of ``c.operator()``
+            corresponding to the eigenvalue one.
+
+        These are the only possible eigenspaces for that operator, and this
+        algebra is a direct sum of them. The spaces ``J0`` and ``J1`` are
+        orthogonal, and are subalgebras of this algebra with the appropriate
+        restrictions.
+
+        SETUP::
+
+            sage: from mjo.eja.eja_algebra import random_eja, RealSymmetricEJA
+
+        EXAMPLES:
+
+        The canonical example comes from the symmetric matrices, which
+        decompose into diagonal and off-diagonal parts::
+
+            sage: J = RealSymmetricEJA(3)
+            sage: C = matrix(QQ, [ [1,0,0],
+            ....:                  [0,1,0],
+            ....:                  [0,0,0] ])
+            sage: c = J(C)
+            sage: J0,J5,J1 = J.peirce_decomposition(c)
+            sage: J0
+            Euclidean Jordan algebra of dimension 1...
+            sage: J5
+            Vector space of degree 6 and dimension 2...
+            sage: J1
+            Euclidean Jordan algebra of dimension 3...
+
+        TESTS:
+
+        Every algebra decomposes trivially with respect to its identity
+        element::
+
+            sage: set_random_seed()
+            sage: J = random_eja()
+            sage: J0,J5,J1 = J.peirce_decomposition(J.one())
+            sage: J0.dimension() == 0 and J5.dimension() == 0
+            True
+            sage: J1.superalgebra() == J and J1.dimension() == J.dimension()
+            True
+
+        The identity elements in the two subalgebras are the
+        projections onto their respective subspaces of the
+        superalgebra's identity element::
+
+            sage: set_random_seed()
+            sage: J = random_eja()
+            sage: x = J.random_element()
+            sage: if not J.is_trivial():
+            ....:     while x.is_nilpotent():
+            ....:         x = J.random_element()
+            sage: c = x.subalgebra_idempotent()
+            sage: J0,J5,J1 = J.peirce_decomposition(c)
+            sage: J1(c) == J1.one()
+            True
+            sage: J0(J.one() - c) == J0.one()
+            True
+
+        """
+        if not c.is_idempotent():
+            raise ValueError("element is not idempotent: %s" % c)
+
+        # Default these to what they should be if they turn out to be
+        # trivial, because eigenspaces_left() won't return eigenvalues
+        # 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 = FiniteDimensionalEuclideanJordanSubalgebra(self, ())
+        J0 = trivial                          # eigenvalue zero
+        J5 = VectorSpace(self.base_ring(), 0) # eigenvalue one-half
+        J1 = trivial                          # eigenvalue one
+
+        for (eigval, eigspace) in c.operator().matrix().right_eigenspaces():
+            if eigval == ~(self.base_ring()(2)):
+                J5 = eigspace
+            else:
+                gens = tuple( self.from_vector(b) for b in eigspace.basis() )
+                subalg = FiniteDimensionalEuclideanJordanSubalgebra(self, gens)
+                if eigval == 0:
+                    J0 = subalg
+                elif eigval == 1:
+                    J1 = subalg
+                else:
+                    raise ValueError("unexpected eigenvalue: %s" % eigval)
+
+        return (J0, J5, J1)
+
+
+    def random_elements(self, count):
+        """
+        Return ``count`` random elements as a tuple.
+
+        SETUP::
+
+            sage: from mjo.eja.eja_algebra import JordanSpinEJA
+
+        EXAMPLES::
+
+            sage: J = JordanSpinEJA(3)
+            sage: x,y,z = J.random_elements(3)
+            sage: all( [ x in J, y in J, z in J ])
+            True
+            sage: len( J.random_elements(10) ) == 10
+            True
+
+        """
+        return tuple( self.random_element() for idx in range(count) )
+
+    @cached_method
     def rank(self):
         """
         Return the rank of this EJA.
 
         ALGORITHM:
 
-        The author knows of no algorithm to compute the rank of an EJA
-        where only the multiplication table is known. In lieu of one, we
-        require the rank to be specified when the algebra is created,
-        and simply pass along that number here.
+        We first compute the polynomial "column matrices" `p_{k}` that
+        evaluate to `x^k` on the coordinates of `x`. Then, we begin
+        adding them to a matrix one at a time, and trying to solve the
+        system that makes `p_{0}`,`p_{1}`,..., `p_{s-1}` add up to
+        `p_{s}`. This will succeed only when `s` is the rank of the
+        algebra, as proven in a recent draft paper of mine.
 
         SETUP::
 
-            sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
+            sage: from mjo.eja.eja_algebra import (HadamardEJA,
+            ....:                                  JordanSpinEJA,
             ....:                                  RealSymmetricEJA,
             ....:                                  ComplexHermitianEJA,
             ....:                                  QuaternionHermitianEJA,
@@ -520,31 +828,101 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
         The rank of the `n`-by-`n` Hermitian real, complex, or
         quaternion matrices is `n`::
 
-            sage: RealSymmetricEJA(2).rank()
-            2
-            sage: ComplexHermitianEJA(2).rank()
-            2
+            sage: RealSymmetricEJA(4).rank()
+            4
+            sage: ComplexHermitianEJA(3).rank()
+            3
             sage: QuaternionHermitianEJA(2).rank()
             2
-            sage: RealSymmetricEJA(5).rank()
-            5
-            sage: ComplexHermitianEJA(5).rank()
-            5
-            sage: QuaternionHermitianEJA(5).rank()
-            5
 
         TESTS:
 
         Ensure that every EJA that we know how to construct has a
-        positive integer rank::
+        positive integer rank, unless the algebra is trivial in
+        which case its rank will be zero::
 
             sage: set_random_seed()
-            sage: r = random_eja().rank()
-            sage: r in ZZ and r > 0
+            sage: J = random_eja()
+            sage: r = J.rank()
+            sage: r in ZZ
             True
+            sage: r > 0 or (r == 0 and J.is_trivial())
+            True
+
+        Ensure that computing the rank actually works, since the ranks
+        of all simple algebras are known and will be cached by default::
+
+            sage: J = HadamardEJA(4)
+            sage: J.rank.clear_cache()
+            sage: J.rank()
+            4
+
+        ::
+
+            sage: J = JordanSpinEJA(4)
+            sage: J.rank.clear_cache()
+            sage: J.rank()
+            2
+
+        ::
+
+            sage: J = RealSymmetricEJA(3)
+            sage: J.rank.clear_cache()
+            sage: J.rank()
+            3
+
+        ::
+
+            sage: J = ComplexHermitianEJA(2)
+            sage: J.rank.clear_cache()
+            sage: J.rank()
+            2
+
+        ::
+
+            sage: J = QuaternionHermitianEJA(2)
+            sage: J.rank.clear_cache()
+            sage: J.rank()
+            2
 
         """
-        return self._rank
+        n = self.dimension()
+        if n == 0:
+            return 0
+        elif n == 1:
+            return 1
+
+        var_names = [ "X" + str(z) for z in range(1,n+1) ]
+        R = PolynomialRing(self.base_ring(), var_names)
+        vars = R.gens()
+
+        def L_x_i_j(i,j):
+            # From a result in my book, these are the entries of the
+            # basis representation of L_x.
+            return sum( vars[k]*self.monomial(k).operator().matrix()[i,j]
+                        for k in range(n) )
+
+        L_x = matrix(R, n, n, L_x_i_j)
+        x_powers = [ vars[k]*(L_x**k)*self.one().to_vector()
+                     for k in range(n) ]
+
+        # Can assume n >= 2
+        M = matrix([x_powers[0]])
+        old_rank = 1
+
+        for d in range(1,n):
+            M = matrix(M.rows() + [x_powers[d]])
+            M.echelonize()
+            # TODO: we've basically solved the system here.
+            # We should save the echelonized matrix somehow
+            # so that it can be reused in the charpoly method.
+            new_rank = M.rank()
+            if new_rank == old_rank:
+                return new_rank
+            else:
+                old_rank = new_rank
+
+        return n
 
 
     def vector_space(self):
@@ -559,7 +937,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
 
             sage: J = RealSymmetricEJA(2)
             sage: J.vector_space()
-            Vector space of dimension 3 over Rational Field
+            Vector space of dimension 3 over...
 
         """
         return self.zero().to_vector().parent().ambient_vector_space()
@@ -568,7 +946,61 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
     Element = FiniteDimensionalEuclideanJordanAlgebraElement
 
 
-class RealCartesianProductEJA(FiniteDimensionalEuclideanJordanAlgebra):
+class KnownRankEJA(object):
+    """
+    A class for algebras that we actually know we can construct.  The
+    main issue is that, for most of our methods to make sense, we need
+    to know the rank of our algebra. Thus we can't simply generate a
+    "random" algebra, or even check that a given basis and product
+    satisfy the axioms; because even if everything looks OK, we wouldn't
+    know the rank we need to actuallty build the thing.
+
+    Not really a subclass of FDEJA because doing that causes method
+    resolution errors, e.g.
+
+      TypeError: Error when calling the metaclass bases
+      Cannot create a consistent method resolution
+      order (MRO) for bases FiniteDimensionalEuclideanJordanAlgebra,
+      KnownRankEJA
+
+    """
+    @staticmethod
+    def _max_test_case_size():
+        """
+        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 quite vague -- 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.
+
+        We default to five in this class, which is safe in `R^n`. The
+        matrix algebra subclasses (or any class where the "size" is
+        interpreted to be far less than the dimension) should override
+        with a smaller number.
+        """
+        return 5
+
+    @classmethod
+    def random_instance(cls, field=AA, **kwargs):
+        """
+        Return a random instance of this type of algebra.
+
+        Beware, this will crash for "most instances" because the
+        constructor below looks wrong.
+        """
+        if cls is TrivialEJA:
+            # The TrivialEJA class doesn't take an "n" argument because
+            # there's only one.
+            return cls(field)
+
+        n = ZZ.random_element(cls._max_test_case_size()) + 1
+        return cls(n, field, **kwargs)
+
+
+class HadamardEJA(FiniteDimensionalEuclideanJordanAlgebra, KnownRankEJA):
     """
     Return the Euclidean Jordan Algebra corresponding to the set
     `R^n` under the Hadamard product.
@@ -579,13 +1011,13 @@ class RealCartesianProductEJA(FiniteDimensionalEuclideanJordanAlgebra):
 
     SETUP::
 
-        sage: from mjo.eja.eja_algebra import RealCartesianProductEJA
+        sage: from mjo.eja.eja_algebra import HadamardEJA
 
     EXAMPLES:
 
     This multiplication table can be verified by hand::
 
-        sage: J = RealCartesianProductEJA(3)
+        sage: J = HadamardEJA(3)
         sage: e0,e1,e2 = J.gens()
         sage: e0*e0
         e0
@@ -600,44 +1032,51 @@ class RealCartesianProductEJA(FiniteDimensionalEuclideanJordanAlgebra):
         sage: e2*e2
         e2
 
+    TESTS:
+
+    We can change the generator prefix::
+
+        sage: HadamardEJA(3, prefix='r').gens()
+        (r0, r1, r2)
+
     """
-    def __init__(self, n, field=QQ):
+    def __init__(self, n, field=AA, **kwargs):
         V = VectorSpace(field, n)
-        mult_table = [ [ V.basis()[i]*(i == j) for i in range(n) ]
-                       for j in range(n) ]
+        mult_table = [ [ V.gen(i)*(i == j) for j in range(n) ]
+                       for i in range(n) ]
 
-        fdeja = super(RealCartesianProductEJA, self)
-        return fdeja.__init__(field, mult_table, rank=n)
+        fdeja = super(HadamardEJA, self)
+        fdeja.__init__(field, mult_table, **kwargs)
+        self.rank.set_cache(n)
 
     def inner_product(self, x, y):
-        return _usual_ip(x,y)
-
-
-def random_eja():
-    """
-    Return a "random" finite-dimensional Euclidean Jordan Algebra.
+        """
+        Faster to reimplement than to use natural representations.
 
-    ALGORITHM:
+        SETUP::
 
-    For now, we choose a random natural number ``n`` (greater than zero)
-    and then give you back one of the following:
+            sage: from mjo.eja.eja_algebra import HadamardEJA
 
-      * The cartesian product of the rational numbers ``n`` times; this is
-        ``QQ^n`` with the Hadamard product.
+        TESTS:
 
-      * The Jordan spin algebra on ``QQ^n``.
+        Ensure that this is the usual inner product for the algebras
+        over `R^n`::
 
-      * The ``n``-by-``n`` rational symmetric matrices with the symmetric
-        product.
+            sage: set_random_seed()
+            sage: J = HadamardEJA.random_instance()
+            sage: x,y = J.random_elements(2)
+            sage: X = x.natural_representation()
+            sage: Y = y.natural_representation()
+            sage: x.inner_product(y) == J.natural_inner_product(X,Y)
+            True
 
-      * The ``n``-by-``n`` complex-rational Hermitian matrices embedded
-        in the space of ``2n``-by-``2n`` real symmetric matrices.
+        """
+        return x.to_vector().inner_product(y.to_vector())
 
-      * The ``n``-by-``n`` quaternion-rational Hermitian matrices embedded
-        in the space of ``4n``-by-``4n`` real symmetric matrices.
 
-    Later this might be extended to return Cartesian products of the
-    EJAs above.
+def random_eja(field=AA, nontrivial=False):
+    """
+    Return a "random" finite-dimensional Euclidean Jordan Algebra.
 
     SETUP::
 
@@ -646,462 +1085,492 @@ def random_eja():
     TESTS::
 
         sage: random_eja()
-        Euclidean Jordan algebra of degree...
+        Euclidean Jordan algebra of dimension...
 
     """
+    eja_classes = KnownRankEJA.__subclasses__()
+    if nontrivial:
+        eja_classes.remove(TrivialEJA)
+    classname = choice(eja_classes)
+    return classname.random_instance(field=field)
 
-    # The max_n component lets us choose different upper bounds on the
-    # value "n" that gets passed to the constructor. This is needed
-    # because e.g. R^{10} is reasonable to test, while the Hermitian
-    # 10-by-10 quaternion matrices are not.
-    (constructor, max_n) = choice([(RealCartesianProductEJA, 6),
-                                   (JordanSpinEJA, 6),
-                                   (RealSymmetricEJA, 5),
-                                   (ComplexHermitianEJA, 4),
-                                   (QuaternionHermitianEJA, 3)])
-    n = ZZ.random_element(1, max_n)
-    return constructor(n, field=QQ)
 
 
 
-def _real_symmetric_basis(n, field=QQ):
-    """
-    Return a basis for the space of real symmetric n-by-n matrices.
-    """
-    # The basis of symmetric matrices, as matrices, in their R^(n-by-n)
-    # coordinates.
-    S = []
-    for i in xrange(n):
-        for j in xrange(i+1):
-            Eij = matrix(field, n, lambda k,l: k==i and l==j)
-            if i == j:
-                Sij = Eij
-            else:
-                # Beware, orthogonal but not normalized!
-                Sij = Eij + Eij.transpose()
-            S.append(Sij)
-    return tuple(S)
 
 
-def _complex_hermitian_basis(n, field=QQ):
-    """
-    Returns a basis for the space of complex Hermitian n-by-n matrices.
+class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
+    @staticmethod
+    def _max_test_case_size():
+        # Play it safe, since this will be squared and the underlying
+        # field can have dimension 4 (quaternions) too.
+        return 2
 
-    SETUP::
+    def __init__(self, field, basis, normalize_basis=True, **kwargs):
+        """
+        Compared to the superclass constructor, we take a basis instead of
+        a multiplication table because the latter can be computed in terms
+        of the former when the product is known (like it is here).
+        """
+        # Used in this class's fast _charpoly_coeff() override.
+        self._basis_normalizers = None
+
+        # We're going to loop through this a few times, so now's a good
+        # time to ensure that it isn't a generator expression.
+        basis = tuple(basis)
+
+        if len(basis) > 1 and normalize_basis:
+            # We'll need sqrt(2) to normalize the basis, and this
+            # winds up in the multiplication table, so the whole
+            # algebra needs to be over the field extension.
+            R = PolynomialRing(field, 'z')
+            z = R.gen()
+            p = z**2 - 2
+            if p.is_irreducible():
+                field = field.extension(p, 'sqrt2', embedding=RLF(2).sqrt())
+                basis = tuple( s.change_ring(field) for s in basis )
+            self._basis_normalizers = tuple(
+                ~(self.natural_inner_product(s,s).sqrt()) for s in basis )
+            basis = tuple(s*c for (s,c) in zip(basis,self._basis_normalizers))
+
+        Qs = self.multiplication_table_from_matrix_basis(basis)
+
+        fdeja = super(MatrixEuclideanJordanAlgebra, self)
+        fdeja.__init__(field, Qs, natural_basis=basis, **kwargs)
+        return
 
-        sage: from mjo.eja.eja_algebra import _complex_hermitian_basis
 
-    TESTS::
+    @cached_method
+    def rank(self):
+        r"""
+        Override the parent method with something that tries to compute
+        over a faster (non-extension) field.
+        """
+        if self._basis_normalizers is None:
+            # We didn't normalize, so assume that the basis we started
+            # with had entries in a nice field.
+            return super(MatrixEuclideanJordanAlgebra, self).rank()
+        else:
+            basis = ( (b/n) for (b,n) in zip(self.natural_basis(),
+                                             self._basis_normalizers) )
 
-        sage: set_random_seed()
-        sage: n = ZZ.random_element(1,5)
-        sage: all( M.is_symmetric() for M in _complex_hermitian_basis(n) )
-        True
+            # Do this over the rationals and convert back at the end.
+            # Only works because we know the entries of the basis are
+            # integers.
+            J = MatrixEuclideanJordanAlgebra(QQ,
+                                             basis,
+                                             normalize_basis=False)
+            return J.rank()
 
-    """
-    F = QuadraticField(-1, 'I')
-    I = F.gen()
-
-    # This is like the symmetric case, but we need to be careful:
-    #
-    #   * We want conjugate-symmetry, not just symmetry.
-    #   * The diagonal will (as a result) be real.
-    #
-    S = []
-    for i in xrange(n):
-        for j in xrange(i+1):
-            Eij = matrix(field, n, lambda k,l: k==i and l==j)
-            if i == j:
-                Sij = _embed_complex_matrix(Eij)
-                S.append(Sij)
+    @cached_method
+    def _charpoly_coeff(self, i):
+        """
+        Override the parent method with something that tries to compute
+        over a faster (non-extension) field.
+        """
+        if self._basis_normalizers is None:
+            # We didn't normalize, so assume that the basis we started
+            # with had entries in a nice field.
+            return super(MatrixEuclideanJordanAlgebra, self)._charpoly_coeff(i)
+        else:
+            basis = ( (b/n) for (b,n) in zip(self.natural_basis(),
+                                             self._basis_normalizers) )
+
+            # Do this over the rationals and convert back at the end.
+            J = MatrixEuclideanJordanAlgebra(QQ,
+                                             basis,
+                                             normalize_basis=False)
+            (_,x,_,_) = J._charpoly_matrix_system()
+            p = J._charpoly_coeff(i)
+            # p might be missing some vars, have to substitute "optionally"
+            pairs = zip(x.base_ring().gens(), self._basis_normalizers)
+            substitutions = { v: v*c for (v,c) in pairs }
+            result = p.subs(substitutions)
+
+            # The result of "subs" can be either a coefficient-ring
+            # element or a polynomial. Gotta handle both cases.
+            if result in QQ:
+                return self.base_ring()(result)
             else:
-                # Beware, orthogonal but not normalized! The second one
-                # has a minus because it's conjugated.
-                Sij_real = _embed_complex_matrix(Eij + Eij.transpose())
-                S.append(Sij_real)
-                Sij_imag = _embed_complex_matrix(I*Eij - I*Eij.transpose())
-                S.append(Sij_imag)
-    return tuple(S)
+                return result.change_ring(self.base_ring())
 
 
-def _quaternion_hermitian_basis(n, field=QQ):
-    """
-    Returns a basis for the space of quaternion Hermitian n-by-n matrices.
+    @staticmethod
+    def multiplication_table_from_matrix_basis(basis):
+        """
+        At least three of the five simple Euclidean Jordan algebras have the
+        symmetric multiplication (A,B) |-> (AB + BA)/2, where the
+        multiplication on the right is matrix multiplication. Given a basis
+        for the underlying matrix space, this function returns a
+        multiplication table (obtained by looping through the basis
+        elements) for an algebra of those matrices.
+        """
+        # In S^2, for example, we nominally have four coordinates even
+        # though the space is of dimension three only. The vector space V
+        # is supposed to hold the entire long vector, and the subspace W
+        # of V will be spanned by the vectors that arise from symmetric
+        # matrices. Thus for S^2, dim(V) == 4 and dim(W) == 3.
+        field = basis[0].base_ring()
+        dimension = basis[0].nrows()
+
+        V = VectorSpace(field, dimension**2)
+        W = V.span_of_basis( _mat2vec(s) for s in basis )
+        n = len(basis)
+        mult_table = [[W.zero() for j in range(n)] for i in range(n)]
+        for i in range(n):
+            for j in range(n):
+                mat_entry = (basis[i]*basis[j] + basis[j]*basis[i])/2
+                mult_table[i][j] = W.coordinate_vector(_mat2vec(mat_entry))
 
-    SETUP::
+        return mult_table
 
-        sage: from mjo.eja.eja_algebra import _quaternion_hermitian_basis
 
-    TESTS::
+    @staticmethod
+    def real_embed(M):
+        """
+        Embed the matrix ``M`` into a space of real matrices.
 
-        sage: set_random_seed()
-        sage: n = ZZ.random_element(1,5)
-        sage: all( M.is_symmetric() for M in _quaternion_hermitian_basis(n) )
-        True
+        The matrix ``M`` can have entries in any field at the moment:
+        the real numbers, complex numbers, or quaternions. And although
+        they are not a field, we can probably support octonions at some
+        point, too. This function returns a real matrix that "acts like"
+        the original with respect to matrix multiplication; i.e.
 
-    """
-    Q = QuaternionAlgebra(QQ,-1,-1)
-    I,J,K = Q.gens()
-
-    # This is like the symmetric case, but we need to be careful:
-    #
-    #   * We want conjugate-symmetry, not just symmetry.
-    #   * The diagonal will (as a result) be real.
-    #
-    S = []
-    for i in xrange(n):
-        for j in xrange(i+1):
-            Eij = matrix(Q, n, lambda k,l: k==i and l==j)
-            if i == j:
-                Sij = _embed_quaternion_matrix(Eij)
-                S.append(Sij)
-            else:
-                # Beware, orthogonal but not normalized! The second,
-                # third, and fourth ones have a minus because they're
-                # conjugated.
-                Sij_real = _embed_quaternion_matrix(Eij + Eij.transpose())
-                S.append(Sij_real)
-                Sij_I = _embed_quaternion_matrix(I*Eij - I*Eij.transpose())
-                S.append(Sij_I)
-                Sij_J = _embed_quaternion_matrix(J*Eij - J*Eij.transpose())
-                S.append(Sij_J)
-                Sij_K = _embed_quaternion_matrix(K*Eij - K*Eij.transpose())
-                S.append(Sij_K)
-    return tuple(S)
-
-
-
-def _multiplication_table_from_matrix_basis(basis):
-    """
-    At least three of the five simple Euclidean Jordan algebras have the
-    symmetric multiplication (A,B) |-> (AB + BA)/2, where the
-    multiplication on the right is matrix multiplication. Given a basis
-    for the underlying matrix space, this function returns a
-    multiplication table (obtained by looping through the basis
-    elements) for an algebra of those matrices.
-    """
-    # In S^2, for example, we nominally have four coordinates even
-    # though the space is of dimension three only. The vector space V
-    # is supposed to hold the entire long vector, and the subspace W
-    # of V will be spanned by the vectors that arise from symmetric
-    # matrices. Thus for S^2, dim(V) == 4 and dim(W) == 3.
-    field = basis[0].base_ring()
-    dimension = basis[0].nrows()
-
-    V = VectorSpace(field, dimension**2)
-    W = V.span_of_basis( _mat2vec(s) for s in basis )
-    n = len(basis)
-    mult_table = [[W.zero() for i in range(n)] for j in range(n)]
-    for i in range(n):
-        for j in range(n):
-            mat_entry = (basis[i]*basis[j] + basis[j]*basis[i])/2
-            mult_table[i][j] = W.coordinate_vector(_mat2vec(mat_entry))
-
-    return mult_table
-
-
-def _embed_complex_matrix(M):
-    """
-    Embed the n-by-n complex matrix ``M`` into the space of real
-    matrices of size 2n-by-2n via the map the sends each entry `z = a +
-    bi` to the block matrix ``[[a,b],[-b,a]]``.
+          real_embed(M*N) = real_embed(M)*real_embed(N)
 
-    SETUP::
+        """
+        raise NotImplementedError
 
-        sage: from mjo.eja.eja_algebra import _embed_complex_matrix
 
-    EXAMPLES::
+    @staticmethod
+    def real_unembed(M):
+        """
+        The inverse of :meth:`real_embed`.
+        """
+        raise NotImplementedError
 
-        sage: F = QuadraticField(-1,'i')
-        sage: x1 = F(4 - 2*i)
-        sage: x2 = F(1 + 2*i)
-        sage: x3 = F(-i)
-        sage: x4 = F(6)
-        sage: M = matrix(F,2,[[x1,x2],[x3,x4]])
-        sage: _embed_complex_matrix(M)
-        [ 4 -2| 1  2]
-        [ 2  4|-2  1]
-        [-----+-----]
-        [ 0 -1| 6  0]
-        [ 1  0| 0  6]
 
-    TESTS:
+    @classmethod
+    def natural_inner_product(cls,X,Y):
+        Xu = cls.real_unembed(X)
+        Yu = cls.real_unembed(Y)
+        tr = (Xu*Yu).trace()
 
-    Embedding is a homomorphism (isomorphism, in fact)::
+        if tr in RLF:
+            # It's real already.
+            return tr
 
-        sage: set_random_seed()
-        sage: n = ZZ.random_element(5)
-        sage: F = QuadraticField(-1, 'i')
-        sage: X = random_matrix(F, n)
-        sage: Y = random_matrix(F, n)
-        sage: actual = _embed_complex_matrix(X) * _embed_complex_matrix(Y)
-        sage: expected = _embed_complex_matrix(X*Y)
-        sage: actual == expected
-        True
+        # Otherwise, try the thing that works for complex numbers; and
+        # if that doesn't work, the thing that works for quaternions.
+        try:
+            return tr.vector()[0] # real part, imag part is index 1
+        except AttributeError:
+            # A quaternions doesn't have a vector() method, but does
+            # have coefficient_tuple() method that returns the
+            # coefficients of 1, i, j, and k -- in that order.
+            return tr.coefficient_tuple()[0]
 
-    """
-    n = M.nrows()
-    if M.ncols() != n:
-        raise ValueError("the matrix 'M' must be square")
-    field = M.base_ring()
-    blocks = []
-    for z in M.list():
-        a = z.real()
-        b = z.imag()
-        blocks.append(matrix(field, 2, [[a,b],[-b,a]]))
 
-    # We can drop the imaginaries here.
-    return matrix.block(field.base_ring(), n, blocks)
+class RealMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
+    @staticmethod
+    def real_embed(M):
+        """
+        The identity function, for embedding real matrices into real
+        matrices.
+        """
+        return M
+
+    @staticmethod
+    def real_unembed(M):
+        """
+        The identity function, for unembedding real matrices from real
+        matrices.
+        """
+        return M
 
 
-def _unembed_complex_matrix(M):
+class RealSymmetricEJA(RealMatrixEuclideanJordanAlgebra, KnownRankEJA):
     """
-    The inverse of _embed_complex_matrix().
+    The rank-n simple EJA consisting of real symmetric n-by-n
+    matrices, the usual symmetric Jordan product, and the trace inner
+    product. It has dimension `(n^2 + n)/2` over the reals.
 
     SETUP::
 
-        sage: from mjo.eja.eja_algebra import (_embed_complex_matrix,
-        ....:                                  _unembed_complex_matrix)
+        sage: from mjo.eja.eja_algebra import RealSymmetricEJA
 
     EXAMPLES::
 
-        sage: A = matrix(QQ,[ [ 1,  2,   3,  4],
-        ....:                 [-2,  1,  -4,  3],
-        ....:                 [ 9,  10, 11, 12],
-        ....:                 [-10, 9, -12, 11] ])
-        sage: _unembed_complex_matrix(A)
-        [  2*i + 1   4*i + 3]
-        [ 10*i + 9 12*i + 11]
+        sage: J = RealSymmetricEJA(2)
+        sage: e0, e1, e2 = J.gens()
+        sage: e0*e0
+        e0
+        sage: e1*e1
+        1/2*e0 + 1/2*e2
+        sage: e2*e2
+        e2
+
+    In theory, our "field" can be any subfield of the reals::
+
+        sage: RealSymmetricEJA(2, RDF)
+        Euclidean Jordan algebra of dimension 3 over Real Double Field
+        sage: RealSymmetricEJA(2, RR)
+        Euclidean Jordan algebra of dimension 3 over Real Field with
+        53 bits of precision
 
     TESTS:
 
-    Unembedding is the inverse of embedding::
+    The dimension of this algebra is `(n^2 + n) / 2`::
 
         sage: set_random_seed()
-        sage: F = QuadraticField(-1, 'i')
-        sage: M = random_matrix(F, 3)
-        sage: _unembed_complex_matrix(_embed_complex_matrix(M)) == M
+        sage: n_max = RealSymmetricEJA._max_test_case_size()
+        sage: n = ZZ.random_element(1, n_max)
+        sage: J = RealSymmetricEJA(n)
+        sage: J.dimension() == (n^2 + n)/2
         True
 
-    """
-    n = ZZ(M.nrows())
-    if M.ncols() != n:
-        raise ValueError("the matrix 'M' must be square")
-    if not n.mod(2).is_zero():
-        raise ValueError("the matrix 'M' must be a complex embedding")
-
-    F = QuadraticField(-1, 'i')
-    i = F.gen()
-
-    # Go top-left to bottom-right (reading order), converting every
-    # 2-by-2 block we see to a single complex element.
-    elements = []
-    for k in xrange(n/2):
-        for j in xrange(n/2):
-            submat = M[2*k:2*k+2,2*j:2*j+2]
-            if submat[0,0] != submat[1,1]:
-                raise ValueError('bad on-diagonal submatrix')
-            if submat[0,1] != -submat[1,0]:
-                raise ValueError('bad off-diagonal submatrix')
-            z = submat[0,0] + submat[0,1]*i
-            elements.append(z)
-
-    return matrix(F, n/2, elements)
-
-
-def _embed_quaternion_matrix(M):
-    """
-    Embed the n-by-n quaternion matrix ``M`` into the space of real
-    matrices of size 4n-by-4n by first sending each quaternion entry
-    `z = a + bi + cj + dk` to the block-complex matrix
-    ``[[a + bi, c+di],[-c + di, a-bi]]`, and then embedding those into
-    a real matrix.
+    The Jordan multiplication is what we think it is::
 
-    SETUP::
+        sage: set_random_seed()
+        sage: J = RealSymmetricEJA.random_instance()
+        sage: x,y = J.random_elements(2)
+        sage: actual = (x*y).natural_representation()
+        sage: X = x.natural_representation()
+        sage: Y = y.natural_representation()
+        sage: expected = (X*Y + Y*X)/2
+        sage: actual == expected
+        True
+        sage: J(expected) == x*y
+        True
 
-        sage: from mjo.eja.eja_algebra import _embed_quaternion_matrix
+    We can change the generator prefix::
 
-    EXAMPLES::
+        sage: RealSymmetricEJA(3, prefix='q').gens()
+        (q0, q1, q2, q3, q4, q5)
 
-        sage: Q = QuaternionAlgebra(QQ,-1,-1)
-        sage: i,j,k = Q.gens()
-        sage: x = 1 + 2*i + 3*j + 4*k
-        sage: M = matrix(Q, 1, [[x]])
-        sage: _embed_quaternion_matrix(M)
-        [ 1  2  3  4]
-        [-2  1 -4  3]
-        [-3  4  1 -2]
-        [-4 -3  2  1]
+    Our natural basis is normalized with respect to the natural inner
+    product unless we specify otherwise::
 
-    Embedding is a homomorphism (isomorphism, in fact)::
+        sage: set_random_seed()
+        sage: J = RealSymmetricEJA.random_instance()
+        sage: all( b.norm() == 1 for b in J.gens() )
+        True
+
+    Since our natural basis is normalized with respect to the natural
+    inner product, and since we know that this algebra is an EJA, any
+    left-multiplication operator's matrix will be symmetric because
+    natural->EJA basis representation is an isometry and within the EJA
+    the operator is self-adjoint by the Jordan axiom::
 
         sage: set_random_seed()
-        sage: n = ZZ.random_element(5)
-        sage: Q = QuaternionAlgebra(QQ,-1,-1)
-        sage: X = random_matrix(Q, n)
-        sage: Y = random_matrix(Q, n)
-        sage: actual = _embed_quaternion_matrix(X)*_embed_quaternion_matrix(Y)
-        sage: expected = _embed_quaternion_matrix(X*Y)
-        sage: actual == expected
+        sage: x = RealSymmetricEJA.random_instance().random_element()
+        sage: x.operator().matrix().is_symmetric()
         True
 
     """
-    quaternions = M.base_ring()
-    n = M.nrows()
-    if M.ncols() != n:
-        raise ValueError("the matrix 'M' must be square")
-
-    F = QuadraticField(-1, 'i')
-    i = F.gen()
-
-    blocks = []
-    for z in M.list():
-        t = z.coefficient_tuple()
-        a = t[0]
-        b = t[1]
-        c = t[2]
-        d = t[3]
-        cplx_matrix = matrix(F, 2, [[ a + b*i, c + d*i],
-                                    [-c + d*i, a - b*i]])
-        blocks.append(_embed_complex_matrix(cplx_matrix))
-
-    # We should have real entries by now, so use the realest field
-    # we've got for the return value.
-    return matrix.block(quaternions.base_ring(), n, blocks)
-
-
-def _unembed_quaternion_matrix(M):
-    """
-    The inverse of _embed_quaternion_matrix().
+    @classmethod
+    def _denormalized_basis(cls, n, field):
+        """
+        Return a basis for the space of real symmetric n-by-n matrices.
 
-    SETUP::
+        SETUP::
 
-        sage: from mjo.eja.eja_algebra import (_embed_quaternion_matrix,
-        ....:                                  _unembed_quaternion_matrix)
+            sage: from mjo.eja.eja_algebra import RealSymmetricEJA
 
-    EXAMPLES::
+        TESTS::
 
-        sage: M = matrix(QQ, [[ 1,  2,  3,  4],
-        ....:                 [-2,  1, -4,  3],
-        ....:                 [-3,  4,  1, -2],
-        ....:                 [-4, -3,  2,  1]])
-        sage: _unembed_quaternion_matrix(M)
-        [1 + 2*i + 3*j + 4*k]
+            sage: set_random_seed()
+            sage: n = ZZ.random_element(1,5)
+            sage: B = RealSymmetricEJA._denormalized_basis(n,QQ)
+            sage: all( M.is_symmetric() for M in  B)
+            True
 
-    TESTS:
+        """
+        # 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 S
 
-    Unembedding is the inverse of embedding::
 
-        sage: set_random_seed()
-        sage: Q = QuaternionAlgebra(QQ, -1, -1)
-        sage: M = random_matrix(Q, 3)
-        sage: _unembed_quaternion_matrix(_embed_quaternion_matrix(M)) == M
-        True
+    @staticmethod
+    def _max_test_case_size():
+        return 4 # Dimension 10
 
-    """
-    n = ZZ(M.nrows())
-    if M.ncols() != n:
-        raise ValueError("the matrix 'M' must be square")
-    if not n.mod(4).is_zero():
-        raise ValueError("the matrix 'M' must be a complex embedding")
-
-    Q = QuaternionAlgebra(QQ,-1,-1)
-    i,j,k = Q.gens()
-
-    # Go top-left to bottom-right (reading order), converting every
-    # 4-by-4 block we see to a 2-by-2 complex block, to a 1-by-1
-    # quaternion block.
-    elements = []
-    for l in xrange(n/4):
-        for m in xrange(n/4):
-            submat = _unembed_complex_matrix(M[4*l:4*l+4,4*m:4*m+4])
-            if submat[0,0] != submat[1,1].conjugate():
-                raise ValueError('bad on-diagonal submatrix')
-            if submat[0,1] != -submat[1,0].conjugate():
-                raise ValueError('bad off-diagonal submatrix')
-            z  = submat[0,0].real() + submat[0,0].imag()*i
-            z += submat[0,1].real()*j + submat[0,1].imag()*k
-            elements.append(z)
-
-    return matrix(Q, n/4, elements)
-
-
-# The usual inner product on R^n.
-def _usual_ip(x,y):
-    return x.to_vector().inner_product(y.to_vector())
-
-# The inner product used for the real symmetric simple EJA.
-# We keep it as a separate function because e.g. the complex
-# algebra uses the same inner product, except divided by 2.
-def _matrix_ip(X,Y):
-    X_mat = X.natural_representation()
-    Y_mat = Y.natural_representation()
-    return (X_mat*Y_mat).trace()
-
-
-class RealSymmetricEJA(FiniteDimensionalEuclideanJordanAlgebra):
-    """
-    The rank-n simple EJA consisting of real symmetric n-by-n
-    matrices, the usual symmetric Jordan product, and the trace inner
-    product. It has dimension `(n^2 + n)/2` over the reals.
 
-    SETUP::
+    def __init__(self, n, field=AA, **kwargs):
+        basis = self._denormalized_basis(n, field)
+        super(RealSymmetricEJA, self).__init__(field, basis, **kwargs)
+        self.rank.set_cache(n)
 
-        sage: from mjo.eja.eja_algebra import RealSymmetricEJA
 
-    EXAMPLES::
+class ComplexMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
+    @staticmethod
+    def real_embed(M):
+        """
+        Embed the n-by-n complex matrix ``M`` into the space of real
+        matrices of size 2n-by-2n via the map the sends each entry `z = a +
+        bi` to the block matrix ``[[a,b],[-b,a]]``.
 
-        sage: J = RealSymmetricEJA(2)
-        sage: e0, e1, e2 = J.gens()
-        sage: e0*e0
-        e0
-        sage: e1*e1
-        e0 + e2
-        sage: e2*e2
-        e2
+        SETUP::
 
-    TESTS:
+            sage: from mjo.eja.eja_algebra import \
+            ....:   ComplexMatrixEuclideanJordanAlgebra
 
-    The dimension of this algebra is `(n^2 + n) / 2`::
+        EXAMPLES::
 
-        sage: set_random_seed()
-        sage: n = ZZ.random_element(1,5)
-        sage: J = RealSymmetricEJA(n)
-        sage: J.dimension() == (n^2 + n)/2
-        True
+            sage: F = QuadraticField(-1, 'I')
+            sage: x1 = F(4 - 2*i)
+            sage: x2 = F(1 + 2*i)
+            sage: x3 = F(-i)
+            sage: x4 = F(6)
+            sage: M = matrix(F,2,[[x1,x2],[x3,x4]])
+            sage: ComplexMatrixEuclideanJordanAlgebra.real_embed(M)
+            [ 4 -2| 1  2]
+            [ 2  4|-2  1]
+            [-----+-----]
+            [ 0 -1| 6  0]
+            [ 1  0| 0  6]
 
-    The Jordan multiplication is what we think it is::
+        TESTS:
 
-        sage: set_random_seed()
-        sage: n = ZZ.random_element(1,5)
-        sage: J = RealSymmetricEJA(n)
-        sage: x = J.random_element()
-        sage: y = J.random_element()
-        sage: actual = (x*y).natural_representation()
-        sage: X = x.natural_representation()
-        sage: Y = y.natural_representation()
-        sage: expected = (X*Y + Y*X)/2
-        sage: actual == expected
-        True
-        sage: J(expected) == x*y
-        True
+        Embedding is a homomorphism (isomorphism, in fact)::
 
-    """
-    def __init__(self, n, field=QQ):
-        S = _real_symmetric_basis(n, field=field)
-        Qs = _multiplication_table_from_matrix_basis(S)
+            sage: set_random_seed()
+            sage: n_max = ComplexMatrixEuclideanJordanAlgebra._max_test_case_size()
+            sage: n = ZZ.random_element(n_max)
+            sage: F = QuadraticField(-1, 'I')
+            sage: X = random_matrix(F, n)
+            sage: Y = random_matrix(F, n)
+            sage: Xe = ComplexMatrixEuclideanJordanAlgebra.real_embed(X)
+            sage: Ye = ComplexMatrixEuclideanJordanAlgebra.real_embed(Y)
+            sage: XYe = ComplexMatrixEuclideanJordanAlgebra.real_embed(X*Y)
+            sage: Xe*Ye == XYe
+            True
+
+        """
+        n = M.nrows()
+        if M.ncols() != n:
+            raise ValueError("the matrix 'M' must be square")
 
-        fdeja = super(RealSymmetricEJA, self)
-        return fdeja.__init__(field,
-                              Qs,
-                              rank=n,
-                              natural_basis=S)
+        # We don't need any adjoined elements...
+        field = M.base_ring().base_ring()
 
-    def inner_product(self, x, y):
-        return _matrix_ip(x,y)
+        blocks = []
+        for z in M.list():
+            a = z.list()[0] # real part, I guess
+            b = z.list()[1] # imag part, I guess
+            blocks.append(matrix(field, 2, [[a,b],[-b,a]]))
+
+        return matrix.block(field, n, blocks)
+
+
+    @staticmethod
+    def real_unembed(M):
+        """
+        The inverse of _embed_complex_matrix().
+
+        SETUP::
+
+            sage: from mjo.eja.eja_algebra import \
+            ....:   ComplexMatrixEuclideanJordanAlgebra
+
+        EXAMPLES::
+
+            sage: A = matrix(QQ,[ [ 1,  2,   3,  4],
+            ....:                 [-2,  1,  -4,  3],
+            ....:                 [ 9,  10, 11, 12],
+            ....:                 [-10, 9, -12, 11] ])
+            sage: ComplexMatrixEuclideanJordanAlgebra.real_unembed(A)
+            [  2*I + 1   4*I + 3]
+            [ 10*I + 9 12*I + 11]
+
+        TESTS:
+
+        Unembedding is the inverse of embedding::
+
+            sage: set_random_seed()
+            sage: F = QuadraticField(-1, 'I')
+            sage: M = random_matrix(F, 3)
+            sage: Me = ComplexMatrixEuclideanJordanAlgebra.real_embed(M)
+            sage: ComplexMatrixEuclideanJordanAlgebra.real_unembed(Me) == M
+            True
+
+        """
+        n = ZZ(M.nrows())
+        if M.ncols() != n:
+            raise ValueError("the matrix 'M' must be square")
+        if not n.mod(2).is_zero():
+            raise ValueError("the matrix 'M' must be a complex embedding")
+
+        # If "M" was normalized, its base ring might have roots
+        # adjoined and they can stick around after unembedding.
+        field = M.base_ring()
+        R = PolynomialRing(field, 'z')
+        z = R.gen()
+        if field is AA:
+            # Sage doesn't know how to embed AA into QQbar, i.e. how
+            # to adjoin sqrt(-1) to AA.
+            F = QQbar
+        else:
+            F = field.extension(z**2 + 1, 'I', embedding=CLF(-1).sqrt())
+        i = F.gen()
+
+        # Go top-left to bottom-right (reading order), converting every
+        # 2-by-2 block we see to a single complex element.
+        elements = []
+        for k in range(n/2):
+            for j in range(n/2):
+                submat = M[2*k:2*k+2,2*j:2*j+2]
+                if submat[0,0] != submat[1,1]:
+                    raise ValueError('bad on-diagonal submatrix')
+                if submat[0,1] != -submat[1,0]:
+                    raise ValueError('bad off-diagonal submatrix')
+                z = submat[0,0] + submat[0,1]*i
+                elements.append(z)
+
+        return matrix(F, n/2, elements)
+
+
+    @classmethod
+    def natural_inner_product(cls,X,Y):
+        """
+        Compute a natural inner product in this algebra directly from
+        its real embedding.
+
+        SETUP::
+
+            sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
+
+        TESTS:
+
+        This gives the same answer as the slow, default method implemented
+        in :class:`MatrixEuclideanJordanAlgebra`::
+
+            sage: set_random_seed()
+            sage: J = ComplexHermitianEJA.random_instance()
+            sage: x,y = J.random_elements(2)
+            sage: Xe = x.natural_representation()
+            sage: Ye = y.natural_representation()
+            sage: X = ComplexHermitianEJA.real_unembed(Xe)
+            sage: Y = ComplexHermitianEJA.real_unembed(Ye)
+            sage: expected = (X*Y).trace().real()
+            sage: actual = ComplexHermitianEJA.natural_inner_product(Xe,Ye)
+            sage: actual == expected
+            True
+
+        """
+        return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/2
 
 
-class ComplexHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra):
+class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra, KnownRankEJA):
     """
     The rank-n simple EJA consisting of complex Hermitian n-by-n
     matrices over the real numbers, the usual symmetric Jordan product,
@@ -1112,12 +1581,23 @@ class ComplexHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra):
 
         sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
 
+    EXAMPLES:
+
+    In theory, our "field" can be any subfield of the reals::
+
+        sage: ComplexHermitianEJA(2, RDF)
+        Euclidean Jordan algebra of dimension 4 over Real Double Field
+        sage: ComplexHermitianEJA(2, RR)
+        Euclidean Jordan algebra of dimension 4 over Real Field with
+        53 bits of precision
+
     TESTS:
 
     The dimension of this algebra is `n^2`::
 
         sage: set_random_seed()
-        sage: n = ZZ.random_element(1,5)
+        sage: n_max = ComplexHermitianEJA._max_test_case_size()
+        sage: n = ZZ.random_element(1, n_max)
         sage: J = ComplexHermitianEJA(n)
         sage: J.dimension() == n^2
         True
@@ -1125,10 +1605,8 @@ class ComplexHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra):
     The Jordan multiplication is what we think it is::
 
         sage: set_random_seed()
-        sage: n = ZZ.random_element(1,5)
-        sage: J = ComplexHermitianEJA(n)
-        sage: x = J.random_element()
-        sage: y = J.random_element()
+        sage: J = ComplexHermitianEJA.random_instance()
+        sage: x,y = J.random_elements(2)
         sage: actual = (x*y).natural_representation()
         sage: X = x.natural_representation()
         sage: Y = y.natural_representation()
@@ -1138,30 +1616,257 @@ class ComplexHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra):
         sage: J(expected) == x*y
         True
 
+    We can change the generator prefix::
+
+        sage: ComplexHermitianEJA(2, prefix='z').gens()
+        (z0, z1, z2, z3)
+
+    Our natural basis is normalized with respect to the natural inner
+    product unless we specify otherwise::
+
+        sage: set_random_seed()
+        sage: J = ComplexHermitianEJA.random_instance()
+        sage: all( b.norm() == 1 for b in J.gens() )
+        True
+
+    Since our natural basis is normalized with respect to the natural
+    inner product, and since we know that this algebra is an EJA, any
+    left-multiplication operator's matrix will be symmetric because
+    natural->EJA basis representation is an isometry and within the EJA
+    the operator is self-adjoint by the Jordan axiom::
+
+        sage: set_random_seed()
+        sage: x = ComplexHermitianEJA.random_instance().random_element()
+        sage: x.operator().matrix().is_symmetric()
+        True
+
     """
-    def __init__(self, n, field=QQ):
-        S = _complex_hermitian_basis(n)
-        Qs = _multiplication_table_from_matrix_basis(S)
 
-        fdeja = super(ComplexHermitianEJA, self)
-        return fdeja.__init__(field,
-                              Qs,
-                              rank=n,
-                              natural_basis=S)
+    @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.
 
-    def inner_product(self, x, y):
-        # Since a+bi on the diagonal is represented as
+        SETUP::
+
+            sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
+
+        TESTS::
+
+            sage: set_random_seed()
+            sage: n = ZZ.random_element(1,5)
+            sage: field = QuadraticField(2, 'sqrt2')
+            sage: B = ComplexHermitianEJA._denormalized_basis(n, field)
+            sage: all( M.is_symmetric() for M in  B)
+            True
+
+        """
+        R = PolynomialRing(field, 'z')
+        z = R.gen()
+        F = field.extension(z**2 + 1, 'I')
+        I = F.gen()
+
+        # This is like the symmetric case, but we need to be careful:
         #
-        #   a + bi  = [  a  b  ]
-        #             [ -b  a  ],
+        #   * We want conjugate-symmetry, not just symmetry.
+        #   * The diagonal will (as a result) be real.
         #
-        # we'll double-count the "a" entries if we take the trace of
-        # the embedding.
-        return _matrix_ip(x,y)/2
+        S = []
+        for i in range(n):
+            for j in range(i+1):
+                Eij = matrix(F, n, lambda k,l: k==i and l==j)
+                if i == j:
+                    Sij = cls.real_embed(Eij)
+                    S.append(Sij)
+                else:
+                    # The second one has a minus because it's conjugated.
+                    Sij_real = cls.real_embed(Eij + Eij.transpose())
+                    S.append(Sij_real)
+                    Sij_imag = cls.real_embed(I*Eij - I*Eij.transpose())
+                    S.append(Sij_imag)
+
+        # Since we embedded these, we can drop back to the "field" that we
+        # started with instead of the complex extension "F".
+        return ( s.change_ring(field) for s in S )
+
+
+    def __init__(self, n, field=AA, **kwargs):
+        basis = self._denormalized_basis(n,field)
+        super(ComplexHermitianEJA,self).__init__(field, basis, **kwargs)
+        self.rank.set_cache(n)
+
+
+class QuaternionMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
+    @staticmethod
+    def real_embed(M):
+        """
+        Embed the n-by-n quaternion matrix ``M`` into the space of real
+        matrices of size 4n-by-4n by first sending each quaternion entry `z
+        = a + bi + cj + dk` to the block-complex matrix ``[[a + bi,
+        c+di],[-c + di, a-bi]]`, and then embedding those into a real
+        matrix.
+
+        SETUP::
+
+            sage: from mjo.eja.eja_algebra import \
+            ....:   QuaternionMatrixEuclideanJordanAlgebra
+
+        EXAMPLES::
+
+            sage: Q = QuaternionAlgebra(QQ,-1,-1)
+            sage: i,j,k = Q.gens()
+            sage: x = 1 + 2*i + 3*j + 4*k
+            sage: M = matrix(Q, 1, [[x]])
+            sage: QuaternionMatrixEuclideanJordanAlgebra.real_embed(M)
+            [ 1  2  3  4]
+            [-2  1 -4  3]
+            [-3  4  1 -2]
+            [-4 -3  2  1]
 
+        Embedding is a homomorphism (isomorphism, in fact)::
 
-class QuaternionHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra):
+            sage: set_random_seed()
+            sage: n_max = QuaternionMatrixEuclideanJordanAlgebra._max_test_case_size()
+            sage: n = ZZ.random_element(n_max)
+            sage: Q = QuaternionAlgebra(QQ,-1,-1)
+            sage: X = random_matrix(Q, n)
+            sage: Y = random_matrix(Q, n)
+            sage: Xe = QuaternionMatrixEuclideanJordanAlgebra.real_embed(X)
+            sage: Ye = QuaternionMatrixEuclideanJordanAlgebra.real_embed(Y)
+            sage: XYe = QuaternionMatrixEuclideanJordanAlgebra.real_embed(X*Y)
+            sage: Xe*Ye == XYe
+            True
+
+        """
+        quaternions = M.base_ring()
+        n = M.nrows()
+        if M.ncols() != n:
+            raise ValueError("the matrix 'M' must be square")
+
+        F = QuadraticField(-1, 'I')
+        i = F.gen()
+
+        blocks = []
+        for z in M.list():
+            t = z.coefficient_tuple()
+            a = t[0]
+            b = t[1]
+            c = t[2]
+            d = t[3]
+            cplxM = matrix(F, 2, [[ a + b*i, c + d*i],
+                                 [-c + d*i, a - b*i]])
+            realM = ComplexMatrixEuclideanJordanAlgebra.real_embed(cplxM)
+            blocks.append(realM)
+
+        # We should have real entries by now, so use the realest field
+        # we've got for the return value.
+        return matrix.block(quaternions.base_ring(), n, blocks)
+
+
+
+    @staticmethod
+    def real_unembed(M):
+        """
+        The inverse of _embed_quaternion_matrix().
+
+        SETUP::
+
+            sage: from mjo.eja.eja_algebra import \
+            ....:   QuaternionMatrixEuclideanJordanAlgebra
+
+        EXAMPLES::
+
+            sage: M = matrix(QQ, [[ 1,  2,  3,  4],
+            ....:                 [-2,  1, -4,  3],
+            ....:                 [-3,  4,  1, -2],
+            ....:                 [-4, -3,  2,  1]])
+            sage: QuaternionMatrixEuclideanJordanAlgebra.real_unembed(M)
+            [1 + 2*i + 3*j + 4*k]
+
+        TESTS:
+
+        Unembedding is the inverse of embedding::
+
+            sage: set_random_seed()
+            sage: Q = QuaternionAlgebra(QQ, -1, -1)
+            sage: M = random_matrix(Q, 3)
+            sage: Me = QuaternionMatrixEuclideanJordanAlgebra.real_embed(M)
+            sage: QuaternionMatrixEuclideanJordanAlgebra.real_unembed(Me) == M
+            True
+
+        """
+        n = ZZ(M.nrows())
+        if M.ncols() != n:
+            raise ValueError("the matrix 'M' must be square")
+        if not n.mod(4).is_zero():
+            raise ValueError("the matrix 'M' must be a quaternion embedding")
+
+        # Use the base ring of the matrix to ensure that its entries can be
+        # multiplied by elements of the quaternion algebra.
+        field = M.base_ring()
+        Q = QuaternionAlgebra(field,-1,-1)
+        i,j,k = Q.gens()
+
+        # Go top-left to bottom-right (reading order), converting every
+        # 4-by-4 block we see to a 2-by-2 complex block, to a 1-by-1
+        # quaternion block.
+        elements = []
+        for l in range(n/4):
+            for m in range(n/4):
+                submat = ComplexMatrixEuclideanJordanAlgebra.real_unembed(
+                    M[4*l:4*l+4,4*m:4*m+4] )
+                if submat[0,0] != submat[1,1].conjugate():
+                    raise ValueError('bad on-diagonal submatrix')
+                if submat[0,1] != -submat[1,0].conjugate():
+                    raise ValueError('bad off-diagonal submatrix')
+                z  = submat[0,0].real()
+                z += submat[0,0].imag()*i
+                z += submat[0,1].real()*j
+                z += submat[0,1].imag()*k
+                elements.append(z)
+
+        return matrix(Q, n/4, elements)
+
+
+    @classmethod
+    def natural_inner_product(cls,X,Y):
+        """
+        Compute a natural inner product in this algebra directly from
+        its real embedding.
+
+        SETUP::
+
+            sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
+
+        TESTS:
+
+        This gives the same answer as the slow, default method implemented
+        in :class:`MatrixEuclideanJordanAlgebra`::
+
+            sage: set_random_seed()
+            sage: J = QuaternionHermitianEJA.random_instance()
+            sage: x,y = J.random_elements(2)
+            sage: Xe = x.natural_representation()
+            sage: Ye = y.natural_representation()
+            sage: X = QuaternionHermitianEJA.real_unembed(Xe)
+            sage: Y = QuaternionHermitianEJA.real_unembed(Ye)
+            sage: expected = (X*Y).trace().coefficient_tuple()[0]
+            sage: actual = QuaternionHermitianEJA.natural_inner_product(Xe,Ye)
+            sage: actual == expected
+            True
+
+        """
+        return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/4
+
+
+class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra,
+                             KnownRankEJA):
     """
     The rank-n simple EJA consisting of self-adjoint n-by-n quaternion
     matrices, the usual symmetric Jordan product, and the
@@ -1172,12 +1877,23 @@ class QuaternionHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra):
 
         sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
 
+    EXAMPLES:
+
+    In theory, our "field" can be any subfield of the reals::
+
+        sage: QuaternionHermitianEJA(2, RDF)
+        Euclidean Jordan algebra of dimension 6 over Real Double Field
+        sage: QuaternionHermitianEJA(2, RR)
+        Euclidean Jordan algebra of dimension 6 over Real Field with
+        53 bits of precision
+
     TESTS:
 
-    The dimension of this algebra is `n^2`::
+    The dimension of this algebra is `2*n^2 - n`::
 
         sage: set_random_seed()
-        sage: n = ZZ.random_element(1,5)
+        sage: n_max = QuaternionHermitianEJA._max_test_case_size()
+        sage: n = ZZ.random_element(1, n_max)
         sage: J = QuaternionHermitianEJA(n)
         sage: J.dimension() == 2*(n^2) - n
         True
@@ -1185,10 +1901,8 @@ class QuaternionHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra):
     The Jordan multiplication is what we think it is::
 
         sage: set_random_seed()
-        sage: n = ZZ.random_element(1,5)
-        sage: J = QuaternionHermitianEJA(n)
-        sage: x = J.random_element()
-        sage: y = J.random_element()
+        sage: J = QuaternionHermitianEJA.random_instance()
+        sage: x,y = J.random_elements(2)
         sage: actual = (x*y).natural_representation()
         sage: X = x.natural_representation()
         sage: Y = y.natural_representation()
@@ -1198,35 +1912,217 @@ class QuaternionHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra):
         sage: J(expected) == x*y
         True
 
+    We can change the generator prefix::
+
+        sage: QuaternionHermitianEJA(2, prefix='a').gens()
+        (a0, a1, a2, a3, a4, a5)
+
+    Our natural basis is normalized with respect to the natural inner
+    product unless we specify otherwise::
+
+        sage: set_random_seed()
+        sage: J = QuaternionHermitianEJA.random_instance()
+        sage: all( b.norm() == 1 for b in J.gens() )
+        True
+
+    Since our natural basis is normalized with respect to the natural
+    inner product, and since we know that this algebra is an EJA, any
+    left-multiplication operator's matrix will be symmetric because
+    natural->EJA basis representation is an isometry and within the EJA
+    the operator is self-adjoint by the Jordan axiom::
+
+        sage: set_random_seed()
+        sage: x = QuaternionHermitianEJA.random_instance().random_element()
+        sage: x.operator().matrix().is_symmetric()
+        True
+
     """
-    def __init__(self, n, field=QQ):
-        S = _quaternion_hermitian_basis(n)
-        Qs = _multiplication_table_from_matrix_basis(S)
+    @classmethod
+    def _denormalized_basis(cls, n, field):
+        """
+        Returns a basis for the space of quaternion Hermitian n-by-n matrices.
 
-        fdeja = super(QuaternionHermitianEJA, self)
-        return fdeja.__init__(field,
-                              Qs,
-                              rank=n,
-                              natural_basis=S)
+        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.
 
-    def inner_product(self, x, y):
-        # Since a+bi+cj+dk on the diagonal is represented as
+        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_symmetric() for M in B )
+            True
+
+        """
+        Q = QuaternionAlgebra(QQ,-1,-1)
+        I,J,K = Q.gens()
+
+        # This is like the symmetric case, but we need to be careful:
         #
-        #   a + bi +cj + dk = [  a  b  c  d]
-        #                     [ -b  a -d  c]
-        #                     [ -c  d  a -b]
-        #                     [ -d -c  b  a],
+        #   * We want conjugate-symmetry, not just symmetry.
+        #   * The diagonal will (as a result) be real.
         #
-        # we'll quadruple-count the "a" entries if we take the trace of
-        # the embedding.
-        return _matrix_ip(x,y)/4
+        S = []
+        for i in range(n):
+            for j in range(i+1):
+                Eij = matrix(Q, n, lambda k,l: k==i and l==j)
+                if i == j:
+                    Sij = cls.real_embed(Eij)
+                    S.append(Sij)
+                else:
+                    # The second, third, and fourth ones have a minus
+                    # because they're conjugated.
+                    Sij_real = cls.real_embed(Eij + Eij.transpose())
+                    S.append(Sij_real)
+                    Sij_I = cls.real_embed(I*Eij - I*Eij.transpose())
+                    S.append(Sij_I)
+                    Sij_J = cls.real_embed(J*Eij - J*Eij.transpose())
+                    S.append(Sij_J)
+                    Sij_K = cls.real_embed(K*Eij - K*Eij.transpose())
+                    S.append(Sij_K)
+
+        # Since we embedded these, we can drop back to the "field" that we
+        # started with instead of the quaternion algebra "Q".
+        return ( s.change_ring(field) for s in S )
+
+
+    def __init__(self, n, field=AA, **kwargs):
+        basis = self._denormalized_basis(n,field)
+        super(QuaternionHermitianEJA,self).__init__(field, basis, **kwargs)
+        self.rank.set_cache(n)
+
+
+class BilinearFormEJA(FiniteDimensionalEuclideanJordanAlgebra, KnownRankEJA):
+    r"""
+    The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
+    with the half-trace inner product and jordan product ``x*y =
+    (x0*y0 + <B*x_bar,y_bar>, x0*y_bar + y0*x_bar)`` where ``B`` is a
+    symmetric positive-definite "bilinear form" matrix. It has
+    dimension `n` over the reals, and reduces to the ``JordanSpinEJA``
+    when ``B`` is the identity matrix of order ``n-1``.
+
+    SETUP::
+
+        sage: from mjo.eja.eja_algebra import (BilinearFormEJA,
+        ....:                                  JordanSpinEJA)
+
+    EXAMPLES:
+
+    When no bilinear form is specified, the identity matrix is used,
+    and the resulting algebra is the Jordan spin algebra::
+
+        sage: J0 = BilinearFormEJA(3)
+        sage: J1 = JordanSpinEJA(3)
+        sage: J0.multiplication_table() == J0.multiplication_table()
+        True
+
+    TESTS:
 
+    We can create a zero-dimensional algebra::
 
-class JordanSpinEJA(FiniteDimensionalEuclideanJordanAlgebra):
+        sage: J = BilinearFormEJA(0)
+        sage: J.basis()
+        Finite family {}
+
+    We can check the multiplication condition given in the Jordan, von
+    Neumann, and Wigner paper (and also discussed on my "On the
+    symmetry..." paper). Note that this relies heavily on the standard
+    choice of basis, as does anything utilizing the bilinear form matrix::
+
+        sage: set_random_seed()
+        sage: n = ZZ.random_element(5)
+        sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular')
+        sage: B = M.transpose()*M
+        sage: J = BilinearFormEJA(n, B=B)
+        sage: eis = VectorSpace(M.base_ring(), M.ncols()).basis()
+        sage: V = J.vector_space()
+        sage: sis = [ J.from_vector(V([0] + (M.inverse()*ei).list()))
+        ....:         for ei in eis ]
+        sage: actual = [ sis[i]*sis[j]
+        ....:            for i in range(n-1)
+        ....:            for j in range(n-1) ]
+        sage: expected = [ J.one() if i == j else J.zero()
+        ....:              for i in range(n-1)
+        ....:              for j in range(n-1) ]
+        sage: actual == expected
+        True
+    """
+    def __init__(self, n, field=AA, B=None, **kwargs):
+        if B is None:
+            self._B = matrix.identity(field, max(0,n-1))
+        else:
+            self._B = B
+
+        V = VectorSpace(field, n)
+        mult_table = [[V.zero() for j in range(n)] for i in range(n)]
+        for i in range(n):
+            for j in range(n):
+                x = V.gen(i)
+                y = V.gen(j)
+                x0 = x[0]
+                xbar = x[1:]
+                y0 = y[0]
+                ybar = y[1:]
+                z0 = x0*y0 + (self._B*xbar).inner_product(ybar)
+                zbar = y0*xbar + x0*ybar
+                z = V([z0] + zbar.list())
+                mult_table[i][j] = z
+
+        # The rank of this algebra is two, unless we're in a
+        # one-dimensional ambient space (because the rank is bounded
+        # by the ambient dimension).
+        fdeja = super(BilinearFormEJA, self)
+        fdeja.__init__(field, mult_table, **kwargs)
+        self.rank.set_cache(min(n,2))
+
+    def inner_product(self, x, y):
+        r"""
+        Half of the trace inner product.
+
+        This is defined so that the special case of the Jordan spin
+        algebra gets the usual inner product.
+
+        SETUP::
+
+            sage: from mjo.eja.eja_algebra import BilinearFormEJA
+
+        TESTS:
+
+        Ensure that this is one-half of the trace inner-product when
+        the algebra isn't just the reals (when ``n`` isn't one). This
+        is in Faraut and Koranyi, and also my "On the symmetry..."
+        paper::
+
+            sage: set_random_seed()
+            sage: n = ZZ.random_element(2,5)
+            sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular')
+            sage: B = M.transpose()*M
+            sage: J = BilinearFormEJA(n, B=B)
+            sage: x = J.random_element()
+            sage: y = J.random_element()
+            sage: x.inner_product(y) == (x*y).trace()/2
+            True
+
+        """
+        xvec = x.to_vector()
+        xbar = xvec[1:]
+        yvec = y.to_vector()
+        ybar = yvec[1:]
+        return x[0]*y[0] + (self._B*xbar).inner_product(ybar)
+
+
+class JordanSpinEJA(BilinearFormEJA):
     """
     The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
     with the usual inner product and jordan product ``x*y =
-    (<x_bar,y_bar>, x0*y_bar + y0*x_bar)``. It has dimension `n` over
+    (<x,y>, x0*y_bar + y0*x_bar)``. It has dimension `n` over
     the reals.
 
     SETUP::
@@ -1254,29 +2150,63 @@ class JordanSpinEJA(FiniteDimensionalEuclideanJordanAlgebra):
         sage: e2*e3
         0
 
+    We can change the generator prefix::
+
+        sage: JordanSpinEJA(2, prefix='B').gens()
+        (B0, B1)
+
+    TESTS:
+
+        Ensure that we have the usual inner product on `R^n`::
+
+            sage: set_random_seed()
+            sage: J = JordanSpinEJA.random_instance()
+            sage: x,y = J.random_elements(2)
+            sage: X = x.natural_representation()
+            sage: Y = y.natural_representation()
+            sage: x.inner_product(y) == J.natural_inner_product(X,Y)
+            True
+
     """
-    def __init__(self, n, field=QQ):
-        V = VectorSpace(field, n)
-        mult_table = [[V.zero() for i in range(n)] for j in range(n)]
-        for i in range(n):
-            for j in range(n):
-                x = V.basis()[i]
-                y = V.basis()[j]
-                x0 = x[0]
-                xbar = x[1:]
-                y0 = y[0]
-                ybar = y[1:]
-                # z = x*y
-                z0 = x.inner_product(y)
-                zbar = y0*xbar + x0*ybar
-                z = V([z0] + zbar.list())
-                mult_table[i][j] = z
+    def __init__(self, n, field=AA, **kwargs):
+        # This is a special case of the BilinearFormEJA with the identity
+        # matrix as its bilinear form.
+        return super(JordanSpinEJA, self).__init__(n, field, **kwargs)
 
-        # The rank of the spin algebra is two, unless we're in a
-        # one-dimensional ambient space (because the rank is bounded by
-        # the ambient dimension).
-        fdeja = super(JordanSpinEJA, self)
-        return fdeja.__init__(field, mult_table, rank=min(n,2))
 
-    def inner_product(self, x, y):
-        return _usual_ip(x,y)
+class TrivialEJA(FiniteDimensionalEuclideanJordanAlgebra, KnownRankEJA):
+    """
+    The trivial Euclidean Jordan algebra consisting of only a zero element.
+
+    SETUP::
+
+        sage: from mjo.eja.eja_algebra import TrivialEJA
+
+    EXAMPLES::
+
+        sage: J = TrivialEJA()
+        sage: J.dimension()
+        0
+        sage: J.zero()
+        0
+        sage: J.one()
+        0
+        sage: 7*J.one()*12*J.one()
+        0
+        sage: J.one().inner_product(J.one())
+        0
+        sage: J.one().norm()
+        0
+        sage: J.one().subalgebra_generated_by()
+        Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
+        sage: J.rank()
+        0
+
+    """
+    def __init__(self, field=AA, **kwargs):
+        mult_table = []
+        fdeja = super(TrivialEJA, self)
+        # The rank is zero using my definition, namely the dimension of the
+        # largest subalgebra generated by any element.
+        fdeja.__init__(field, mult_table, **kwargs)
+        self.rank.set_cache(0)