]> gitweb.michael.orlitzky.com - sage.d.git/blobdiff - mjo/eja/eja_algebra.py
eja: add brute-force associativity test.
[sage.d.git] / mjo / eja / eja_algebra.py
index 51ff79054eab8cdb83fe48c3d566131598b46278..3b64ddd7374d8931f80fdefe4b0fb1734b682995 100644 (file)
@@ -1,9 +1,53 @@
 """
-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.
-
+Representations and constructions for Euclidean Jordan algebras.
+
+A Euclidean Jordan algebra is a Jordan algebra that has some
+additional properties:
+
+  1.   It is finite-dimensional.
+  2.   Its scalar field is the real numbers.
+  3a.  An inner product is defined on it, and...
+  3b.  That inner product is compatible with the Jordan product
+       in the sense that `<x*y,z> = <y,x*z>` for all elements
+       `x,y,z` in the algebra.
+
+Every Euclidean Jordan algebra is formally-real: for any two elements
+`x` and `y` in the algebra, `x^{2} + y^{2} = 0` implies that `x = y =
+0`. Conversely, every finite-dimensional formally-real Jordan algebra
+can be made into a Euclidean Jordan algebra with an appropriate choice
+of inner-product.
+
+Formally-real Jordan algebras were originally studied as a framework
+for quantum mechanics. Today, Euclidean Jordan algebras are crucial in
+symmetric cone optimization, since every symmetric cone arises as the
+cone of squares in some Euclidean Jordan algebra.
+
+It is known that every Euclidean Jordan algebra decomposes into an
+orthogonal direct sum (essentially, a Cartesian product) of simple
+algebras, and that moreover, up to Jordan-algebra isomorphism, there
+are only five families of simple algebras. We provide constructions
+for these simple algebras:
+
+  * :class:`BilinearFormEJA`
+  * :class:`RealSymmetricEJA`
+  * :class:`ComplexHermitianEJA`
+  * :class:`QuaternionHermitianEJA`
+
+Missing from this list is the algebra of three-by-three octononion
+Hermitian matrices, as there is (as of yet) no implementation of the
+octonions in SageMath. In addition to these, we provide two other
+example constructions,
+
+  * :class:`HadamardEJA`
+  * :class:`TrivialEJA`
+
+The Jordan spin algebra is a bilinear form algebra where the bilinear
+form is the identity. The Hadamard EJA is simply a Cartesian product
+of one-dimensional spin algebras. And last but not least, the trivial
+EJA is exactly what you think. Cartesian products of these are also
+supported using the usual ``cartesian_product()`` function; as a
+result, we support (up to isomorphism) all Euclidean Jordan algebras
+that don't involve octonions.
 
 SETUP::
 
@@ -13,7 +57,6 @@ EXAMPLES::
 
     sage: random_eja()
     Euclidean Jordan algebra of dimension...
-
 """
 
 from itertools import repeat
@@ -31,10 +74,9 @@ from sage.modules.free_module import FreeModule, VectorSpace
 from sage.rings.all import (ZZ, QQ, AA, QQbar, RR, RLF, CLF,
                             PolynomialRing,
                             QuadraticField)
-from mjo.eja.eja_element import (CartesianProductEJAElement,
-                                 FiniteDimensionalEJAElement)
+from mjo.eja.eja_element import FiniteDimensionalEJAElement
 from mjo.eja.eja_operator import FiniteDimensionalEJAOperator
