]> gitweb.michael.orlitzky.com - sage.d.git/blobdiff - mjo/eja/eja_algebra.py
eja: begin refactoring to allow noncanonical inner products.
[sage.d.git] / mjo / eja / eja_algebra.py
index c40c8be4c371255f9bc174f99862ef0a7a204555..c569098b3d3e5d6fbceef6352e22861dd125a7f8 100644 (file)
@@ -3,6 +3,17 @@ Euclidean Jordan Algebras. These are formally-real Jordan Algebras;
 specifically those where u^2 + v^2 = 0 implies that u = v = 0. They
 are used in optimization, and have some additional nice methods beyond
 what can be supported in a general Jordan Algebra.
+
+
+SETUP::
+
+    sage: from mjo.eja.eja_algebra import random_eja
+
+EXAMPLES::
+
+    sage: random_eja()
+    Euclidean Jordan algebra of dimension...
+
 """
 
 from itertools import repeat
@@ -14,7 +25,6 @@ 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.misc.table import table
 from sage.modules.free_module import FreeModule, VectorSpace
 from sage.rings.all import (ZZ, QQ, AA, QQbar, RR, RLF, CLF,
@@ -23,6 +33,7 @@ from sage.rings.all import (ZZ, QQ, AA, QQbar, RR, RLF, CLF,
 from mjo.eja.eja_element import FiniteDimensionalEuclideanJordanAlgebraElement
 lazy_import('mjo.eja.eja_subalgebra',
             'FiniteDimensionalEuclideanJordanSubalgebra')
+from mjo.eja.eja_operator import FiniteDimensionalEuclideanJordanAlgebraOperator
 from mjo.eja.eja_utils import _mat2vec
 
 class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
@@ -216,25 +227,6 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
         coords =  W.coordinate_vector(_mat2vec(elt))
         return self.from_vector(coords)
 
-    @staticmethod
-    def _max_random_instance_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.
-        """
-        raise NotImplementedError
-
     def _repr_(self):
         """
         Return a string representation of ``self``.
@@ -393,7 +385,9 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
 
         SETUP::
 
-            sage: from mjo.eja.eja_algebra import random_eja
+            sage: from mjo.eja.eja_algebra import (random_eja,
+            ....:                                  HadamardEJA,
+            ....:                                  BilinearFormEJA)
 
         EXAMPLES:
 
@@ -406,10 +400,34 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
             sage: (x*y).inner_product(z) == y.inner_product(x*z)
             True
 
+        TESTS:
+
+        Ensure that this is the usual inner product for the algebras
+        over `R^n`::
+
+            sage: set_random_seed()
+            sage: J = HadamardEJA.random_instance()
+            sage: x,y = J.random_elements(2)
+            sage: actual = x.inner_product(y)
+            sage: expected = x.to_vector().inner_product(y.to_vector())
+            sage: actual == expected
+            True
+
+        Ensure that this is one-half of the trace inner-product in a
+        BilinearFormEJA that 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: J = BilinearFormEJA.random_instance()
+            sage: n = J.dimension()
+            sage: x = J.random_element()
+            sage: y = J.random_element()
+            sage: (n == 1) or (x.inner_product(y) == (x*y).trace()/2)
+            True
         """
-        X = x.natural_representation()
-        Y = y.natural_representation()
-        return self.natural_inner_product(X,Y)
+        B = self._inner_product_matrix
+        return (B*x.to_vector()).inner_product(y.to_vector())
 
 
     def is_trivial(self):
@@ -539,20 +557,6 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
             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):
         """
@@ -842,16 +846,6 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
         return tuple( self.random_element(thorough)
                       for idx in range(count) )
 
-    @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.
-        """
-        n = ZZ.random_element(cls._max_random_instance_size() + 1)
-        return cls(n, field, **kwargs)
 
     @cached_method
     def _charpoly_coefficients(self):
@@ -1013,32 +1007,6 @@ class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
     Element = FiniteDimensionalEuclideanJordanAlgebraElement
 
 
