]> gitweb.michael.orlitzky.com - sage.d.git/blobdiff - mjo/eja/euclidean_jordan_algebra.py
eja: move a bunch of operator_matrix() tests to operator().
[sage.d.git] / mjo / eja / euclidean_jordan_algebra.py
index b25839815289ba6f60630a14b790dbfe29f8969b..750b6c1c37460a704f83aeb62b66aaf4ce1ca952 100644 (file)
@@ -5,13 +5,349 @@ are used in optimization, and have some additional nice methods beyond
 what can be supported in a general Jordan Algebra.
 """
 
-from sage.categories.magmatic_algebras import MagmaticAlgebras
+from sage.categories.finite_dimensional_algebras_with_basis import FiniteDimensionalAlgebrasWithBasis
+from sage.categories.map import Map
 from sage.structure.element import is_Matrix
 from sage.structure.category_object import normalize_names
 
 from sage.algebras.finite_dimensional_algebras.finite_dimensional_algebra import FiniteDimensionalAlgebra
 from sage.algebras.finite_dimensional_algebras.finite_dimensional_algebra_element import FiniteDimensionalAlgebraElement
 
+
+class FiniteDimensionalEuclideanJordanAlgebraOperator(Map):
+    def __init__(self, domain_eja, codomain_eja, mat):
+        if not (
+          isinstance(domain_eja, FiniteDimensionalEuclideanJordanAlgebra) and
+          isinstance(codomain_eja, FiniteDimensionalEuclideanJordanAlgebra) ):
+            raise ValueError('(co)domains must be finite-dimensional Euclidean '
+                             'Jordan algebras')
+
+        F = domain_eja.base_ring()
+        if not (F == codomain_eja.base_ring()):
+            raise ValueError("domain and codomain must have the same base ring")
+
+        # We need to supply something here to avoid getting the
+        # default Homset of the parent FiniteDimensionalAlgebra class,
+        # which messes up e.g. equality testing.
+        parent = Hom(domain_eja, codomain_eja, VectorSpaces(F))
+
+        # The Map initializer will set our parent to a homset, which
+        # is explicitly NOT what we want, because these ain't algebra
+        # homomorphisms.
+        super(FiniteDimensionalEuclideanJordanAlgebraOperator,self).__init__(parent)
+
+        # Keep a matrix around to do all of the real work. It would
+        # be nice if we could use a VectorSpaceMorphism instead, but
+        # those use row vectors that we don't want to accidentally
+        # expose to our users.
+        self._matrix = mat
+
+
+    def _call_(self, x):
+        """
+        Allow this operator to be called only on elements of an EJA.
+
+        EXAMPLES::
+
+            sage: J = JordanSpinEJA(3)
+            sage: x = J.linear_combination(zip(range(len(J.gens())), J.gens()))
+            sage: id = identity_matrix(J.base_ring(), J.dimension())
+            sage: f = FiniteDimensionalEuclideanJordanAlgebraOperator(J,J,id)
+            sage: f(x) == x
+            True
+
+        """
+        return self.codomain()(self.matrix()*x.vector())
+
+
+    def _add_(self, other):
+        """
+        Add the ``other`` EJA operator to this one.
+
+        EXAMPLES:
+
+        When we add two EJA operators, we get another one back::
+
+            sage: J = RealSymmetricEJA(2)
+            sage: id = identity_matrix(J.base_ring(), J.dimension())
+            sage: f = FiniteDimensionalEuclideanJordanAlgebraOperator(J,J,id)
+            sage: g = FiniteDimensionalEuclideanJordanAlgebraOperator(J,J,id)
+            sage: f + g
+            Linear operator between finite-dimensional Euclidean Jordan
+            algebras represented by the matrix:
+            [2 0 0]
+            [0 2 0]
+            [0 0 2]
+            Domain: Euclidean Jordan algebra of degree 3 over Rational Field
+            Codomain: Euclidean Jordan algebra of degree 3 over Rational Field
+
+        If you try to add two identical vector space operators but on
+        different EJAs, that should blow up::
+
+            sage: J1 = RealSymmetricEJA(2)
+            sage: J2 = JordanSpinEJA(3)
+            sage: id = identity_matrix(QQ, 3)
+            sage: f = FiniteDimensionalEuclideanJordanAlgebraOperator(J1,J1,id)
+            sage: g = FiniteDimensionalEuclideanJordanAlgebraOperator(J2,J2,id)
+            sage: f + g
+            Traceback (most recent call last):
+            ...
+            TypeError: unsupported operand parent(s) for +: ...
+
+        """
+        return FiniteDimensionalEuclideanJordanAlgebraOperator(
+                self.domain(),
+                self.codomain(),
+                self.matrix() + other.matrix())
+
+
+    def _composition_(self, other, homset):
+        """
+        Compose two EJA operators to get another one (and NOT a formal
+        composite object) back.
+
+        EXAMPLES::
+
+            sage: J1 = JordanSpinEJA(3)
+            sage: J2 = RealCartesianProductEJA(2)
+            sage: J3 = RealSymmetricEJA(1)
+            sage: mat1 = matrix(QQ, [[1,2,3],
+            ....:                    [4,5,6]])
+            sage: mat2 = matrix(QQ, [[7,8]])
+            sage: g = FiniteDimensionalEuclideanJordanAlgebraOperator(J1,
+            ....:                                                     J2,
+            ....:                                                     mat1)
+            sage: f = FiniteDimensionalEuclideanJordanAlgebraOperator(J2,
+            ....:                                                     J3,
+            ....:                                                     mat2)
+            sage: f*g
+            Linear operator between finite-dimensional Euclidean Jordan
+            algebras represented by the matrix:
+            [39 54 69]
+            Domain: Euclidean Jordan algebra of degree 3 over Rational Field
+            Codomain: Euclidean Jordan algebra of degree 1 over Rational Field
+
+        """
+        return FiniteDimensionalEuclideanJordanAlgebraOperator(
+          other.domain(),
+          self.codomain(),
+          self.matrix()*other.matrix())
+
+
+    def __eq__(self, other):
+        if self.domain() != other.domain():
+            return False
+        if self.codomain() != other.codomain():
+            return False
+        if self.matrix() != other.matrix():
+            return False
+        return True
+
+    def __invert__(self):
+        """
+        Invert this EJA operator.
+
+        EXAMPLES::
+
+            sage: J = RealSymmetricEJA(2)
+            sage: id = identity_matrix(J.base_ring(), J.dimension())
+            sage: f = FiniteDimensionalEuclideanJordanAlgebraOperator(J,J,id)
+            sage: ~f
+            Linear operator between finite-dimensional Euclidean Jordan
+            algebras represented by the matrix:
+            [1 0 0]
+            [0 1 0]
+            [0 0 1]
+            Domain: Euclidean Jordan algebra of degree 3 over Rational Field
+            Codomain: Euclidean Jordan algebra of degree 3 over Rational Field
+
+        """
+        return FiniteDimensionalEuclideanJordanAlgebraOperator(
+                self.codomain(),
+                self.domain(),
+                ~self.matrix())
+
+
+    def __mul__(self, other):
+        """
+        Compose two EJA operators, or scale myself by an element of the
+        ambient vector space.
+
+        We need to override the real ``__mul__`` function to prevent the
+        coercion framework from throwing an error when it fails to convert
+        a base ring element into a morphism.
+
+        EXAMPLES:
+
+        We can scale an operator on a rational algebra by a rational number::
+
+            sage: J = RealSymmetricEJA(2)
+            sage: e0,e1,e2 = J.gens()
+            sage: x = 2*e0 + 4*e1 + 16*e2
+            sage: x.operator()
+            Linear operator between finite-dimensional Euclidean Jordan algebras
+            represented by the matrix:
+            [ 2  4  0]
+            [ 2  9  2]
+            [ 0  4 16]
+            Domain: Euclidean Jordan algebra of degree 3 over Rational Field
+            Codomain: Euclidean Jordan algebra of degree 3 over Rational Field
+            sage: x.operator()*(1/2)
+            Linear operator between finite-dimensional Euclidean Jordan algebras
+            represented by the matrix:
+            [  1   2   0]
+            [  1 9/2   1]
+            [  0   2   8]
+            Domain: Euclidean Jordan algebra of degree 3 over Rational Field
+            Codomain: Euclidean Jordan algebra of degree 3 over Rational Field
+
+        """
+        if other in self.codomain().base_ring():
+            return FiniteDimensionalEuclideanJordanAlgebraOperator(
+                self.domain(),
+                self.codomain(),
+                self.matrix()*other)
+
+        # This should eventually delegate to _composition_ after performing
+        # some sanity checks for us.
+        mor = super(FiniteDimensionalEuclideanJordanAlgebraOperator,self)
+        return mor.__mul__(other)
+
+
+    def _neg_(self):
+        """
+        Negate this EJA operator.
+
+        EXAMPLES::
+
+            sage: J = RealSymmetricEJA(2)
+            sage: id = identity_matrix(J.base_ring(), J.dimension())
+            sage: f = FiniteDimensionalEuclideanJordanAlgebraOperator(J,J,id)
+            sage: -f
+            Linear operator between finite-dimensional Euclidean Jordan
+            algebras represented by the matrix:
+            [-1  0  0]
+            [ 0 -1  0]
+            [ 0  0 -1]
+            Domain: Euclidean Jordan algebra of degree 3 over Rational Field
+            Codomain: Euclidean Jordan algebra of degree 3 over Rational Field
+
+        """
+        return FiniteDimensionalEuclideanJordanAlgebraOperator(
+                self.domain(),
+                self.codomain(),
+                -self.matrix())
+
+
+    def __pow__(self, n):
+        """
+        Raise this EJA operator to the power ``n``.
+
+        TESTS:
+
+        Ensure that we get back another EJA operator that can be added,
+        subtracted, et cetera::
+
+            sage: J = RealSymmetricEJA(2)
+            sage: id = identity_matrix(J.base_ring(), J.dimension())
+            sage: f = FiniteDimensionalEuclideanJordanAlgebraOperator(J,J,id)
+            sage: f^0 + f^1 + f^2
+            Linear operator between finite-dimensional Euclidean Jordan
+            algebras represented by the matrix:
+            [3 0 0]
+            [0 3 0]
+            [0 0 3]
+            Domain: Euclidean Jordan algebra of degree 3 over Rational Field
+            Codomain: Euclidean Jordan algebra of degree 3 over Rational Field
+
+        """
+        if (n == 1):
+            return self
+        elif (n == 0):
+            # Raising a vector space morphism to the zero power gives
+            # you back a special IdentityMorphism that is useless to us.
+            rows = self.codomain().dimension()
+            cols = self.domain().dimension()
+            mat = matrix.identity(self.base_ring(), rows, cols)
+        else:
+            mat = self.matrix()**n
+
+        return FiniteDimensionalEuclideanJordanAlgebraOperator(
+                 self.domain(),
+                 self.codomain(),
+                 mat)
+
+
+    def _repr_(self):
+        r"""
+
+        A text representation of this linear operator on a Euclidean
+        Jordan Algebra.
+
+        EXAMPLES::
+
+            sage: J = JordanSpinEJA(2)
+            sage: id = identity_matrix(J.base_ring(), J.dimension())
+            sage: FiniteDimensionalEuclideanJordanAlgebraOperator(J,J,id)
+            Linear operator between finite-dimensional Euclidean Jordan
+            algebras represented by the matrix:
+            [1 0]
+            [0 1]
+            Domain: Euclidean Jordan algebra of degree 2 over Rational Field
+            Codomain: Euclidean Jordan algebra of degree 2 over Rational Field
+
+        """
+        msg = ("Linear operator between finite-dimensional Euclidean Jordan "
+                "algebras represented by the matrix:\n",
+               "{!r}\n",
+               "Domain: {}\n",
+               "Codomain: {}")
+        return ''.join(msg).format(self.matrix(),
+                                   self.domain(),
+                                   self.codomain())
+
+
+    def _sub_(self, other):
+        """
+        Subtract ``other`` from this EJA operator.
+
+        EXAMPLES::
+
+            sage: J = RealSymmetricEJA(2)
+            sage: id = identity_matrix(J.base_ring(),J.dimension())
+            sage: f = FiniteDimensionalEuclideanJordanAlgebraOperator(J,J,id)
+            sage: f - (f*2)
+            Linear operator between finite-dimensional Euclidean Jordan
+            algebras represented by the matrix:
+            [-1  0  0]
+            [ 0 -1  0]
+            [ 0  0 -1]
+            Domain: Euclidean Jordan algebra of degree 3 over Rational Field
+            Codomain: Euclidean Jordan algebra of degree 3 over Rational Field
+
+        """
+        return (self + (-other))
+
+
+    def matrix(self):
+        """
+        Return the matrix representation of this operator with respect
+        to the default bases of its (co)domain.
+
+        EXAMPLES::
+
+            sage: J = RealSymmetricEJA(2)
+            sage: mat = matrix(J.base_ring(), J.dimension(), range(9))
+            sage: f = FiniteDimensionalEuclideanJordanAlgebraOperator(J,J,mat)
+            sage: f.matrix()
+            [0 1 2]
+            [3 4 5]
+            [6 7 8]
+
+        """
+        return self._matrix
+
+
 class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
     @staticmethod
     def __classcall_private__(cls,
@@ -21,8 +357,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
                               assume_associative=False,
                               category=None,
                               rank=None,
-                              natural_basis=None,
-                              inner_product=None):
+                              natural_basis=None):
         n = len(mult_table)
         mult_table = [b.base_extend(field) for b in mult_table]
         for b in mult_table:
@@ -31,7 +366,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
                 raise ValueError("input is not a multiplication table")
         mult_table = tuple(mult_table)
 
-        cat = MagmaticAlgebras(field).FiniteDimensional().WithBasis()
+        cat = FiniteDimensionalAlgebrasWithBasis(field)
         cat.or_subcategory(category)
         if assume_associative:
             cat = cat.Associative()
@@ -46,18 +381,17 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
                                  names=names,
                                  category=cat,
                                  rank=rank,
-                                 natural_basis=natural_basis,
-                                 inner_product=inner_product)
+                                 natural_basis=natural_basis)
 
 
-    def __init__(self, field,
+    def __init__(self,
+                 field,
                  mult_table,
                  names='e',
                  assume_associative=False,
                  category=None,
                  rank=None,
-                 natural_basis=None,
-                 inner_product=None):
+                 natural_basis=None):
         """
         EXAMPLES:
 