-from mjo.eja.eja_utils import _mat2vec
+from mjo.eja.eja_utils import _all2list, _mat2vec
 
 class FiniteDimensionalEJA(CombinatorialFreeModule):
     r"""
@@ -42,24 +84,33 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
 
     INPUT:
 
-      - basis -- a tuple of basis elements in "matrix form," which
-        must be the same form as the arguments to ``jordan_product``
-        and ``inner_product``. In reality, "matrix form" can be either
-        vectors, matrices, or a Cartesian product (ordered tuple)
-        of vectors or matrices. All of these would ideally be vector
-        spaces in sage with no special-casing needed; but in reality
-        we turn vectors into column-matrices and Cartesian products
-        `(a,b)` into column matrices `(a,b)^{T}` after converting
-        `a` and `b` themselves.
-
-      - jordan_product -- function of two elements (in matrix form)
-        that returns their jordan product in this algebra; this will
-        be applied to ``basis`` to compute a multiplication table for
-        the algebra.
-
-      - inner_product -- function of two elements (in matrix form) that
-        returns their inner product. This will be applied to ``basis`` to
-        compute an inner-product table (basically a matrix) for this algebra.
+      - ``basis`` -- a tuple; a tuple of basis elements in "matrix
+        form," which must be the same form as the arguments to
+        ``jordan_product`` and ``inner_product``. In reality, "matrix
+        form" can be either vectors, matrices, or a Cartesian product
+        (ordered tuple) of vectors or matrices. All of these would
+        ideally be vector spaces in sage with no special-casing
+        needed; but in reality we turn vectors into column-matrices
+        and Cartesian products `(a,b)` into column matrices
+        `(a,b)^{T}` after converting `a` and `b` themselves.
+
+      - ``jordan_product`` -- a function; afunction of two ``basis``
+        elements (in matrix form) that returns their jordan product,
+        also in matrix form; this will be applied to ``basis`` to
+        compute a multiplication table for the algebra.
+
+      - ``inner_product`` -- a function; a function of two ``basis``
+        elements (in matrix form) that returns their inner
+        product. This will be applied to ``basis`` to compute an
+        inner-product table (basically a matrix) for this algebra.
+
+      - ``field`` -- a subfield of the reals (default: ``AA``); the scalar
+        field for the algebra.
+
+      - ``orthonormalize`` -- boolean (default: ``True``); whether or
+        not to orthonormalize the basis. Doing so is expensive and
+        generally rules out using the rationals as your ``field``, but
+        is required for spectral decompositions.
 
     """
     Element = FiniteDimensionalEJAElement
@@ -76,6 +127,18 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
                  check_axioms=True,
                  prefix='e'):
 
+        # Keep track of whether or not the matrix basis consists of
+        # tuples, since we need special cases for them damned near
+        # everywhere.  This is INDEPENDENT of whether or not the
+        # algebra is a cartesian product, since a subalgebra of a
+        # cartesian product will have a basis of tuples, but will not
+        # in general itself be a cartesian product algebra.
+        self._matrix_basis_is_cartesian = False
+        n = len(basis)
+        if n > 0:
+            if hasattr(basis[0], 'cartesian_factors'):
+                self._matrix_basis_is_cartesian = True
+
         if check_field:
             if not field.is_subring(RR):
                 # Note: this does return true for the real algebraic
@@ -89,7 +152,13 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
             # The field for a cartesian product algebra comes from one
             # of its factors and is the same for all factors, so
             # there's no need to "reapply" it on product algebras.
-            basis = tuple( b.change_ring(field) for b in basis )
+            if self._matrix_basis_is_cartesian:
+                # OK since if n == 0, the basis does not consist of tuples.
+                P = basis[0].parent()
+                basis = tuple( P(tuple(b_i.change_ring(field) for b_i in b))
+                               for b in basis )
+            else:
+                basis = tuple( b.change_ring(field) for b in basis )
 
 
         if check_axioms:
@@ -109,7 +178,8 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
 
 
         category = MagmaticAlgebras(field).FiniteDimensional()
-        category = category.WithBasis().Unital()
+        category = category.WithBasis().Unital().Commutative()
+
         if associative:
             # Element subalgebras can take advantage of this.
             category = category.Associative()
@@ -118,12 +188,12 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
 
         # Call the superclass constructor so that we can use its from_vector()
         # method to build our multiplication table.
-        n = len(basis)
-        super().__init__(field,
-                         range(n),
-                         prefix=prefix,
-                         category=category,
-                         bracket=False)
+        CombinatorialFreeModule.__init__(self,
+                                         field,
+                                         range(n),
+                                         prefix=prefix,
+                                         category=category,
+                                         bracket=False)
 
         # Now comes all of the hard work. We'll be constructing an
         # ambient vector space V that our (vectorized) basis lives in,