-
-def random_eja(field=AA):
-    """
-    Return a "random" finite-dimensional Euclidean Jordan Algebra.
-
-    SETUP::
-
-        sage: from mjo.eja.eja_algebra import random_eja
-
-    TESTS::
-
-        sage: random_eja()
-        Euclidean Jordan algebra of dimension...
-
-    """
-    classname = choice([TrivialEJA,
-                        HadamardEJA,
-                        JordanSpinEJA,
-                        RealSymmetricEJA,
-                        ComplexHermitianEJA,
-                        QuaternionHermitianEJA])
-    return classname.random_instance(field=field)
-
-
-
-
 class RationalBasisEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
     r"""
     Algebras whose basis consists of vectors with rational
@@ -1098,12 +1066,72 @@ class RationalBasisEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebr
         return tuple(map(lambda x: x.change_ring(self.base_ring()), a))
 
 
-class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
+class ConcreteEuclideanJordanAlgebra:
+    r"""
+    A class for the Euclidean Jordan algebras that we know by name.
+
+    These are the Jordan algebras whose basis, multiplication table,
+    rank, and so on are known a priori. More to the point, they are
+    the Euclidean Jordan algebras for which we are able to conjure up
+    a "random instance."
+
+    SETUP::
+
+        sage: from mjo.eja.eja_algebra import ConcreteEuclideanJordanAlgebra
+
+    TESTS:
+
+    Our natural basis is normalized with respect to the natural inner
+    product unless we specify otherwise::
+
+        sage: set_random_seed()
+        sage: J = ConcreteEuclideanJordanAlgebra.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: J = ConcreteEuclideanJordanAlgebra.random_instance()
+        sage: x = J.random_element()
+        sage: x.operator().matrix().is_symmetric()
+        True
+
+    """
+
     @staticmethod
     def _max_random_instance_size():
-        # Play it safe, since this will be squared and the underlying
-        # field can have dimension 4 (quaternions) too.
-        return 2
+        """
+        Return an integer "size" that is an upper bound on the size of
+        this algebra when it is used in a random test
+        case. Unfortunately, the term "size" is ambiguous -- when
+        dealing with `R^n` under either the Hadamard or Jordan spin
+        product, the "size" refers to the dimension `n`. When dealing
+        with a matrix algebra (real symmetric or complex/quaternion
+        Hermitian), it refers to the size of the matrix, which is far
+        less than the dimension of the underlying vector space.
+
+        This method must be implemented in each subclass.
+        """
+        raise NotImplementedError
+
+    @classmethod
+    def random_instance(cls, field=AA, **kwargs):
+        """
+        Return a random instance of this type of algebra.
+
+        This method should be implemented in each subclass.
+        """
+        from sage.misc.prandom import choice
+        eja_class = choice(cls.__subclasses__())
+        return eja_class.random_instance(field)
+
+
+class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
 
     def __init__(self, field, basis, normalize_basis=True, **kwargs):
         """
@@ -1119,6 +1147,10 @@ class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
         basis = tuple(basis)
 
         algebra_dim = len(basis)
+        degree = 0 # size of the matrices
+        if algebra_dim > 0:
+            degree = basis[0].nrows()
+
         if algebra_dim > 1 and normalize_basis:
             # We'll need sqrt(2) to normalize the basis, and this
             # winds up in the multiplication table, so the whole
@@ -1133,10 +1165,37 @@ class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
                 ~(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)
+        # Now compute the multiplication and inner product tables.
+        # We have to do this *after* normalizing the basis, because
+        # scaling affects the answers.
+        V = VectorSpace(field, degree**2)
+        W = V.span_of_basis( _mat2vec(s) for s in basis )
+        mult_table = [[W.zero() for j in range(algebra_dim)]
+                                for i in range(algebra_dim)]
+        ip_table = [[W.zero() for j in range(algebra_dim)]
+                              for i in range(algebra_dim)]
+        for i in range(algebra_dim):
+            for j in range(algebra_dim):
+                mat_entry = (basis[i]*basis[j] + basis[j]*basis[i])/2
+                mult_table[i][j] = W.coordinate_vector(_mat2vec(mat_entry))
+
+                try:
+                    # HACK: ignore the error here if we don't need the
+                    # inner product (as is the case when we construct
+                    # a dummy QQ-algebra for fast charpoly coefficients.
+                    ip_table[i][j] = self.natural_inner_product(basis[i],
+                                                                basis[j])
+                except:
+                    pass
+
+        try:
+            # HACK PART DEUX
+            self._inner_product_matrix = matrix(field,ip_table)
+        except:
+            pass
 
         super(MatrixEuclideanJordanAlgebra, self).__init__(field,
-                                                           Qs,
+                                                           mult_table,
                                                            natural_basis=basis,
                                                            **kwargs)
 