@@ -73,7 +407,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
         """
         self._rank = rank
         self._natural_basis = natural_basis
-        self._inner_product = inner_product
+        self._multiplication_table = mult_table
         fda = super(FiniteDimensionalEuclideanJordanAlgebra, self)
         fda.__init__(field,
                      mult_table,
@@ -89,18 +423,200 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
         return fmt.format(self.degree(), self.base_ring())
 
 
+    def _a_regular_element(self):
+        """
+        Guess a regular element. Needed to compute the basis for our
+        characteristic polynomial coefficients.
+        """
+        gs = self.gens()
+        z = self.sum( (i+1)*gs[i] for i in range(len(gs)) )
+        if not z.is_regular():
+            raise ValueError("don't know a regular element")
+        return z
+
+
+    @cached_method
+    def _charpoly_basis_space(self):
+        """
+        Return the vector space spanned by the basis used in our
+        characteristic polynomial coefficients. This is used not only to
+        compute those coefficients, but also any time we need to
+        evaluate the coefficients (like when we compute the trace or
+        determinant).
+        """
+        z = self._a_regular_element()
+        V = self.vector_space()
+        V1 = V.span_of_basis( (z**k).vector() for k in range(self.rank()) )
+        b =  (V1.basis() + V1.complement().basis())
+        return V.span_of_basis(b)
+
+
+    @cached_method
+    def _charpoly_coeff(self, i):
+        """
+        Return the coefficient polynomial "a_{i}" of this algebra's
+        general characteristic polynomial.
+
+        Having this be a separate cached method lets us compute and
+        store the trace/determinant (a_{r-1} and a_{0} respectively)
+        separate from the entire characteristic polynomial.
+        """
+        (A_of_x, x, xr, detA) = self._charpoly_matrix_system()
+        R = A_of_x.base_ring()
+        if i >= self.rank():
+            # Guaranteed by theory
+            return R.zero()
+
+        # Danger: the in-place modification is done for performance
+        # reasons (reconstructing a matrix with huge polynomial
+        # entries is slow), but I don't know how cached_method works,
+        # so it's highly possible that we're modifying some global
+        # list variable by reference, here. In other words, you
+        # probably shouldn't call this method twice on the same
+        # algebra, at the same time, in two threads
+        Ai_orig = A_of_x.column(i)
+        A_of_x.set_column(i,xr)
+        numerator = A_of_x.det()
+        A_of_x.set_column(i,Ai_orig)
+
+        # We're relying on the theory here to ensure that each a_i is
+        # indeed back in R, and the added negative signs are to make
+        # the whole charpoly expression sum to zero.
+        return R(-numerator/detA)
+
+
+    @cached_method
+    def _charpoly_matrix_system(self):
+        """
+        Compute the matrix whose entries A_ij are polynomials in
+        X1,...,XN, the vector ``x`` of variables X1,...,XN, the vector
+        corresponding to `x^r` and the determinent of the matrix A =
+        [A_ij]. In other words, all of the fixed (cachable) data needed
+        to compute the coefficients of the characteristic polynomial.
+        """
+        r = self.rank()
+        n = self.dimension()
+
+        # Construct a new algebra over a multivariate polynomial ring...
+        names = ['X' + str(i) for i in range(1,n+1)]
+        R = PolynomialRing(self.base_ring(), names)
+        J = FiniteDimensionalEuclideanJordanAlgebra(R,
+                                                    self._multiplication_table,
+                                                    rank=r)
+
+        idmat = identity_matrix(J.base_ring(), n)
+
+        W = self._charpoly_basis_space()
+        W = W.change_ring(R.fraction_field())
+
+        # Starting with the standard coordinates x = (X1,X2,...,Xn)
+        # and then converting the entries to W-coordinates allows us
+        # to pass in the standard coordinates to the charpoly and get
+        # back the right answer. Specifically, with x = (X1,X2,...,Xn),
+        # we have
+        #
+        #   W.coordinates(x^2) eval'd at (standard z-coords)
+        #     =
+        #   W-coords of (z^2)
+        #     =
+        #   W-coords of (standard coords of x^2 eval'd at std-coords of z)
+        #
+        # 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(vector(R, R.gens()))
+        l1 = [column_matrix(W.coordinates((x**k).vector())) for k in range(r)]
+        l2 = [idmat.column(k-1).column() for k in range(r+1, n+1)]
+        A_of_x = block_matrix(R, 1, n, (l1 + l2))
+        xr = W.coordinates((x**r).vector())
+        return (A_of_x, x, xr, A_of_x.det())
+
+
+    @cached_method
+    def characteristic_polynomial(self):
+        """
+
+        .. WARNING::
+
+            This implementation doesn't guarantee that the polynomial
+            denominator in the coefficients is not identically zero, so
+            theoretically it could crash. The way that this is handled
+            in e.g. Faraut and Koranyi is to use a basis that guarantees
+            the denominator is non-zero. But, doing so requires knowledge
+            of at least one regular element, and we don't even know how
+            to do that. The trade-off is that, if we use the standard basis,
+            the resulting polynomial will accept the "usual" coordinates. In
+            other words, we don't have to do a change of basis before e.g.
+            computing the trace or determinant.
+
+        EXAMPLES:
+
+        The characteristic polynomial in the spin algebra is given in
+        Alizadeh, Example 11.11::
+
+            sage: J = JordanSpinEJA(3)
+            sage: p = J.characteristic_polynomial(); p
+            X1^2 - X2^2 - X3^2 + (-2*t)*X1 + t^2
+            sage: xvec = J.one().vector()
+            sage: p(*xvec)
+            t^2 - 2*t + 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) ]
+
+        # We go to a bit of trouble here to reorder the
+        # indeterminates, so that it's easier to evaluate the
+        # characteristic polynomial at x's coordinates and get back
+        # something in terms of t, which is what we want.
+        R = a[0].parent()
+        S = PolynomialRing(self.base_ring(),'t')
+        t = S.gen(0)
+        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)) )
+
+
     def inner_product(self, x, y):
         """
         The inner product associated with this Euclidean Jordan algebra.
 
-        Will default to the trace inner product if nothing else.
+        Defaults to the trace inner product, but can be overridden by
+        subclasses if they are sure that the necessary properties are
+        satisfied.
+
+        EXAMPLES:
+
+        The inner product must satisfy its axiom for this algebra to truly
+        be a Euclidean Jordan Algebra::
+
+            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).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")
-        if self._inner_product is None:
-            return x.trace_inner_product(y)
-        else:
-            return self._inner_product(x,y)
+        return x.trace_inner_product(y)
 
 
     def natural_basis(self):
@@ -118,7 +634,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
 
         EXAMPLES::
 
-            sage: J = RealSymmetricSimpleEJA(2)
+            sage: J = RealSymmetricEJA(2)
             sage: J.basis()
             Family (e0, e1, e2)
             sage: J.natural_basis()
@@ -129,7 +645,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
 
         ::
 
-            sage: J = JordanSpinSimpleEJA(2)
+            sage: J = JordanSpinEJA(2)
             sage: J.basis()
             Family (e0, e1)
             sage: J.natural_basis()
@@ -154,26 +670,49 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
         else:
             return self._rank
 
+    def vector_space(self):
+        """
+        Return the vector space that underlies this algebra.
+
+        EXAMPLES::
+
+            sage: J = RealSymmetricEJA(2)
+            sage: J.vector_space()
+            Vector space of dimension 3 over Rational Field
+
+        """
+        return self.zero().vector().parent().ambient_vector_space()
+
 
     class Element(FiniteDimensionalAlgebraElement):
         """
         An element of a Euclidean Jordan algebra.
         """
 
+        def __dir__(self):
+            """
+            Oh man, I should not be doing this. This hides the "disabled"
+            methods ``left_matrix`` and ``matrix`` from introspection;
+            in particular it removes them from tab-completion.
+            """
+            return filter(lambda s: s not in ['left_matrix', 'matrix'],
+                          dir(self.__class__) )
+
+
         def __init__(self, A, elt=None):
             """
             EXAMPLES:
 
             The identity in `S^n` is converted to the identity in the EJA::
 