@@ -132,17 +202,9 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
         # we see in things like x = 1*e1 + 2*e2.
         vector_basis = basis
 
-        def flatten(b):
-            # flatten a vector, matrix, or cartesian product of those
-            # things into a long list.
-            if cartesian_product:
-                return sum(( b_i.list() for b_i in b ), [])
-            else:
-                return b.list()
-
         degree = 0
         if n > 0:
-            degree = len(flatten(basis[0]))
+            degree = len(_all2list(basis[0]))
 
         # Build an ambient space that fits our matrix basis when
         # written out as "long vectors."
@@ -156,7 +218,7 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
             # Save a copy of the un-orthonormalized basis for later.
             # Convert it to ambient V (vector) coordinates while we're
             # at it, because we'd have to do it later anyway.
-            deortho_vector_basis = tuple( V(flatten(b)) for b in basis )
+            deortho_vector_basis = tuple( V(_all2list(b)) for b in basis )
 
             from mjo.eja.eja_utils import gram_schmidt
             basis = tuple(gram_schmidt(basis, inner_product))
@@ -168,7 +230,7 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
         # Now create the vector space for the algebra, which will have
         # its own set of non-ambient coordinates (in terms of the
         # supplied basis).
-        vector_basis = tuple( V(flatten(b)) for b in basis )
+        vector_basis = tuple( V(_all2list(b)) for b in basis )
         W = V.span_of_basis( vector_basis, check=check_axioms)
 
         if orthonormalize:
@@ -200,7 +262,7 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
                 # The jordan product returns a matrixy answer, so we
                 # have to convert it to the algebra coordinates.
                 elt = jordan_product(q_i, q_j)
-                elt = W.coordinate_vector(V(flatten(elt)))
+                elt = W.coordinate_vector(V(_all2list(elt)))
                 self._multiplication_table[i][j] = self.from_vector(elt)
 
                 if not orthonormalize:
@@ -248,6 +310,35 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
 
 
     def product_on_basis(self, i, j):
+        r"""
+        Returns the Jordan product of the `i` and `j`th basis elements.
+
+        This completely defines the Jordan product on the algebra, and
+        is used direclty by our superclass machinery to implement
+        :meth:`product`.
+
+        SETUP::
+
+            sage: from mjo.eja.eja_algebra import random_eja
+
+        TESTS::
+
+            sage: set_random_seed()
+            sage: J = random_eja()
+            sage: n = J.dimension()
+            sage: ei = J.zero()
+            sage: ej = J.zero()
+            sage: ei_ej = J.zero()*J.zero()
+            sage: if n > 0:
+            ....:     i = ZZ.random_element(n)
+            ....:     j = ZZ.random_element(n)
+            ....:     ei = J.gens()[i]
+            ....:     ej = J.gens()[j]
+            ....:     ei_ej = J.product_on_basis(i,j)
+            sage: ei*ej == ei_ej
+            True
+
+        """
         # We only stored the lower-triangular portion of the
         # multiplication table.
         if j <= i:
@@ -332,6 +423,18 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
         """
         return "Associative" in self.category().axioms()
 
+    def _is_commutative(self):
+        r"""
+        Whether or not this algebra's multiplication table is commutative.
+
+        This method should of course always return ``True``, unless
+        this algebra was constructed with ``check_axioms=False`` and
+        passed an invalid multiplication table.
+        """
+        return all( self.product_on_basis(i,j) == self.product_on_basis(i,j)
+                    for i in range(self.dimension())
+                    for j in range(self.dimension()) )
+
     def _is_jordanian(self):
         r"""
         Whether or not this algebra's multiplication table respects the
@@ -339,7 +442,7 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
 
         We only check one arrangement of `x` and `y`, so for a
         ``True`` result to be truly true, you should also check