@@ -1195,39 +1254,6 @@ class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
         return tuple( a_i.subs(subs_dict) for a_i in a )
 
 
-    @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.
-        if len(basis) == 0:
-            return []
-
-        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))
-
-        return mult_table
-
-
     @staticmethod
     def real_embed(M):
         """
@@ -1252,7 +1278,6 @@ class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
         """
         raise NotImplementedError
 
-
     @classmethod
     def natural_inner_product(cls,X,Y):
         Xu = cls.real_unembed(X)
@@ -1287,7 +1312,8 @@ class RealMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
         return M
 
 
-class RealSymmetricEJA(RealMatrixEuclideanJordanAlgebra):
+class RealSymmetricEJA(RealMatrixEuclideanJordanAlgebra,
+                       ConcreteEuclideanJordanAlgebra):
     """
     The rank-n simple EJA consisting of real symmetric n-by-n
     matrices, the usual symmetric Jordan product, and the trace inner
@@ -1346,25 +1372,6 @@ class RealSymmetricEJA(RealMatrixEuclideanJordanAlgebra):
         sage: RealSymmetricEJA(3, prefix='q').gens()
         (q0, q1, q2, q3, q4, q5)
 
-    Our natural basis is normalized with respect to the natural inner
-    product unless we specify otherwise::
-
-        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: x = RealSymmetricEJA.random_instance().random_element()
-        sage: x.operator().matrix().is_symmetric()
-        True
-
     We can construct the (trivial) algebra of rank zero::
 
         sage: RealSymmetricEJA(0)
@@ -1407,6 +1414,13 @@ class RealSymmetricEJA(RealMatrixEuclideanJordanAlgebra):
     def _max_random_instance_size():
         return 4 # Dimension 10
 
+    @classmethod
+    def random_instance(cls, field=AA, **kwargs):
+        """
+        Return a random instance of this type of algebra.
+        """
+        n = ZZ.random_element(cls._max_random_instance_size() + 1)
+        return cls(n, field, **kwargs)
 
     def __init__(self, n, field=AA, **kwargs):
         basis = self._denormalized_basis(n, field)
@@ -1450,8 +1464,7 @@ class ComplexMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
         Embedding is a homomorphism (isomorphism, in fact)::
 
             sage: set_random_seed()
-            sage: n_max = ComplexMatrixEuclideanJordanAlgebra._max_random_instance_size()
-            sage: n = ZZ.random_element(n_max)
+            sage: n = ZZ.random_element(3)
             sage: F = QuadraticField(-1, 'I')
             sage: X = random_matrix(F, n)
             sage: Y = random_matrix(F, n)
@@ -1576,7 +1589,8 @@ class ComplexMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
         return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/2
 
 
-class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra):
+class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra,
+                          ConcreteEuclideanJordanAlgebra):
     """
     The rank-n simple EJA consisting of complex Hermitian n-by-n
     matrices over the real numbers, the usual symmetric Jordan product,
@@ -1627,25 +1641,6 @@ class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra):
         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
-
     We can construct the (trivial) algebra of rank zero::
 
         sage: ComplexHermitianEJA(0)
@@ -1715,6 +1710,17 @@ class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra):
                                                  **kwargs)
         self.rank.set_cache(n)
 
+    @staticmethod
+    def _max_random_instance_size():
+        return 3 # Dimension 9
+
+    @classmethod
+    def random_instance(cls, field=AA, **kwargs):
+        """
+        Return a random instance of this type of algebra.
+        """
+        n = ZZ.random_element(cls._max_random_instance_size() + 1)
+        return cls(n, field, **kwargs)
 
 class QuaternionMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
     @staticmethod
@@ -1746,8 +1752,7 @@ class QuaternionMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
         Embedding is a homomorphism (isomorphism, in fact)::
 
             sage: set_random_seed()