-                sage: J = RealSymmetricSimpleEJA(3)
+                sage: J = RealSymmetricEJA(3)
                 sage: I = identity_matrix(QQ,3)
                 sage: J(I) == J.one()
                 True
 
             This skew-symmetric matrix can't be represented in the EJA::
 
-                sage: J = RealSymmetricSimpleEJA(3)
+                sage: J = RealSymmetricEJA(3)
                 sage: A = matrix(QQ,3, lambda i,j: i-j)
                 sage: J(A)
                 Traceback (most recent call last):
@@ -222,7 +761,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
 
                 sage: set_random_seed()
                 sage: x = random_eja().random_element()
-                sage: x.operator_matrix()*x.vector() == (x^2).vector()
+                sage: x.operator()(x) == (x^2)
                 True
 
             A few examples of power-associativity::
@@ -241,34 +780,96 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
                 sage: x = random_eja().random_element()
                 sage: m = ZZ.random_element(0,10)
                 sage: n = ZZ.random_element(0,10)
-                sage: Lxm = (x^m).operator_matrix()
-                sage: Lxn = (x^n).operator_matrix()
+                sage: Lxm = (x^m).operator()
+                sage: Lxn = (x^n).operator()
                 sage: Lxm*Lxn == Lxn*Lxm
                 True
 
             """
-            A = self.parent()
             if n == 0:
-                return A.one()
+                return self.parent().one()
             elif n == 1:
                 return self
             else:
-                return A( (self.operator_matrix()**(n-1))*self.vector() )
+                return (self.operator()**(n-1))(self)
+
+
+        def apply_univariate_polynomial(self, p):
+            """
+            Apply the univariate polynomial ``p`` to this element.
+
+            A priori, SageMath won't allow us to apply a univariate
+            polynomial to an element of an EJA, because we don't know
+            that EJAs are rings (they are usually not associative). Of
+            course, we know that EJAs are power-associative, so the
+            operation is ultimately kosher. This function sidesteps
+            the CAS to get the answer we want and expect.
+
+            EXAMPLES::
+
+                sage: R = PolynomialRing(QQ, 't')
+                sage: t = R.gen(0)
+                sage: p = t^4 - t^3 + 5*t - 2
+                sage: J = RealCartesianProductEJA(5)
+                sage: J.one().apply_univariate_polynomial(p) == 3*J.one()
+                True
+
+            TESTS:
+
+            We should always get back an element of the algebra::
+
+                sage: set_random_seed()
+                sage: p = PolynomialRing(QQ, 't').random_element()
+                sage: J = random_eja()
+                sage: x = J.random_element()
+                sage: x.apply_univariate_polynomial(p) in J
+                True
+
+            """
+            if len(p.variables()) > 1:
+                raise ValueError("not a univariate polynomial")
+            P = self.parent()
+            R = P.base_ring()
+            # Convert the coeficcients to the parent's base ring,
+            # because a priori they might live in an (unnecessarily)
+            # larger ring for which P.sum() would fail below.
+            cs = [ R(c) for c in p.coefficients(sparse=False) ]
+            return P.sum( cs[k]*(self**k) for k in range(len(cs)) )
 
 
         def characteristic_polynomial(self):
             """