-        :meth:`is_commutative`. This method should of course always
+        :meth:`_is_commutative`. This method should of course always
         return ``True``, unless this algebra was constructed with
         ``check_axioms=False`` and passed an invalid multiplication table.
         """
@@ -349,6 +452,81 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
                     for i in range(self.dimension())
                     for j in range(self.dimension()) )
 
+    def _jordan_product_is_associative(self):
+        r"""
+        Return whether or not this algebra's Jordan product is
+        associative; that is, whether or not `x*(y*z) = (x*y)*z`
+        for all `x,y,x`.
+
+        This method should agree with :meth:`is_associative` unless
+        you lied about the value of the ``associative`` parameter
+        when you constructed the algebra.
+
+        SETUP::
+
+            sage: from mjo.eja.eja_algebra import (RealSymmetricEJA,
+            ....:                                  ComplexHermitianEJA,
+            ....:                                  QuaternionHermitianEJA)
+
+        EXAMPLES::
+
+            sage: J = RealSymmetricEJA(4, orthonormalize=False)
+            sage: J._jordan_product_is_associative()
+            False
+            sage: x = sum(J.gens())
+            sage: A = x.subalgebra_generated_by()
+            sage: A._jordan_product_is_associative()
+            True
+
+        ::
+
+            sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
+            sage: J._jordan_product_is_associative()
+            False
+            sage: x = sum(J.gens())
+            sage: A = x.subalgebra_generated_by(orthonormalize=False)
+            sage: A._jordan_product_is_associative()
+            True
+
+        ::
+
+            sage: J = QuaternionHermitianEJA(2)
+            sage: J._jordan_product_is_associative()
+            False
+            sage: x = sum(J.gens())
+            sage: A = x.subalgebra_generated_by()
+            sage: A._jordan_product_is_associative()
+            True
+
+        """
+        R = self.base_ring()
+
+        # Used to check whether or not something is zero.
+        epsilon = R.zero()
+        if not R.is_exact():
+            # I don't know of any examples that make this magnitude
+            # necessary because I don't know how to make an
+            # associative algebra when the element subalgebra
+            # construction is unreliable (as it is over RDF; we can't
+            # find the degree of an element because we can't compute
+            # the rank of a matrix). But even multiplication of floats
+            # is non-associative, so *some* epsilon is needed... let's
+            # just take the one from _inner_product_is_associative?
+            epsilon = 1e-15
+
+        for i in range(self.dimension()):
+            for j in range(self.dimension()):
+                for k in range(self.dimension()):
+                    x = self.gens()[i]
+                    y = self.gens()[j]
+                    z = self.gens()[k]
+                    diff = (x*y)*z - x*(y*z)
+
+                    if diff.norm() > epsilon:
+                        return False
+
+        return True
+
     def _inner_product_is_associative(self):
         r"""
         Return whether or not this algebra's inner product `B` is
@@ -358,11 +536,14 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
         this algebra was constructed with ``check_axioms=False`` and
         passed an invalid Jordan or inner-product.
         """
+        R = self.base_ring()
 
-        # Used to check whether or not something is zero in an inexact
-        # ring. This number is sufficient to allow the construction of
-        # QuaternionHermitianEJA(2, field=RDF) with check_axioms=True.
-        epsilon = 1e-16
+        # Used to check whether or not something is zero.
+        epsilon = R.zero()
+        if not R.is_exact():
+            # This choice is sufficient to allow the construction of
+            # QuaternionHermitianEJA(2, field=RDF) with check_axioms=True.
+            epsilon = 1e-15
 
         for i in range(self.dimension()):
             for j in range(self.dimension()):
@@ -372,12 +553,8 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
                     z = self.gens()[k]
                     diff = (x*y).inner_product(z) - x.inner_product(y*z)
 
-                    if self.base_ring().is_exact():
-                        if diff != 0:
-                            return False
-                    else:
-                        if diff.abs() > epsilon:
-                            return False
+                    if diff.abs() > epsilon:
+                        return False
 
         return True
 
@@ -413,6 +590,15 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
             ...
             ValueError: not an element of this algebra
 
+        Tuples work as well, provided that the matrix basis for the
+        algebra consists of them::
+
+            sage: J1 = HadamardEJA(3)
+            sage: J2 = RealSymmetricEJA(2)
+            sage: J = cartesian_product([J1,J2])
+            sage: J( (J1.matrix_basis()[1], J2.matrix_basis()[2]) )
+            e(0, 1) + e(1, 2)
+
         TESTS:
 
         Ensure that we can convert any element of the two non-matrix
@@ -429,13 +615,23 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
             sage: J(x.to_vector().column()) == x
             True
 
+        We cannot coerce elements between algebras just because their
+        matrix representations are compatible::
+
+            sage: J1 = HadamardEJA(3)
+            sage: J2 = JordanSpinEJA(3)
+            sage: J2(J1.one())
+            Traceback (most recent call last):
+            ...
+            ValueError: not an element of this algebra
+            sage: J1(J2.zero())
+            Traceback (most recent call last):
+            ...
+            ValueError: not an element of this algebra
+
         """
         msg = "not an element of this algebra"