-            sage: n_max = QuaternionMatrixEuclideanJordanAlgebra._max_random_instance_size()
-            sage: n = ZZ.random_element(n_max)
+            sage: n = ZZ.random_element(2)
             sage: Q = QuaternionAlgebra(QQ,-1,-1)
             sage: X = random_matrix(Q, n)
             sage: Y = random_matrix(Q, n)
@@ -1879,8 +1884,9 @@ class QuaternionMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
         return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/4
 
 
-class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra):
-    """
+class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra,
+                             ConcreteEuclideanJordanAlgebra):
+    r"""
     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
@@ -1930,25 +1936,6 @@ class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra):
         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
-
     We can construct the (trivial) algebra of rank zero::
 
         sage: QuaternionHermitianEJA(0)
@@ -2019,8 +2006,24 @@ class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra):
                                                     **kwargs)
         self.rank.set_cache(n)
 
+    @staticmethod
+    def _max_random_instance_size():
+        r"""
+        The maximum rank of a random QuaternionHermitianEJA.
+        """
+        return 2 # Dimension 6
+
+    @classmethod
+    def random_instance(cls, field=AA, **kwargs):
+        """
+        Return a random instance of this type of algebra.
+        """
+        n = ZZ.random_element(cls._max_random_instance_size() + 1)
+        return cls(n, field, **kwargs)
+
 
-class HadamardEJA(RationalBasisEuclideanJordanAlgebra):
+class HadamardEJA(RationalBasisEuclideanJordanAlgebra,
+                  ConcreteEuclideanJordanAlgebra):
     """
     Return the Euclidean Jordan Algebra corresponding to the set
     `R^n` under the Hadamard product.
@@ -2065,6 +2068,16 @@ class HadamardEJA(RationalBasisEuclideanJordanAlgebra):
         mult_table = [ [ V.gen(i)*(i == j) for j in range(n) ]
                        for i in range(n) ]
 
+        # Inner products are real numbers and not algebra
+        # elements, so once we turn the algebra element
+        # into a vector in inner_product(), we never go
+        # back. As a result -- contrary to what we do with
+        # self._multiplication_table -- we store the inner
+        # product table as a plain old matrix and not as
+        # an algebra operator.
+        ip_table = matrix.identity(field,n)
+        self._inner_product_matrix = ip_table
+
         super(HadamardEJA, self).__init__(field,
                                           mult_table,
                                           check_axioms=False,
@@ -2078,34 +2091,22 @@ class HadamardEJA(RationalBasisEuclideanJordanAlgebra):
 
     @staticmethod
     def _max_random_instance_size():
+        r"""
+        The maximum dimension of a random HadamardEJA.
+        """
         return 5
 
-    def inner_product(self, x, y):
+    @classmethod
+    def random_instance(cls, field=AA, **kwargs):
         """
-        Faster to reimplement than to use natural representations.
-
-        SETUP::
-
-            sage: from mjo.eja.eja_algebra import HadamardEJA
-
-        TESTS:
-
-        Ensure that this is the usual inner product for the algebras
-        over `R^n`::
-
-            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
-
+        Return a random instance of this type of algebra.
         """
-        return x.to_vector().inner_product(y.to_vector())
+        n = ZZ.random_element(cls._max_random_instance_size() + 1)
+        return cls(n, field, **kwargs)
 
 
-class BilinearFormEJA(RationalBasisEuclideanJordanAlgebra):
+class BilinearFormEJA(RationalBasisEuclideanJordanAlgebra,
+                      ConcreteEuclideanJordanAlgebra):
     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 =
@@ -2185,7 +2186,6 @@ class BilinearFormEJA(RationalBasisEuclideanJordanAlgebra):
         True
     """
     def __init__(self, B, field=AA, **kwargs):
-        self._B = B
         n = B.nrows()
 
         if not B.is_positive_definite():
@@ -2206,13 +2206,24 @@ class BilinearFormEJA(RationalBasisEuclideanJordanAlgebra):
                 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).
+        # Inner products are real numbers and not algebra
+        # elements, so once we turn the algebra element
+        # into a vector in inner_product(), we never go
+        # back. As a result -- contrary to what we do with
+        # self._multiplication_table -- we store the inner
+        # product table as a plain old matrix and not as
+        # an algebra operator.
+        ip_table = B
+        self._inner_product_matrix = ip_table
+
         super(BilinearFormEJA, self).__init__(field,
                                               mult_table,
                                               check_axioms=False,
                                               **kwargs)