-            Return my characteristic polynomial (if I'm a regular
-            element).
+            Return the characteristic polynomial of this element.
+
+            EXAMPLES:
+
+            The rank of `R^3` is three, and the minimal polynomial of
+            the identity element is `(t-1)` from which it follows that
+            the characteristic polynomial should be `(t-1)^3`::
+
+                sage: J = RealCartesianProductEJA(3)
+                sage: J.one().characteristic_polynomial()
+                t^3 - 3*t^2 + 3*t - 1
+
+            Likewise, the characteristic of the zero element in the
+            rank-three algebra `R^{n}` should be `t^{3}`::
+
+                sage: J = RealCartesianProductEJA(3)
+                sage: J.zero().characteristic_polynomial()
+                t^3
+
+            The characteristic polynomial of an element should evaluate
+            to zero on that element::
+
+                sage: set_random_seed()
+                sage: x = RealCartesianProductEJA(3).random_element()
+                sage: p = x.characteristic_polynomial()
+                sage: x.apply_univariate_polynomial(p)
+                0
 
-            Eventually this should be implemented in terms of the parent
-            algebra's characteristic polynomial that works for ALL
-            elements.
             """
-            if self.is_regular():
-                return self.minimal_polynomial()
-            else:
-                raise NotImplementedError('irregular element')
+            p = self.parent().characteristic_polynomial()
+            return p(*self.vector())
 
 
         def inner_product(self, other):
@@ -281,7 +882,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
             inner product on `R^n` (this example only works because the
             basis for the Jordan algebra is the standard basis in `R^n`)::
 
-                sage: J = JordanSpinSimpleEJA(3)
+                sage: J = JordanSpinEJA(3)
                 sage: x = vector(QQ,[1,2,3])
                 sage: y = vector(QQ,[4,5,6])
                 sage: x.inner_product(y)
@@ -294,7 +895,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
             so the inner product of the identity matrix with itself
             should be the `n`::
 
-                sage: J = RealSymmetricSimpleEJA(3)
+                sage: J = RealSymmetricEJA(3)
                 sage: J.one().inner_product(J.one())
                 3
 
@@ -303,7 +904,13 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
             part because the product of Hermitian matrices may not be
             Hermitian::
 
-                sage: J = ComplexHermitianSimpleEJA(3)
+                sage: J = ComplexHermitianEJA(3)
+                sage: J.one().inner_product(J.one())
+                3
+
+            Ditto for the quaternions::
+
+                sage: J = QuaternionHermitianEJA(3)
                 sage: J.one().inner_product(J.one())
                 3
 
@@ -355,12 +962,63 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
                 sage: lhs == rhs
                 True
 
+            Test the first polarization identity from my notes, Koecher Chapter
+            III, or from Baes (2.3)::
+
+                sage: set_random_seed()
+                sage: J = random_eja()
+                sage: x = J.random_element()
+                sage: y = J.random_element()
+                sage: Lx = x.operator()
+                sage: Ly = y.operator()
+                sage: Lxx = (x*x).operator()
+                sage: Lxy = (x*y).operator()
+                sage: bool(2*Lx*Lxy + Ly*Lxx == 2*Lxy*Lx + Lxx*Ly)
+                True
+
+            Test the second polarization identity from my notes or from
+            Baes (2.4)::
+
+                sage: set_random_seed()
+                sage: J = random_eja()
+                sage: x = J.random_element()
+                sage: y = J.random_element()
+                sage: z = J.random_element()
+                sage: Lx = x.operator()
+                sage: Ly = y.operator()
+                sage: Lz = z.operator()
+                sage: Lzy = (z*y).operator()
+                sage: Lxy = (x*y).operator()
+                sage: Lxz = (x*z).operator()
+                sage: bool(Lx*Lzy + Lz*Lxy + Ly*Lxz == Lzy*Lx + Lxy*Lz + Lxz*Ly)
+                True
+
+            Test the third polarization identity from my notes or from
+            Baes (2.5)::
+
+                sage: set_random_seed()
+                sage: J = random_eja()
+                sage: u = J.random_element()
+                sage: y = J.random_element()
+                sage: z = J.random_element()
+                sage: Lu = u.operator()
+                sage: Ly = y.operator()
+                sage: Lz = z.operator()
+                sage: Lzy = (z*y).operator()
+                sage: Luy = (u*y).operator()
+                sage: Luz = (u*z).operator()
+                sage: Luyz = (u*(y*z)).operator()
+                sage: lhs = Lu*Lzy + Lz*Luy + Ly*Luz
+                sage: rhs = Luyz + Ly*Lu*Lz + Lz*Lu*Ly
+                sage: bool(lhs == rhs)
+                True
+
             """
             if not other in self.parent():
                 raise TypeError("'other' must live in the same algebra")
 
-            A = self.operator_matrix()
-            B = other.operator_matrix()
+            A = self.operator()
+            B = other.operator()
             return (A*B == B*A)
 
 
@@ -370,32 +1028,49 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
 
             EXAMPLES::
 
-                sage: J = JordanSpinSimpleEJA(2)
+                sage: J = JordanSpinEJA(2)
                 sage: e0,e1 = J.gens()
-                sage: x = e0 + e1
+                sage: x = sum( J.gens() )
                 sage: x.det()
                 0
-                sage: J = JordanSpinSimpleEJA(3)
+
+            ::
+
+                sage: J = JordanSpinEJA(3)
                 sage: e0,e1,e2 = J.gens()
-                sage: x = e0 + e1 + e2
+                sage: x = sum( J.gens() )
                 sage: x.det()
                 -1
 
+            TESTS:
+
+            An element is invertible if and only if its determinant is
+            non-zero::
+
+                sage: set_random_seed()
+                sage: x = random_eja().random_element()
+                sage: x.is_invertible() == (x.det() != 0)
+                True
+
             """
-            cs = self.characteristic_polynomial().coefficients(sparse=False)
-            r = len(cs) - 1
-            if r >= 0:
-                return cs[0] * (-1)**r
-            else:
-                raise ValueError('charpoly had no coefficients')
+            P = self.parent()
+            r = P.rank()
+            p = P._charpoly_coeff(0)
+            # The _charpoly_coeff function already adds the factor of
+            # -1 to ensure that _charpoly_coeff(0) is really what
+            # appears in front of t^{0} in the charpoly. However,
+            # we want (-1)^r times THAT for the determinant.
+            return ((-1)**r)*p(*self.vector())
 
 
         def inverse(self):
             """
             Return the Jordan-multiplicative inverse of this element.
 
-            We can't use the superclass method because it relies on the
-            algebra being associative.
+            ALGORITHM:
+
+            We appeal to the quadratic representation as in Koecher's
+            Theorem 12 in Chapter III, Section 5.
 
             EXAMPLES:
 
@@ -404,14 +1079,14 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
 
                 sage: set_random_seed()
                 sage: n = ZZ.random_element(1,10)
-                sage: J = JordanSpinSimpleEJA(n)
+                sage: J = JordanSpinEJA(n)
                 sage: x = J.random_element()
-                sage: while x.is_zero():
+                sage: while not x.is_invertible():
                 ....:     x = J.random_element()
                 sage: x_vec = x.vector()
                 sage: x0 = x_vec[0]
                 sage: x_bar = x_vec[1:]
-                sage: coeff = 1/(x0^2 - x_bar.inner_product(x_bar))
+                sage: coeff = ~(x0^2 - x_bar.inner_product(x_bar))
                 sage: inv_vec = x_vec.parent()([x0] + (-x_bar).list())
                 sage: x_inverse = coeff*inv_vec
                 sage: x.inverse() == J(x_inverse)
@@ -426,48 +1101,35 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
                 sage: J.one().inverse() == J.one()
                 True
 
-            If an element has an inverse, it acts like one. TODO: this
-            can be a lot less ugly once ``is_invertible`` doesn't crash
-            on irregular elements::
+            If an element has an inverse, it acts like one::
 
                 sage: set_random_seed()
                 sage: J = random_eja()
                 sage: x = J.random_element()
-                sage: try:
-                ....:     x.inverse()*x == J.one()
-                ....: except:
-                ....:     True
+                sage: (not x.is_invertible()) or (x.inverse()*x == J.one())
                 True
 