-        if elt == 0:
-            # The superclass implementation of random_element()
-            # needs to be able to coerce "0" into the algebra.
-            return self.zero()
-        elif elt in self.base_ring():
+        if elt in self.base_ring():
             # Ensure that no base ring -> algebra coercion is performed
             # by this method. There's some stupidity in sage that would
             # otherwise propagate to this method; for example, sage thinks
@@ -443,9 +639,11 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
             raise ValueError(msg)
 
         try:
+            # Try to convert a vector into a column-matrix...
             elt = elt.column()
         except (AttributeError, TypeError):
-            # Try to convert a vector into a column-matrix
+            # and ignore failure, because we weren't really expecting
+            # a vector as an argument anyway.
             pass
 
         if elt not in self.matrix_space():
@@ -458,14 +656,20 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
         # closure whereas the base ring of the 3-by-3 identity matrix
         # could be QQ instead of QQbar.
         #
+        # And, we also have to handle Cartesian product bases (when
+        # the matrix basis consists of tuples) here. The "good news"
+        # is that we're already converting everything to long vectors,
+        # and that strategy works for tuples as well.
+        #
         # We pass check=False because the matrix basis is "guaranteed"
         # to be linearly independent... right? Ha ha.
-        V = VectorSpace(self.base_ring(), elt.nrows()*elt.ncols())
-        W = V.span_of_basis( (_mat2vec(s) for s in self.matrix_basis()),
+        elt = _all2list(elt)
+        V = VectorSpace(self.base_ring(), len(elt))
+        W = V.span_of_basis( (V(_all2list(s)) for s in self.matrix_basis()),
                              check=False)
 
         try:
-            coords =  W.coordinate_vector(_mat2vec(elt))
+            coords = W.coordinate_vector(V(elt))
         except ArithmeticError:  # vector is not in free module
             raise ValueError(msg)
 
@@ -763,12 +967,49 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
         we think of them as matrices (including column vectors of the
         appropriate size).
 
-        Generally this will be an `n`-by-`1` column-vector space,
+        "By default" this will be an `n`-by-`1` column-matrix space,
         except when the algebra is trivial. There it's `n`-by-`n`
         (where `n` is zero), to ensure that two elements of the matrix
-        space (empty matrices) can be multiplied.
+        space (empty matrices) can be multiplied. For algebras of
+        matrices, this returns the space in which their
+        real embeddings live.
+
+        SETUP::
+
+            sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
+            ....:                                  JordanSpinEJA,
+            ....:                                  QuaternionHermitianEJA,
+            ....:                                  TrivialEJA)
+
+        EXAMPLES:
+
+        By default, the matrix representation is just a column-matrix
+        equivalent to the vector representation::
+
+            sage: J = JordanSpinEJA(3)
+            sage: J.matrix_space()
+            Full MatrixSpace of 3 by 1 dense matrices over Algebraic
+            Real Field
+
+        The matrix representation in the trivial algebra is
+        zero-by-zero instead of the usual `n`-by-one::
+
+            sage: J = TrivialEJA()
+            sage: J.matrix_space()
+            Full MatrixSpace of 0 by 0 dense matrices over Algebraic
+            Real Field
+
+        The matrix space for complex/quaternion Hermitian matrix EJA
+        is the space in which their real-embeddings live, not the
+        original complex/quaternion matrix space::
+
+            sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
+            sage: J.matrix_space()
+            Full MatrixSpace of 4 by 4 dense matrices over Rational Field
+            sage: J = QuaternionHermitianEJA(1,field=QQ,orthonormalize=False)
+            sage: J.matrix_space()
+            Full MatrixSpace of 4 by 4 dense matrices over Rational Field
 