+
+        # 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).
         self.rank.set_cache(min(n,2))
 
         if n == 0:
@@ -2222,6 +2233,9 @@ class BilinearFormEJA(RationalBasisEuclideanJordanAlgebra):
 
     @staticmethod
     def _max_random_instance_size():
+        r"""
+        The maximum dimension of a random BilinearFormEJA.
+        """
         return 5
 
     @classmethod
@@ -2248,35 +2262,6 @@ class BilinearFormEJA(RationalBasisEuclideanJordanAlgebra):
 
         return cls(B, field, **kwargs)
 
-    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: J = BilinearFormEJA.random_instance()
-            sage: n = J.dimension()
-            sage: x = J.random_element()
-            sage: y = J.random_element()
-            sage: (n == 1) or (x.inner_product(y) == (x*y).trace()/2)
-            True
-
-        """
-        return (self._B*x.to_vector()).inner_product(y.to_vector())
-
 
 class JordanSpinEJA(BilinearFormEJA):
     """
@@ -2322,9 +2307,9 @@ class JordanSpinEJA(BilinearFormEJA):
             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)
+            sage: actual = x.inner_product(y)
+            sage: expected = x.to_vector().inner_product(y.to_vector())
+            sage: actual == expected
             True
 
     """
@@ -2334,6 +2319,13 @@ class JordanSpinEJA(BilinearFormEJA):
         B = matrix.identity(field, n)
         super(JordanSpinEJA, self).__init__(B, field, **kwargs)
 
+    @staticmethod
+    def _max_random_instance_size():
+        r"""
+        The maximum dimension of a random JordanSpinEJA.
+        """
+        return 5
+
     @classmethod
     def random_instance(cls, field=AA, **kwargs):
         """
@@ -2345,7 +2337,8 @@ class JordanSpinEJA(BilinearFormEJA):
         return cls(n, field, **kwargs)
 
 
-class TrivialEJA(FiniteDimensionalEuclideanJordanAlgebra):
+class TrivialEJA(FiniteDimensionalEuclideanJordanAlgebra,
+                 ConcreteEuclideanJordanAlgebra):
     """
     The trivial Euclidean Jordan algebra consisting of only a zero element.
 
@@ -2376,6 +2369,7 @@ class TrivialEJA(FiniteDimensionalEuclideanJordanAlgebra):
     """
     def __init__(self, field=AA, **kwargs):
         mult_table = []