-            """
-            if self.parent().is_associative():
-                elt = FiniteDimensionalAlgebraElement(self.parent(), self)
-                return elt.inverse()
+            The inverse of the inverse is what we started with::
 
-            # TODO: we can do better once the call to is_invertible()
-            # doesn't crash on irregular elements.
-            #if not self.is_invertible():
-            #    raise ValueError('element is not invertible')
+                sage: set_random_seed()
+                sage: J = random_eja()
+                sage: x = J.random_element()
+                sage: (not x.is_invertible()) or (x.inverse().inverse() == x)
+                True
 
-            # We do this a little different than the usual recursive
-            # call to a finite-dimensional algebra element, because we
-            # wind up with an inverse that lives in the subalgebra and
-            # we need information about the parent to convert it back.
-            V = self.span_of_powers()
-            assoc_subalg = self.subalgebra_generated_by()
-            # Mis-design warning: the basis used for span_of_powers()
-            # and subalgebra_generated_by() must be the same, and in
-            # the same order!
-            elt = assoc_subalg(V.coordinates(self.vector()))
+            The zero element is never invertible::
 
-            # This will be in the subalgebra's coordinates...
-            fda_elt = FiniteDimensionalAlgebraElement(assoc_subalg, elt)
-            subalg_inverse = fda_elt.inverse()
+                sage: set_random_seed()
+                sage: J = random_eja().zero().inverse()
+                Traceback (most recent call last):
+                ...
+                ValueError: element is not invertible
 
-            # So we have to convert back...
-            basis = [ self.parent(v) for v in V.basis() ]
-            pairs = zip(subalg_inverse.vector(), basis)
-            return self.parent().linear_combination(pairs)
+            """
+            if not self.is_invertible():
+                raise ValueError("element is not invertible")
+
+            return (~self.quadratic_representation())(self)
 
 
         def is_invertible(self):
@@ -476,8 +1138,36 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
 
             We can't use the superclass method because it relies on
             the algebra being associative.
+
+            ALGORITHM:
+
+            The usual way to do this is to check if the determinant is
+            zero, but we need the characteristic polynomial for the
+            determinant. The minimal polynomial is a lot easier to get,
+            so we use Corollary 2 in Chapter V of Koecher to check
+            whether or not the paren't algebra's zero element is a root
+            of this element's minimal polynomial.
+
+            TESTS:
+
+            The identity element is always invertible::
+
+                sage: set_random_seed()
+                sage: J = random_eja()
+                sage: J.one().is_invertible()
+                True
+
+            The zero element is never invertible::
+
+                sage: set_random_seed()
+                sage: J = random_eja()
+                sage: J.zero().is_invertible()
+                False
+
             """
-            return not self.det().is_zero()
+            zero = self.parent().zero()
+            p = self.minimal_polynomial()
+            return not (p(zero) == zero)
 
 
         def is_nilpotent(self):
@@ -534,7 +1224,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
             The identity element always has degree one, but any element
             linearly-independent from it is regular::
 
-                sage: J = JordanSpinSimpleEJA(5)
+                sage: J = JordanSpinEJA(5)
                 sage: J.one().is_regular()
                 False
                 sage: e0, e1, e2, e3, e4 = J.gens() # e0 is the identity
@@ -559,7 +1249,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
 
             EXAMPLES::
 
-                sage: J = JordanSpinSimpleEJA(4)
+                sage: J = JordanSpinEJA(4)
                 sage: J.one().degree()
                 1
                 sage: e0,e1,e2,e3 = J.gens()
@@ -571,7 +1261,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
 
                 sage: set_random_seed()
                 sage: n = ZZ.random_element(1,10)
-                sage: J = JordanSpinSimpleEJA(n)
+                sage: J = JordanSpinEJA(n)
                 sage: x = J.random_element()
                 sage: x == x.coefficient(0)*J.one() or x.degree() == 2
                 True
@@ -580,16 +1270,42 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
             return self.span_of_powers().dimension()
 
 
+        def left_matrix(self):
+            """
+            Our parent class defines ``left_matrix`` and ``matrix``
+            methods whose names are misleading. We don't want them.
+            """
+            raise NotImplementedError("use operator_matrix() instead")
+
+        matrix = left_matrix
+
+
         def minimal_polynomial(self):
             """
-            EXAMPLES::
+            Return the minimal polynomial of this element,
+            as a function of the variable `t`.
+
+            ALGORITHM:
+
+            We restrict ourselves to the associative subalgebra
+            generated by this element, and then return the minimal
+            polynomial of this element's operator matrix (in that
+            subalgebra). This works by Baes Proposition 2.3.16.
+
+            TESTS:
+
+            The minimal polynomial of the identity and zero elements are
+            always the same::
 
                 sage: set_random_seed()
-                sage: x = random_eja().random_element()
-                sage: x.degree() == x.minimal_polynomial().degree()
-                True
+                sage: J = random_eja()
+                sage: J.one().minimal_polynomial()
+                t - 1
+                sage: J.zero().minimal_polynomial()
+                t
 
-            ::
+            The degree of an element is (by one definition) the degree
+            of its minimal polynomial::
 
                 sage: set_random_seed()
                 sage: x = random_eja().random_element()
@@ -603,38 +1319,38 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
 
                 sage: set_random_seed()
                 sage: n = ZZ.random_element(2,10)
-                sage: J = JordanSpinSimpleEJA(n)
+                sage: J = JordanSpinEJA(n)
                 sage: y = J.random_element()
                 sage: while y == y.coefficient(0)*J.one():
                 ....:     y = J.random_element()
                 sage: y0 = y.vector()[0]
                 sage: y_bar = y.vector()[1:]
                 sage: actual = y.minimal_polynomial()
-                sage: x = SR.symbol('x', domain='real')
-                sage: expected = x^2 - 2*y0*x + (y0^2 - norm(y_bar)^2)
+                sage: t = PolynomialRing(J.base_ring(),'t').gen(0)
+                sage: expected = t^2 - 2*y0*t + (y0^2 - norm(y_bar)^2)
                 sage: bool(actual == expected)
                 True
 
-            """
-            # The element we're going to call "minimal_polynomial()" on.
-            # Either myself, interpreted as an element of a finite-
-            # dimensional algebra, or an element of an associative
-            # subalgebra.
-            elt = None
+            The minimal polynomial should always kill its element::
 
-            if self.parent().is_associative():
-                elt = FiniteDimensionalAlgebraElement(self.parent(), self)
-            else:
-                V = self.span_of_powers()
-                assoc_subalg = self.subalgebra_generated_by()
-                # Mis-design warning: the basis used for span_of_powers()
-                # and subalgebra_generated_by() must be the same, and in
-                # the same order!
-                elt = assoc_subalg(V.coordinates(self.vector()))
+                sage: set_random_seed()
+                sage: x = random_eja().random_element()
+                sage: p = x.minimal_polynomial()
+                sage: x.apply_univariate_polynomial(p)
+                0
 
-            # Recursive call, but should work since elt lives in an
-            # associative algebra.
-            return elt.minimal_polynomial()
+            """
+            V = self.span_of_powers()
+            assoc_subalg = self.subalgebra_generated_by()
+            # Mis-design warning: the basis used for span_of_powers()
+            # and subalgebra_generated_by() must be the same, and in
+            # the same order!
+            elt = assoc_subalg(V.coordinates(self.vector()))
+
+            # We get back a symbolic polynomial in 'x' but want a real
+            # polynomial in 't'.
+            p_of_x = elt.operator_matrix().minimal_polynomial()
+            return p_of_x.change_variable_name('t')
 
 
         def natural_representation(self):
@@ -649,7 +1365,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
 
             EXAMPLES::
 
-                sage: J = ComplexHermitianSimpleEJA(3)
+                sage: J = ComplexHermitianEJA(3)
                 sage: J.one()
                 e0 + e5 + e8
                 sage: J.one().natural_representation()
@@ -660,72 +1376,72 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
                 [0 0 0 0 1 0]
                 [0 0 0 0 0 1]
 
+            ::
+
+                sage: J = QuaternionHermitianEJA(3)
+                sage: J.one()
+                e0 + e9 + e14
+                sage: J.one().natural_representation()
+                [1 0 0 0 0 0 0 0 0 0 0 0]
+                [0 1 0 0 0 0 0 0 0 0 0 0]
+                [0 0 1 0 0 0 0 0 0 0 0 0]
+                [0 0 0 1 0 0 0 0 0 0 0 0]
+                [0 0 0 0 1 0 0 0 0 0 0 0]
+                [0 0 0 0 0 1 0 0 0 0 0 0]
+                [0 0 0 0 0 0 1 0 0 0 0 0]
+                [0 0 0 0 0 0 0 1 0 0 0 0]
+                [0 0 0 0 0 0 0 0 1 0 0 0]
+                [0 0 0 0 0 0 0 0 0 1 0 0]
+                [0 0 0 0 0 0 0 0 0 0 1 0]
+                [0 0 0 0 0 0 0 0 0 0 0 1]
+
             """
             B = self.parent().natural_basis()
             W = B[0].matrix_space()
             return W.linear_combination(zip(self.vector(), B))
 
 
-        def operator_matrix(self):
+        def operator(self):
             """