-        Matrix algebras override this with something more useful.
         """
         if self.is_trivial():
             return MatrixSpace(self.base_ring(), 0)
@@ -1029,14 +1270,12 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
         if not c.is_idempotent():
             raise ValueError("element is not idempotent: %s" % c)
 
-        from mjo.eja.eja_subalgebra import FiniteDimensionalEJASubalgebra
-
         # Default these to what they should be if they turn out to be
         # trivial, because eigenspaces_left() won't return eigenvalues
         # corresponding to trivial spaces (e.g. it returns only the
         # eigenspace corresponding to lambda=1 if you take the
         # decomposition relative to the identity element).
-        trivial = FiniteDimensionalEJASubalgebra(self, ())
+        trivial = self.subalgebra(())
         J0 = trivial                          # eigenvalue zero
         J5 = VectorSpace(self.base_ring(), 0) # eigenvalue one-half
         J1 = trivial                          # eigenvalue one
@@ -1046,9 +1285,7 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
                 J5 = eigspace
             else:
                 gens = tuple( self.from_vector(b) for b in eigspace.basis() )
-                subalg = FiniteDimensionalEJASubalgebra(self,
-                                                        gens,
-                                                        check_axioms=False)
+                subalg = self.subalgebra(gens, check_axioms=False)
                 if eigval == 0:
                     J0 = subalg
                 elif eigval == 1:
@@ -1267,6 +1504,14 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
         return len(self._charpoly_coefficients())
 
 
+    def subalgebra(self, basis, **kwargs):
+        r"""
+        Create a subalgebra of this algebra from the given basis.
+        """
+        from mjo.eja.eja_subalgebra import FiniteDimensionalEJASubalgebra
+        return FiniteDimensionalEJASubalgebra(self, basis, **kwargs)
+
+
     def vector_space(self):
         """
         Return the vector space that underlies this algebra.
@@ -1285,7 +1530,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule):
         return self.zero().to_vector().parent().ambient_vector_space()
 
 
-    Element = FiniteDimensionalEJAElement
 
 class RationalBasisEJA(FiniteDimensionalEJA):
     r"""
@@ -1619,9 +1863,9 @@ class RealSymmetricEJA(ConcreteEJA, RealMatrixEJA):
 
     In theory, our "field" can be any subfield of the reals::
 
-        sage: RealSymmetricEJA(2, field=RDF)
+        sage: RealSymmetricEJA(2, field=RDF, check_axioms=True)
         Euclidean Jordan algebra of dimension 3 over Real Double Field
-        sage: RealSymmetricEJA(2, field=RR)
+        sage: RealSymmetricEJA(2, field=RR, check_axioms=True)
         Euclidean Jordan algebra of dimension 3 over Real Field with
         53 bits of precision
 
@@ -1887,9 +2131,9 @@ class ComplexHermitianEJA(ConcreteEJA, ComplexMatrixEJA):
 
     In theory, our "field" can be any subfield of the reals::
 
-        sage: ComplexHermitianEJA(2, field=RDF)
+        sage: ComplexHermitianEJA(2, field=RDF, check_axioms=True)
         Euclidean Jordan algebra of dimension 4 over Real Double Field
-        sage: ComplexHermitianEJA(2, field=RR)
+        sage: ComplexHermitianEJA(2, field=RR, check_axioms=True)
         Euclidean Jordan algebra of dimension 4 over Real Field with
         53 bits of precision
 
@@ -2184,9 +2428,9 @@ class QuaternionHermitianEJA(ConcreteEJA, QuaternionMatrixEJA):
 
     In theory, our "field" can be any subfield of the reals::
 
-        sage: QuaternionHermitianEJA(2, field=RDF)
+        sage: QuaternionHermitianEJA(2, field=RDF, check_axioms=True)
         Euclidean Jordan algebra of dimension 6 over Real Double Field
-        sage: QuaternionHermitianEJA(2, field=RR)
+        sage: QuaternionHermitianEJA(2, field=RR, check_axioms=True)
         Euclidean Jordan algebra of dimension 6 over Real Field with
         53 bits of precision
 
@@ -2836,6 +3080,9 @@ class CartesianProductEJA(CombinatorialFreeModule_CartesianProduct,
         True
 
     """