+        self._inner_product_matrix = matrix(field,0)
         super(TrivialEJA, self).__init__(field,
                                          mult_table,
                                          check_axioms=False,
@@ -2401,7 +2395,8 @@ class DirectSumEJA(FiniteDimensionalEuclideanJordanAlgebra):
 
     SETUP::
 
-        sage: from mjo.eja.eja_algebra import (HadamardEJA,
+        sage: from mjo.eja.eja_algebra import (random_eja,
+        ....:                                  HadamardEJA,
         ....:                                  RealSymmetricEJA,
         ....:                                  DirectSumEJA)
 
@@ -2415,8 +2410,25 @@ class DirectSumEJA(FiniteDimensionalEuclideanJordanAlgebra):
         sage: J.rank()
         5
 
+    TESTS:
+
+    The external direct sum construction is only valid when the two factors
+    have the same base ring; an error is raised otherwise::
+
+        sage: set_random_seed()
+        sage: J1 = random_eja(AA)
+        sage: J2 = random_eja(QQ)
+        sage: J = DirectSumEJA(J1,J2)
+        Traceback (most recent call last):
+        ...
+        ValueError: algebras must share the same base field
+
     """
-    def __init__(self, J1, J2, field=AA, **kwargs):
+    def __init__(self, J1, J2, **kwargs):
+        if J1.base_ring() != J2.base_ring():
+            raise ValueError("algebras must share the same base field")
+        field = J1.base_ring()
+
         self._factors = (J1, J2)
         n1 = J1.dimension()
         n2 = J2.dimension()
@@ -2488,9 +2500,16 @@ class DirectSumEJA(FiniteDimensionalEuclideanJordanAlgebra):
 
         """
         (J1,J2) = self.factors()
-        n = J1.dimension()
-        pi_left  = lambda x: J1.from_vector(x.to_vector()[:n])
-        pi_right = lambda x: J2.from_vector(x.to_vector()[n:])
+        m = J1.dimension()
+        n = J2.dimension()
+        V_basis = self.vector_space().basis()
+        # Need to specify the dimensions explicitly so that we don't
+        # wind up with a zero-by-zero matrix when we want e.g. a
+        # zero-by-two matrix (important for composing things).
+        P1 = matrix(self.base_ring(), m, m+n, V_basis[:m])
+        P2 = matrix(self.base_ring(), n, m+n, V_basis[m:])
+        pi_left = FiniteDimensionalEuclideanJordanAlgebraOperator(self,J1,P1)
+        pi_right = FiniteDimensionalEuclideanJordanAlgebraOperator(self,J2,P2)
         return (pi_left, pi_right)
 
     def inclusions(self):
@@ -2499,7 +2518,8 @@ class DirectSumEJA(FiniteDimensionalEuclideanJordanAlgebra):
 
         SETUP::
 
-            sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
+            sage: from mjo.eja.eja_algebra import (random_eja,
+            ....:                                  JordanSpinEJA,
             ....:                                  RealSymmetricEJA,
             ....:                                  DirectSumEJA)
 
@@ -2524,14 +2544,39 @@ class DirectSumEJA(FiniteDimensionalEuclideanJordanAlgebra):
             sage: J.one().to_vector()
             (1, 0, 0, 1, 0, 1)
 
+        TESTS:
+
+        Composing a projection with the corresponding inclusion should
+        produce the identity map, and mismatching them should produce
+        the zero map::
+
+            sage: set_random_seed()
+            sage: J1 = random_eja()
+            sage: J2 = random_eja()
+            sage: J = DirectSumEJA(J1,J2)
+            sage: (iota_left, iota_right) = J.inclusions()
+            sage: (pi_left, pi_right) = J.projections()
+            sage: pi_left*iota_left == J1.one().operator()
+            True
+            sage: pi_right*iota_right == J2.one().operator()
+            True
+            sage: (pi_left*iota_right).is_zero()
+            True
+            sage: (pi_right*iota_left).is_zero()
+            True
+
         """
         (J1,J2) = self.factors()
-        n = J1.dimension()
+        m = J1.dimension()
+        n = J2.dimension()
         V_basis = self.vector_space().basis()
-        I1 = matrix.column(self.base_ring(), V_basis[:n])
-        I2 = matrix.column(self.base_ring(), V_basis[n:])
-        iota_left = lambda x: self.from_vector(I1*x.to_vector())
-        iota_right = lambda x: self.from_vector(I2*+x.to_vector())
+        # Need to specify the dimensions explicitly so that we don't
+        # wind up with a zero-by-zero matrix when we want e.g. a
+        # two-by-zero matrix (important for composing things).
+        I1 = matrix.column(self.base_ring(), m, m+n, V_basis[:m])
+        I2 = matrix.column(self.base_ring(), n, m+n, V_basis[m:])
+        iota_left = FiniteDimensionalEuclideanJordanAlgebraOperator(J1,self,I1)
+        iota_right = FiniteDimensionalEuclideanJordanAlgebraOperator(J2,self,I2)
         return (iota_left, iota_right)
 
     def inner_product(self, x, y):
@@ -2550,7 +2595,7 @@ class DirectSumEJA(FiniteDimensionalEuclideanJordanAlgebra):
 
         EXAMPLE::
 
-            sage: J1 = HadamardEJA(3)
+            sage: J1 = HadamardEJA(3,QQ)
             sage: J2 = QuaternionHermitianEJA(2,QQ,normalize_basis=False)
             sage: J = DirectSumEJA(J1,J2)
             sage: x1 = J1.one()
@@ -2572,3 +2617,7 @@ class DirectSumEJA(FiniteDimensionalEuclideanJordanAlgebra):
         y2 = pi_right(y)
 
         return (x1.inner_product(y1) + x2.inner_product(y2))
+
+
+
+random_eja = ConcreteEuclideanJordanAlgebra.random_instance