-            Return the matrix that represents left- (or right-)
-            multiplication by this element in the parent algebra.
-
-            We have to override this because the superclass method
-            returns a matrix that acts on row vectors (that is, on
-            the right).
-
-            EXAMPLES:
+            Return the left-multiplication-by-this-element
+            operator on the ambient algebra.
 
-            Test the first polarization identity from my notes, Koecher Chapter
-            III, or from Baes (2.3)::
+            TESTS::
 
                 sage: set_random_seed()
                 sage: J = random_eja()
                 sage: x = J.random_element()
                 sage: y = J.random_element()
-                sage: Lx = x.operator_matrix()
-                sage: Ly = y.operator_matrix()
-                sage: Lxx = (x*x).operator_matrix()
-                sage: Lxy = (x*y).operator_matrix()
-                sage: bool(2*Lx*Lxy + Ly*Lxx == 2*Lxy*Lx + Lxx*Ly)
+                sage: x.operator()(y) == x*y
+                True
+                sage: y.operator()(x) == x*y
                 True
 
-            Test the second polarization identity from my notes or from
-            Baes (2.4)::
+            """
+            P = self.parent()
+            return FiniteDimensionalEuclideanJordanAlgebraOperator(
+                     P,P,
+                     self.operator_matrix() )
 
-                sage: set_random_seed()
-                sage: J = random_eja()
-                sage: x = J.random_element()
-                sage: y = J.random_element()
-                sage: z = J.random_element()
-                sage: Lx = x.operator_matrix()
-                sage: Ly = y.operator_matrix()
-                sage: Lz = z.operator_matrix()
-                sage: Lzy = (z*y).operator_matrix()
-                sage: Lxy = (x*y).operator_matrix()
-                sage: Lxz = (x*z).operator_matrix()
-                sage: bool(Lx*Lzy + Lz*Lxy + Ly*Lxz == Lzy*Lx + Lxy*Lz + Lxz*Ly)
-                True
 
-            Test the third polarization identity from my notes or from
-            Baes (2.5)::
+
+        def operator_matrix(self):
+            """
+            Return the matrix that represents left- (or right-)
+            multiplication by this element in the parent algebra.
+
+            We implement this ourselves to work around the fact that
+            our parent class represents everything with row vectors.
+
+            EXAMPLES:
+
+            Ensure that our operator's ``matrix`` method agrees with
+            this implementation::
 
                 sage: set_random_seed()
                 sage: J = random_eja()
-                sage: u = J.random_element()
-                sage: y = J.random_element()
-                sage: z = J.random_element()
-                sage: Lu = u.operator_matrix()
-                sage: Ly = y.operator_matrix()
-                sage: Lz = z.operator_matrix()
-                sage: Lzy = (z*y).operator_matrix()
-                sage: Luy = (u*y).operator_matrix()
-                sage: Luz = (u*z).operator_matrix()
-                sage: Luyz = (u*(y*z)).operator_matrix()
-                sage: lhs = Lu*Lzy + Lz*Luy + Ly*Luz
-                sage: rhs = Luyz + Ly*Lu*Lz + Lz*Lu*Ly
-                sage: bool(lhs == rhs)
+                sage: x = J.random_element()
+                sage: x.operator().matrix() == x.operator_matrix()
                 True
 
             """
@@ -744,7 +1460,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
 
                 sage: set_random_seed()
                 sage: n = ZZ.random_element(1,10)
-                sage: J = JordanSpinSimpleEJA(n)
+                sage: J = JordanSpinEJA(n)
                 sage: x = J.random_element()
                 sage: x_vec = x.vector()
                 sage: x0 = x_vec[0]
@@ -756,7 +1472,7 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
                 sage: D = (x0^2 - x_bar.inner_product(x_bar))*D
                 sage: D = D + 2*x_bar.tensor_product(x_bar)
                 sage: Q = block_matrix(2,2,[A,B,C,D])
-                sage: Q == x.quadratic_representation()
+                sage: Q == x.quadratic_representation().matrix()
                 True
 
             Test all of the properties from Theorem 11.2 in Alizadeh::
@@ -765,38 +1481,76 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
                 sage: J = random_eja()
                 sage: x = J.random_element()
                 sage: y = J.random_element()
+                sage: Lx = x.operator()
+                sage: Lxx = (x*x).operator()
+                sage: Qx = x.quadratic_representation()
+                sage: Qy = y.quadratic_representation()
+                sage: Qxy = x.quadratic_representation(y)
+                sage: Qex = J.one().quadratic_representation(x)
+                sage: n = ZZ.random_element(10)
+                sage: Qxn = (x^n).quadratic_representation()
 
             Property 1:
 
-                sage: actual = x.quadratic_representation(y)
-                sage: expected = ( (x+y).quadratic_representation()
-                ....:              -x.quadratic_representation()
-                ....:              -y.quadratic_representation() ) / 2
-                sage: actual == expected
+                sage: 2*Qxy == (x+y).quadratic_representation() - Qx - Qy
                 True
 
-            Property 2:
+            Property 2 (multiply on the right for :trac:`28272`):
 
                 sage: alpha = QQ.random_element()
-                sage: actual = (alpha*x).quadratic_representation()
-                sage: expected = (alpha^2)*x.quadratic_representation()
-                sage: actual == expected
+                sage: (alpha*x).quadratic_representation() == Qx*(alpha^2)
+                True
+
+            Property 3:
+
+                sage: not x.is_invertible() or ( Qx(x.inverse()) == x )
+                True
+
+                sage: not x.is_invertible() or (
+                ....:   ~Qx
+                ....:   ==
+                ....:   x.inverse().quadratic_representation() )
+                True
+
+                sage: Qxy(J.one()) == x*y
+                True
+
+            Property 4:
+
+                sage: not x.is_invertible() or (
+                ....:   x.quadratic_representation(x.inverse())*Qx
+                ....:   == Qx*x.quadratic_representation(x.inverse()) )
+                True
+
+                sage: not x.is_invertible() or (
+                ....:   x.quadratic_representation(x.inverse())*Qx
+                ....:   ==
+                ....:   2*x.operator()*Qex - Qx )
+                True
+
+                sage: 2*x.operator()*Qex - Qx == Lxx
                 True
 
             Property 5:
 
-                sage: Qy = y.quadratic_representation()
-                sage: actual = J(Qy*x.vector()).quadratic_representation()
-                sage: expected = Qy*x.quadratic_representation()*Qy
-                sage: actual == expected
+                sage: Qy(x).quadratic_representation() == Qy*Qx*Qy
                 True
 
             Property 6:
 
-                sage: k = ZZ.random_element(1,10)
-                sage: actual = (x^k).quadratic_representation()
-                sage: expected = (x.quadratic_representation())^k
-                sage: actual == expected
+                sage: Qxn == (Qx)^n
+                True
+
+            Property 7:
+
+                sage: not x.is_invertible() or (
+                ....:   Qx*x.inverse().operator() == Lx )
+                True
+
+            Property 8:
+
+                sage: not x.operator_commutes_with(y) or (
+                ....:   Qx(y)^n == Qxn(y^n) )
                 True
 
             """
@@ -805,9 +1559,9 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
             elif not other in self.parent():
                 raise TypeError("'other' must live in the same algebra")
 
-            L = self.operator_matrix()
-            M = other.operator_matrix()
-            return ( L*M + M*L - (self*other).operator_matrix() )
+            L = self.operator()
+            M = other.operator()
+            return ( L*M + M*L - (self*other).operator() )
 
 
         def span_of_powers(self):
@@ -818,7 +1572,10 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
             # The dimension of the subalgebra can't be greater than
             # the big algebra, so just put everything into a list
             # and let span() get rid of the excess.
-            V = self.vector().parent()
+            #
+            # We do the extra ambient_vector_space() in case we're messing
+            # with polynomials and the direct parent is a module.
+            V = self.parent().vector_space()
             return V.span( (self**d).vector() for d in xrange(V.dimension()) )
 
 
@@ -887,12 +1644,11 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
             TESTS::
 
                 sage: set_random_seed()
-                sage: J = eja_rn(5)
-                sage: c = J.random_element().subalgebra_idempotent()
-                sage: c^2 == c
-                True
-                sage: J = JordanSpinSimpleEJA(5)
-                sage: c = J.random_element().subalgebra_idempotent()
+                sage: J = random_eja()
+                sage: x = J.random_element()
+                sage: while x.is_nilpotent():
+                ....:     x = J.random_element()
+                sage: c = x.subalgebra_idempotent()
                 sage: c^2 == c
                 True
 