+    Element = FiniteDimensionalEJAElement
+
+
     def __init__(self, algebras, **kwargs):
         CombinatorialFreeModule_CartesianProduct.__init__(self,
                                                           algebras,
@@ -3099,43 +3346,45 @@ class CartesianProductEJA(CombinatorialFreeModule_CartesianProduct,
         return FiniteDimensionalEJAOperator(Ji,self,Ei.matrix())
 
 
-    def _element_constructor_(self, elt):
-        r"""
-        Construct an element of this algebra from an ordered tuple.
 
-        We just apply the element constructor from each of our factors
-        to the corresponding component of the tuple, and package up
-        the result.
+FiniteDimensionalEJA.CartesianProduct = CartesianProductEJA
 
-        SETUP::
+class RationalBasisCartesianProductEJA(CartesianProductEJA,
+                                       RationalBasisEJA):
+    r"""
+    A separate class for products of algebras for which we know a
+    rational basis.
 
-            sage: from mjo.eja.eja_algebra import (HadamardEJA,
-            ....:                                  RealSymmetricEJA)
+    SETUP::
 
-        EXAMPLES::
+        sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
+        ....:                                  RealSymmetricEJA)
 
-            sage: J1 = HadamardEJA(3)
-            sage: J2 = RealSymmetricEJA(2)
-            sage: J = cartesian_product([J1,J2])
-            sage: J( (J1.matrix_basis()[1], J2.matrix_basis()[2]) )
-            e(0, 1) + e(1, 2)
-        """
-        m = len(self.cartesian_factors())
-        try:
-            z = tuple( self.cartesian_factors()[i](elt[i]) for i in range(m) )
-            return self._cartesian_product_of_elements(z)
-        except:
-            raise ValueError("not an element of this algebra")
+    EXAMPLES:
 
-    Element = CartesianProductEJAElement
+    This gives us fast characteristic polynomial computations in
+    product algebras, too::
 
 
-FiniteDimensionalEJA.CartesianProduct = CartesianProductEJA
+        sage: J1 = JordanSpinEJA(2)
+        sage: J2 = RealSymmetricEJA(3)
+        sage: J = cartesian_product([J1,J2])
+        sage: J.characteristic_polynomial_of().degree()
+        5
+        sage: J.rank()
+        5
+
+    """
+    def __init__(self, algebras, **kwargs):
+        CartesianProductEJA.__init__(self, algebras, **kwargs)
+
+        self._rational_algebra = None
+        if self.vector_space().base_field() is not QQ:
+            self._rational_algebra = cartesian_product([
+                r._rational_algebra for r in algebras
+            ])
+
+
+RationalBasisEJA.CartesianProduct = RationalBasisCartesianProductEJA
 
 random_eja = ConcreteEJA.random_instance
-#def random_eja(*args, **kwargs):
-#    from sage.categories.cartesian_product import cartesian_product
-#    J1 = HadamardEJA(1, **kwargs)
-#    J2 = RealSymmetricEJA(2, **kwargs)
-#    J =  cartesian_product([J1,J2])
-#    return J