@@ -947,23 +1703,81 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
 
             EXAMPLES::
 
-                sage: J = JordanSpinSimpleEJA(3)
-                sage: e0,e1,e2 = J.gens()
-                sage: x = e0 + e1 + e2
+                sage: J = JordanSpinEJA(3)
+                sage: x = sum(J.gens())
                 sage: x.trace()
                 2
 
+            ::
+
+                sage: J = RealCartesianProductEJA(5)
+                sage: J.one().trace()
+                5
+
+            TESTS:
+
+            The trace of an element is a real number::
+
+                sage: set_random_seed()
+                sage: J = random_eja()
+                sage: J.random_element().trace() in J.base_ring()
+                True
+
             """
-            cs = self.characteristic_polynomial().coefficients(sparse=False)
-            if len(cs) >= 2:
-                return -1*cs[-2]
-            else:
-                raise ValueError('charpoly had fewer than 2 coefficients')
+            P = self.parent()
+            r = P.rank()
+            p = P._charpoly_coeff(r-1)
+            # The _charpoly_coeff function already adds the factor of
+            # -1 to ensure that _charpoly_coeff(r-1) is really what
+            # appears in front of t^{r-1} in the charpoly. However,
+            # we want the negative of THAT for the trace.
+            return -p(*self.vector())
 
 
         def trace_inner_product(self, other):
             """
             Return the trace inner product of myself and ``other``.
+
+            TESTS:
+
+            The trace inner product is commutative::
+
+                sage: set_random_seed()
+                sage: J = random_eja()
+                sage: x = J.random_element(); y = J.random_element()
+                sage: x.trace_inner_product(y) == y.trace_inner_product(x)
+                True
+
+            The trace inner product is bilinear::
+
+                sage: set_random_seed()
+                sage: J = random_eja()
+                sage: x = J.random_element()
+                sage: y = J.random_element()
+                sage: z = J.random_element()
+                sage: a = QQ.random_element();
+                sage: actual = (a*(x+z)).trace_inner_product(y)
+                sage: expected = ( a*x.trace_inner_product(y) +
+                ....:              a*z.trace_inner_product(y) )
+                sage: actual == expected
+                True
+                sage: actual = x.trace_inner_product(a*(y+z))
+                sage: expected = ( a*x.trace_inner_product(y) +
+                ....:              a*x.trace_inner_product(z) )
+                sage: actual == expected
+                True
+
+            The trace inner product satisfies the compatibility
+            condition in the definition of a Euclidean Jordan algebra::
+
+                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).trace_inner_product(z) == y.trace_inner_product(x*z)
+                True
+
             """
             if not other in self.parent():
                 raise TypeError("'other' must live in the same algebra")
@@ -971,16 +1785,20 @@ class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
             return (self*other).trace()
 
 
-def eja_rn(dimension, field=QQ):
+class RealCartesianProductEJA(FiniteDimensionalEuclideanJordanAlgebra):
     """
     Return the Euclidean Jordan Algebra corresponding to the set
     `R^n` under the Hadamard product.
 
+    Note: this is nothing more than the Cartesian product of ``n``
+    copies of the spin algebra. Once Cartesian product algebras
+    are implemented, this can go.
+
     EXAMPLES:
 
     This multiplication table can be verified by hand::
 
-        sage: J = eja_rn(3)
+        sage: J = RealCartesianProductEJA(3)
         sage: e0,e1,e2 = J.gens()
         sage: e0*e0
         e0
@@ -996,19 +1814,21 @@ def eja_rn(dimension, field=QQ):
         e2
 
     """
-    # The FiniteDimensionalAlgebra constructor takes a list of
-    # matrices, the ith representing right multiplication by the ith
-    # basis element in the vector space. So if e_1 = (1,0,0), then
-    # right (Hadamard) multiplication of x by e_1 picks out the first
-    # component of x; and likewise for the ith basis element e_i.
-    Qs = [ matrix(field, dimension, dimension, lambda k,j: 1*(k == j == i))
-           for i in xrange(dimension) ]
-
-    return FiniteDimensionalEuclideanJordanAlgebra(field,
-                                                   Qs,
-                                                   rank=dimension,
-                                                   inner_product=_usual_ip)
+    @staticmethod
+    def __classcall_private__(cls, n, field=QQ):
+        # The FiniteDimensionalAlgebra constructor takes a list of
+        # matrices, the ith representing right multiplication by the ith
+        # basis element in the vector space. So if e_1 = (1,0,0), then
+        # right (Hadamard) multiplication of x by e_1 picks out the first
+        # component of x; and likewise for the ith basis element e_i.
+        Qs = [ matrix(field, n, n, lambda k,j: 1*(k == j == i))
+               for i in xrange(n) ]
+
+        fdeja = super(RealCartesianProductEJA, cls)
+        return fdeja.__classcall_private__(cls, field, Qs, rank=n)
 
+    def inner_product(self, x, y):
+        return _usual_ip(x,y)
 
 
 def random_eja():
@@ -1028,6 +1848,12 @@ def random_eja():
       * The ``n``-by-``n`` rational symmetric matrices with the symmetric
         product.
 
+      * The ``n``-by-``n`` complex-rational Hermitian matrices embedded
+        in the space of ``2n``-by-``2n`` real symmetric matrices.
+
+      * 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.
 
@@ -1037,11 +1863,17 @@ def random_eja():
         Euclidean Jordan algebra of degree...
 
     """
-    n = ZZ.random_element(1,5)
-    constructor = choice([eja_rn,
-                          JordanSpinSimpleEJA,
-                          RealSymmetricSimpleEJA,
-                          ComplexHermitianSimpleEJA])
+
+    # 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)
 
 
@@ -1102,6 +1934,48 @@ def _complex_hermitian_basis(n, field=QQ):
     return tuple(S)
 
 
+def _quaternion_hermitian_basis(n, field=QQ):
+    """
+    Returns a basis for the space of quaternion Hermitian n-by-n matrices.
+
+    TESTS::
+
+        sage: set_random_seed()
+        sage: n = ZZ.random_element(1,5)
+        sage: all( M.is_symmetric() for M in _quaternion_hermitian_basis(n) )
+        True
+
+    """
+    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 _mat2vec(m):
         return vector(m.base_ring(), m.list())
 
@@ -1168,13 +2042,27 @@ def _embed_complex_matrix(M):
         sage: x2 = F(1 + 2*i)
         sage: x3 = F(-i)
         sage: x4 = F(6)
-        sage: M = matrix(F,2,[x1,x2,x3,x4])
+        sage: M = matrix(F,2,[[x1,x2],[x3,x4]])
         sage: _embed_complex_matrix(M)
-        [ 4  2| 1 -2]
-        [-2  4| 2  1]
+        [ 4 -2| 1  2]
+        [ 2  4|-2  1]
         [-----+-----]
-        [ 0  1| 6  0]
-        [-1  0| 0  6]
+        [ 0 -1| 6  0]
+        [ 1  0| 0  6]
+
+    TESTS:
+
+    Embedding is a homomorphism (isomorphism, in fact)::
+
+        sage: set_random_seed()
+        sage: n = ZZ.random_element(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
 
     """
     n = M.nrows()
@@ -1185,7 +2073,7 @@ def _embed_complex_matrix(M):
     for z in M.list():
         a = z.real()
         b = z.imag()
-        blocks.append(matrix(field, 2, [[a,-b],[b,a]]))
+        blocks.append(matrix(field, 2, [[a,b],[-b,a]]))
 
     # We can drop the imaginaries here.
     return block_matrix(field.base_ring(), n, blocks)
@@ -1202,8 +2090,19 @@ def _unembed_complex_matrix(M):
         ....:                 [ 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]
+        [  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: _unembed_complex_matrix(_embed_complex_matrix(M)) == M
+        True
+
     """
     n = ZZ(M.nrows())
     if M.ncols() != n:
@@ -1221,14 +2120,123 @@ def _unembed_complex_matrix(M):
         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 real submatrix')
+                raise ValueError('bad on-diagonal submatrix')
             if submat[0,1] != -submat[1,0]:
-                raise ValueError('bad imag submatrix')
-            z = submat[0,0] + submat[1,0]*i
+                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.
+
+    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: _embed_quaternion_matrix(M)
+        [ 1  2  3  4]
+        [-2  1 -4  3]
+        [-3  4  1 -2]
+        [-4 -3  2  1]
+
+    Embedding is a homomorphism (isomorphism, in fact)::
+
+        sage: set_random_seed()
+        sage: n = ZZ.random_element(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
+        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 block_matrix(quaternions.base_ring(), n, blocks)
+
+
+def _unembed_quaternion_matrix(M):
+    """
+    The inverse of _embed_quaternion_matrix().
+
+    EXAMPLES::
+
+        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]
+
+    TESTS:
+
+    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
+
+    """
+    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.vector().inner_product(y.vector())
@@ -1242,7 +2250,7 @@ def _matrix_ip(X,Y):
     return (X_mat*Y_mat).trace()
 
 
-def RealSymmetricSimpleEJA(n, field=QQ):
+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
@@ -1250,7 +2258,7 @@ def RealSymmetricSimpleEJA(n, field=QQ):
 
     EXAMPLES::
 
-        sage: J = RealSymmetricSimpleEJA(2)
+        sage: J = RealSymmetricEJA(2)
         sage: e0, e1, e2 = J.gens()
         sage: e0*e0
         e0
@@ -1265,22 +2273,44 @@ def RealSymmetricSimpleEJA(n, field=QQ):
 
         sage: set_random_seed()
         sage: n = ZZ.random_element(1,5)
-        sage: J = RealSymmetricSimpleEJA(n)
+        sage: J = RealSymmetricEJA(n)
         sage: J.degree() == (n^2 + n)/2
         True
 
+    The Jordan multiplication is what we think it is::
+
+        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
+
     """
-    S = _real_symmetric_basis(n, field=field)
-    (Qs, T) = _multiplication_table_from_matrix_basis(S)
+    @staticmethod
+    def __classcall_private__(cls, n, field=QQ):
+        S = _real_symmetric_basis(n, field=field)
+        (Qs, T) = _multiplication_table_from_matrix_basis(S)
+
+        fdeja = super(RealSymmetricEJA, cls)
+        return fdeja.__classcall_private__(cls,
+                                           field,
+                                           Qs,
+                                           rank=n,
+                                           natural_basis=T)
 
-    return FiniteDimensionalEuclideanJordanAlgebra(field,
-                                                   Qs,
-                                                   rank=n,
-                                                   natural_basis=T,
-                                                   inner_product=_matrix_ip)
+    def inner_product(self, x, y):
+        return _matrix_ip(x,y)
 
 
-def ComplexHermitianSimpleEJA(n, field=QQ):
+class ComplexHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra):
     """
     The rank-n simple EJA consisting of complex Hermitian n-by-n
     matrices over the real numbers, the usual symmetric Jordan product,
@@ -1293,47 +2323,110 @@ def ComplexHermitianSimpleEJA(n, field=QQ):
 
         sage: set_random_seed()
         sage: n = ZZ.random_element(1,5)
-        sage: J = ComplexHermitianSimpleEJA(n)
+        sage: J = ComplexHermitianEJA(n)
         sage: J.degree() == n^2
         True
 
+    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: 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
+
     """
-    S = _complex_hermitian_basis(n)
-    (Qs, T) = _multiplication_table_from_matrix_basis(S)
+    @staticmethod
+    def __classcall_private__(cls, n, field=QQ):
+        S = _complex_hermitian_basis(n)
+        (Qs, T) = _multiplication_table_from_matrix_basis(S)
 
-    # Since a+bi on the diagonal is represented as
-    #
-    #   a + bi  = [  a  b  ]
-    #             [ -b  a  ],
-    #
-    # we'll double-count the "a" entries if we take the trace of
-    # the embedding.
-    ip = lambda X,Y: _matrix_ip(X,Y)/2
+        fdeja = super(ComplexHermitianEJA, cls)
+        return fdeja.__classcall_private__(cls,
+                                           field,
+                                           Qs,
+                                           rank=n,
+                                           natural_basis=T)
 
-    return FiniteDimensionalEuclideanJordanAlgebra(field,
-                                                   Qs,
-                                                   rank=n,
-                                                   natural_basis=T,
-                                                   inner_product=ip)
+    def inner_product(self, x, y):
+        # Since a+bi on the diagonal is represented as
+        #
+        #   a + bi  = [  a  b  ]
+        #             [ -b  a  ],
+        #
+        # we'll double-count the "a" entries if we take the trace of
+        # the embedding.
+        return _matrix_ip(x,y)/2
 
 
-def QuaternionHermitianSimpleEJA(n):
+class QuaternionHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra):
     """
     The rank-n simple EJA consisting of self-adjoint n-by-n quaternion
     matrices, the usual symmetric Jordan product, and the
     real-part-of-trace inner product. It has dimension `2n^2 - n` over
     the reals.
-    """
-    pass
 
-def OctonionHermitianSimpleEJA(n):
-    """
-    This shit be crazy. It has dimension 27 over the reals.
+    TESTS:
+
+    The degree of this algebra is `n^2`::
+
+        sage: set_random_seed()
+        sage: n = ZZ.random_element(1,5)
+        sage: J = QuaternionHermitianEJA(n)
+        sage: J.degree() == 2*(n^2) - n
+        True
+
+    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: 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
+
     """
-    n = 3
-    pass
+    @staticmethod
+    def __classcall_private__(cls, n, field=QQ):
+        S = _quaternion_hermitian_basis(n)
+        (Qs, T) = _multiplication_table_from_matrix_basis(S)
 
-def JordanSpinSimpleEJA(n, field=QQ):
+        fdeja = super(QuaternionHermitianEJA, cls)
+        return fdeja.__classcall_private__(cls,
+                                           field,
+                                           Qs,
+                                           rank=n,
+                                           natural_basis=T)
+
+    def inner_product(self, x, y):
+        # Since a+bi+cj+dk on the diagonal is represented as
+        #
+        #   a + bi +cj + dk = [  a  b  c  d]
+        #                     [ -b  a -d  c]
+        #                     [ -c  d  a -b]
+        #                     [ -d -c  b  a],
+        #
+        # we'll quadruple-count the "a" entries if we take the trace of
+        # the embedding.
+        return _matrix_ip(x,y)/4
+
+
+class JordanSpinEJA(FiniteDimensionalEuclideanJordanAlgebra):
     """
     The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
     with the usual inner product and jordan product ``x*y =
@@ -1344,7 +2437,7 @@ def JordanSpinSimpleEJA(n, field=QQ):
 
     This multiplication table can be verified by hand::
 
-        sage: J = JordanSpinSimpleEJA(4)
+        sage: J = JordanSpinEJA(4)
         sage: e0,e1,e2,e3 = J.gens()
         sage: e0*e0
         e0
@@ -1361,31 +2454,27 @@ def JordanSpinSimpleEJA(n, field=QQ):
         sage: e2*e3
         0
 
-    In one dimension, this is the reals under multiplication::
-
-        sage: J1 = JordanSpinSimpleEJA(1)
-        sage: J2 = eja_rn(1)
-        sage: J1 == J2
-        True
-
     """
-    Qs = []
-    id_matrix = identity_matrix(field, n)
-    for i in xrange(n):
-        ei = id_matrix.column(i)
-        Qi = zero_matrix(field, n)
-        Qi.set_row(0, ei)
-        Qi.set_column(0, ei)
-        Qi += diagonal_matrix(n, [ei[0]]*n)
-        # The addition of the diagonal matrix adds an extra ei[0] in the
-        # upper-left corner of the matrix.
-        Qi[0,0] = Qi[0,0] * ~field(2)
-        Qs.append(Qi)
-
-    # The rank of the spin factor algebra is two, UNLESS we're in a
-    # one-dimensional ambient space (the rank is bounded by the
-    # ambient dimension).
-    return FiniteDimensionalEuclideanJordanAlgebra(field,
-                                                   Qs,
-                                                   rank=min(n,2),
-                                                   inner_product=_usual_ip)
+    @staticmethod
+    def __classcall_private__(cls, n, field=QQ):
+        Qs = []
+        id_matrix = identity_matrix(field, n)
+        for i in xrange(n):
+            ei = id_matrix.column(i)
+            Qi = zero_matrix(field, n)
+            Qi.set_row(0, ei)
+            Qi.set_column(0, ei)
+            Qi += diagonal_matrix(n, [ei[0]]*n)
+            # The addition of the diagonal matrix adds an extra ei[0] in the
+            # upper-left corner of the matrix.
+            Qi[0,0] = Qi[0,0] * ~field(2)
+            Qs.append(Qi)
+
+        # 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, cls)
+        return fdeja.__classcall_private__(cls, field, Qs, rank=min(n,2))
+
+    def inner_product(self, x, y):
+        return _usual_ip(x,y)