]> gitweb.michael.orlitzky.com - sage.d.git/commitdiff
eja: refactor the class hierarchy to separate the matrix EJAs.
authorMichael Orlitzky <michael@orlitzky.com>
Fri, 23 Aug 2019 20:14:18 +0000 (16:14 -0400)
committerMichael Orlitzky <michael@orlitzky.com>
Fri, 23 Aug 2019 20:14:18 +0000 (16:14 -0400)
mjo/eja/TODO
mjo/eja/eja_algebra.py

index f6d7743782613d241c50e05eee665d59278bd4b4..89b4d329b76d848c0caf1528c1c78b7faec1ff9a 100644 (file)
 6. Refactor the current ungodly fast charpoly hack (relies on the
    theory to ensure that the charpolys are equal.)
 
-7. If we factor out a "matrix algebra" class, then it would make sense
-   to replace the custom embedding/unembedding functions with static
-   _real_embedding() and _real_unembedding() methods.
+7. Implement random_instance() for the main EJA class.
 
-8. Implement random_instance() for the main EJA class.
-
-9. Implement random_instance() for the subalgebra class.
+8. Implement random_instance() for the subalgebra class.
index f687e46215a1f1aefb5a5955acffd4fc4c6be301..a207250b4092f97e07d93a62c0ed7e23d9f9536d 100644 (file)
@@ -890,414 +890,123 @@ def random_eja():
 
 
 
-def _real_symmetric_basis(n, field):
-    """
-    Return a basis for the space of real symmetric n-by-n matrices.
-
-    SETUP::
-
-        sage: from mjo.eja.eja_algebra import _real_symmetric_basis
-
-    TESTS::
-
-        sage: set_random_seed()
-        sage: n = ZZ.random_element(1,5)
-        sage: B = _real_symmetric_basis(n, QQ)
-        sage: all( M.is_symmetric() for M in  B)
-        True
-
-    """
-    # The basis of symmetric matrices, as matrices, in their R^(n-by-n)
-    # coordinates.
-    S = []
-    for i in xrange(n):
-        for j in xrange(i+1):
-            Eij = matrix(field, n, lambda k,l: k==i and l==j)
-            if i == j:
-                Sij = Eij
-            else:
-                Sij = Eij + Eij.transpose()
-            S.append(Sij)
-    return tuple(S)
-
-
-def _complex_hermitian_basis(n, field):
-    """
-    Returns a basis for the space of complex Hermitian n-by-n matrices.
-
-    Why do we embed these? Basically, because all of numerical linear
-    algebra assumes that you're working with vectors consisting of `n`
-    entries from a field and scalars from the same field. There's no way
-    to tell SageMath that (for example) the vectors contain complex
-    numbers, while the scalar field is real.
-
-    SETUP::
-
-        sage: from mjo.eja.eja_algebra import _complex_hermitian_basis
-
-    TESTS::
-
-        sage: set_random_seed()
-        sage: n = ZZ.random_element(1,5)
-        sage: field = QuadraticField(2, 'sqrt2')
-        sage: B = _complex_hermitian_basis(n, field)
-        sage: all( M.is_symmetric() for M in  B)
-        True
-
-    """
-    R = PolynomialRing(field, 'z')
-    z = R.gen()
-    F = NumberField(z**2 + 1, 'I', embedding=CLF(-1).sqrt())
-    I = F.gen()
-
-    # This is like the symmetric case, but we need to be careful:
-    #
-    #   * We want conjugate-symmetry, not just symmetry.
-    #   * The diagonal will (as a result) be real.
-    #
-    S = []
-    for i in xrange(n):
-        for j in xrange(i+1):
-            Eij = matrix(F, n, lambda k,l: k==i and l==j)
-            if i == j:
-                Sij = _embed_complex_matrix(Eij)
-                S.append(Sij)
-            else:
-                # The second one has a minus because it's conjugated.
-                Sij_real = _embed_complex_matrix(Eij + Eij.transpose())
-                S.append(Sij_real)
-                Sij_imag = _embed_complex_matrix(I*Eij - I*Eij.transpose())
-                S.append(Sij_imag)
 
-    # Since we embedded these, we can drop back to the "field" that we
-    # started with instead of the complex extension "F".
-    return tuple( s.change_ring(field) for s in S )
 
 
+class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
+    @staticmethod
+    def _max_test_case_size():
+        # Play it safe, since this will be squared and the underlying
+        # field can have dimension 4 (quaternions) too.
+        return 3
 
-def _quaternion_hermitian_basis(n, field):
-    """
-    Returns a basis for the space of quaternion Hermitian n-by-n matrices.
-
-    Why do we embed these? Basically, because all of numerical linear
-    algebra assumes that you're working with vectors consisting of `n`
-    entries from a field and scalars from the same field. There's no way
-    to tell SageMath that (for example) the vectors contain complex
-    numbers, while the scalar field is real.
-
-    SETUP::
-
-        sage: from mjo.eja.eja_algebra import _quaternion_hermitian_basis
-
-    TESTS::
-
-        sage: set_random_seed()
-        sage: n = ZZ.random_element(1,5)
-        sage: B = _quaternion_hermitian_basis(n, QQ)
-        sage: all( M.is_symmetric() for M in B )
-        True
-
-    """
-    Q = QuaternionAlgebra(QQ,-1,-1)
-    I,J,K = Q.gens()
-
-    # This is like the symmetric case, but we need to be careful:
-    #
-    #   * We want conjugate-symmetry, not just symmetry.
-    #   * The diagonal will (as a result) be real.
-    #
-    S = []
-    for i in xrange(n):
-        for j in xrange(i+1):
-            Eij = matrix(Q, n, lambda k,l: k==i and l==j)
-            if i == j:
-                Sij = _embed_quaternion_matrix(Eij)
-                S.append(Sij)
-            else:
-                # Beware, orthogonal but not normalized! The second,
-                # third, and fourth ones have a minus because they're
-                # conjugated.
-                Sij_real = _embed_quaternion_matrix(Eij + Eij.transpose())
-                S.append(Sij_real)
-                Sij_I = _embed_quaternion_matrix(I*Eij - I*Eij.transpose())
-                S.append(Sij_I)
-                Sij_J = _embed_quaternion_matrix(J*Eij - J*Eij.transpose())
-                S.append(Sij_J)
-                Sij_K = _embed_quaternion_matrix(K*Eij - K*Eij.transpose())
-                S.append(Sij_K)
-    return tuple(S)
-
-
-
-def _multiplication_table_from_matrix_basis(basis):
-    """
-    At least three of the five simple Euclidean Jordan algebras have the
-    symmetric multiplication (A,B) |-> (AB + BA)/2, where the
-    multiplication on the right is matrix multiplication. Given a basis
-    for the underlying matrix space, this function returns a
-    multiplication table (obtained by looping through the basis
-    elements) for an algebra of those matrices.
-    """
-    # In S^2, for example, we nominally have four coordinates even
-    # though the space is of dimension three only. The vector space V
-    # is supposed to hold the entire long vector, and the subspace W
-    # of V will be spanned by the vectors that arise from symmetric
-    # matrices. Thus for S^2, dim(V) == 4 and dim(W) == 3.
-    field = basis[0].base_ring()
-    dimension = basis[0].nrows()
-
-    V = VectorSpace(field, dimension**2)
-    W = V.span_of_basis( _mat2vec(s) for s in basis )
-    n = len(basis)
-    mult_table = [[W.zero() for 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
-
-
-def _embed_complex_matrix(M):
-    """
-    Embed the n-by-n complex matrix ``M`` into the space of real
-    matrices of size 2n-by-2n via the map the sends each entry `z = a +
-    bi` to the block matrix ``[[a,b],[-b,a]]``.
-
-    SETUP::
-
-        sage: from mjo.eja.eja_algebra import (_embed_complex_matrix,
-        ....:                                  ComplexHermitianEJA)
-
-    EXAMPLES::
-
-        sage: F = QuadraticField(-1, 'i')
-        sage: x1 = F(4 - 2*i)
-        sage: x2 = F(1 + 2*i)
-        sage: x3 = F(-i)
-        sage: x4 = F(6)
-        sage: M = matrix(F,2,[[x1,x2],[x3,x4]])
-        sage: _embed_complex_matrix(M)
-        [ 4 -2| 1  2]
-        [ 2  4|-2  1]
-        [-----+-----]
-        [ 0 -1| 6  0]
-        [ 1  0| 0  6]
-
-    TESTS:
-
-    Embedding is a homomorphism (isomorphism, in fact)::
-
-        sage: set_random_seed()
-        sage: n_max = ComplexHermitianEJA._max_test_case_size()
-        sage: n = ZZ.random_element(n_max)
-        sage: F = QuadraticField(-1, 'i')
-        sage: X = random_matrix(F, n)
-        sage: Y = random_matrix(F, n)
-        sage: actual = _embed_complex_matrix(X) * _embed_complex_matrix(Y)
-        sage: expected = _embed_complex_matrix(X*Y)
-        sage: actual == expected
-        True
-
-    """
-    n = M.nrows()
-    if M.ncols() != n:
-        raise ValueError("the matrix 'M' must be square")
-    field = M.base_ring()
-    blocks = []
-    for z in M.list():
-        a = z.vector()[0] # real part, I guess
-        b = z.vector()[1] # imag part, I guess
-        blocks.append(matrix(field, 2, [[a,b],[-b,a]]))
-
-    # We can drop the imaginaries here.
-    return matrix.block(field.base_ring(), n, blocks)
-
-
-def _unembed_complex_matrix(M):
-    """
-    The inverse of _embed_complex_matrix().
-
-    SETUP::
-
-        sage: from mjo.eja.eja_algebra import (_embed_complex_matrix,
-        ....:                                  _unembed_complex_matrix)
-
-    EXAMPLES::
-
-        sage: A = matrix(QQ,[ [ 1,  2,   3,  4],
-        ....:                 [-2,  1,  -4,  3],
-        ....:                 [ 9,  10, 11, 12],
-        ....:                 [-10, 9, -12, 11] ])
-        sage: _unembed_complex_matrix(A)
-        [  2*i + 1   4*i + 3]
-        [ 10*i + 9 12*i + 11]
-
-    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
+    @classmethod
+    def _denormalized_basis(cls, n, field):
+        raise NotImplementedError
 
-    """
-    n = ZZ(M.nrows())
-    if M.ncols() != n:
-        raise ValueError("the matrix 'M' must be square")
-    if not n.mod(2).is_zero():
-        raise ValueError("the matrix 'M' must be a complex embedding")
-
-    field = M.base_ring() # This should already have sqrt2
-    R = PolynomialRing(field, 'z')
-    z = R.gen()
-    F = NumberField(z**2 + 1,'i', embedding=CLF(-1).sqrt())
-    i = F.gen()
-
-    # Go top-left to bottom-right (reading order), converting every
-    # 2-by-2 block we see to a single complex element.
-    elements = []
-    for k in xrange(n/2):
-        for j in xrange(n/2):
-            submat = M[2*k:2*k+2,2*j:2*j+2]
-            if submat[0,0] != submat[1,1]:
-                raise ValueError('bad on-diagonal submatrix')
-            if submat[0,1] != -submat[1,0]:
-                raise ValueError('bad off-diagonal submatrix')
-            z = submat[0,0] + submat[0,1]*i
-            elements.append(z)
-
-    return matrix(F, n/2, elements)
-
-
-def _embed_quaternion_matrix(M):
-    """
-    Embed the n-by-n quaternion matrix ``M`` into the space of real
-    matrices of size 4n-by-4n by first sending each quaternion entry
-    `z = a + bi + cj + dk` to the block-complex matrix
-    ``[[a + bi, c+di],[-c + di, a-bi]]`, and then embedding those into
-    a real matrix.
+    def __init__(self, n, field=QQ, normalize_basis=True, **kwargs):
+        S = self._denormalized_basis(n, field)
 
-    SETUP::
+        if n > 1 and normalize_basis:
+            # We'll need sqrt(2) to normalize the basis, and this
+            # winds up in the multiplication table, so the whole
+            # algebra needs to be over the field extension.
+            R = PolynomialRing(field, 'z')
+            z = R.gen()
+            p = z**2 - 2
+            if p.is_irreducible():
+                field = NumberField(p, 'sqrt2', embedding=RLF(2).sqrt())
+                S = [ s.change_ring(field) for s in S ]
+            self._basis_normalizers = tuple(
+                ~(self.natural_inner_product(s,s).sqrt()) for s in S )
+            S = tuple( s*c for (s,c) in zip(S,self._basis_normalizers) )
 
-        sage: from mjo.eja.eja_algebra import (_embed_quaternion_matrix,
-        ....:                                  QuaternionHermitianEJA)
+        Qs = self.multiplication_table_from_matrix_basis(S)
 
-    EXAMPLES::
+        fdeja = super(MatrixEuclideanJordanAlgebra, self)
+        return fdeja.__init__(field,
+                              Qs,
+                              rank=n,
+                              natural_basis=S,
+                              **kwargs)
 
-        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)::
+    @staticmethod
+    def multiplication_table_from_matrix_basis(basis):
+        """
+        At least three of the five simple Euclidean Jordan algebras have the
+        symmetric multiplication (A,B) |-> (AB + BA)/2, where the
+        multiplication on the right is matrix multiplication. Given a basis
+        for the underlying matrix space, this function returns a
+        multiplication table (obtained by looping through the basis
+        elements) for an algebra of those matrices.
+        """
+        # In S^2, for example, we nominally have four coordinates even
+        # though the space is of dimension three only. The vector space V
+        # is supposed to hold the entire long vector, and the subspace W
+        # of V will be spanned by the vectors that arise from symmetric
+        # matrices. Thus for S^2, dim(V) == 4 and dim(W) == 3.
+        field = basis[0].base_ring()
+        dimension = basis[0].nrows()
+
+        V = VectorSpace(field, dimension**2)
+        W = V.span_of_basis( _mat2vec(s) for s in basis )
+        n = len(basis)
+        mult_table = [[W.zero() for j in range(n)] for i in range(n)]
+        for i in range(n):
+            for j in range(n):
+                mat_entry = (basis[i]*basis[j] + basis[j]*basis[i])/2
+                mult_table[i][j] = W.coordinate_vector(_mat2vec(mat_entry))
 
-        sage: set_random_seed()
-        sage: n_max = QuaternionHermitianEJA._max_test_case_size()
-        sage: n = ZZ.random_element(n_max)
-        sage: Q = QuaternionAlgebra(QQ,-1,-1)
-        sage: X = random_matrix(Q, n)
-        sage: Y = random_matrix(Q, n)
-        sage: actual = _embed_quaternion_matrix(X)*_embed_quaternion_matrix(Y)
-        sage: expected = _embed_quaternion_matrix(X*Y)
-        sage: actual == expected
-        True
+        return mult_table
 
-    """
-    quaternions = M.base_ring()
-    n = M.nrows()
-    if M.ncols() != n:
-        raise ValueError("the matrix 'M' must be square")
-
-    F = QuadraticField(-1, 'i')
-    i = F.gen()
-
-    blocks = []
-    for z in M.list():
-        t = z.coefficient_tuple()
-        a = t[0]
-        b = t[1]
-        c = t[2]
-        d = t[3]
-        cplx_matrix = matrix(F, 2, [[ a + b*i, c + d*i],
-                                    [-c + d*i, a - b*i]])
-        blocks.append(_embed_complex_matrix(cplx_matrix))
-
-    # We should have real entries by now, so use the realest field
-    # we've got for the return value.
-    return matrix.block(quaternions.base_ring(), n, blocks)
-
-
-def _unembed_quaternion_matrix(M):
-    """
-    The inverse of _embed_quaternion_matrix().
 
-    SETUP::
+    @staticmethod
+    def real_embed(M):
+        """
+        Embed the matrix ``M`` into a space of real matrices.
 
-        sage: from mjo.eja.eja_algebra import (_embed_quaternion_matrix,
-        ....:                                  _unembed_quaternion_matrix)
+        The matrix ``M`` can have entries in any field at the moment:
+        the real numbers, complex numbers, or quaternions. And although
+        they are not a field, we can probably support octonions at some
+        point, too. This function returns a real matrix that "acts like"
+        the original with respect to matrix multiplication; i.e.
 
-    EXAMPLES::
+          real_embed(M*N) = real_embed(M)*real_embed(N)
 
-        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]
+        """
+        raise NotImplementedError
 
-    TESTS:
 
-    Unembedding is the inverse of embedding::
+    @staticmethod
+    def real_unembed(M):
+        """
+        The inverse of :meth:`real_embed`.
+        """
+        raise NotImplementedError
 
-        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")
-
-    # Use the base ring of the matrix to ensure that its entries can be
-    # multiplied by elements of the quaternion algebra.
-    field = M.base_ring()
-    Q = QuaternionAlgebra(field,-1,-1)
-    i,j,k = Q.gens()
-
-    # Go top-left to bottom-right (reading order), converting every
-    # 4-by-4 block we see to a 2-by-2 complex block, to a 1-by-1
-    # quaternion block.
-    elements = []
-    for l in 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].vector()[0]   # real part
-            z += submat[0,0].vector()[1]*i # imag part
-            z += submat[0,1].vector()[0]*j # real part
-            z += submat[0,1].vector()[1]*k # imag part
-            elements.append(z)
-
-    return matrix(Q, n/4, elements)
-
-
-
-class RealSymmetricEJA(FiniteDimensionalEuclideanJordanAlgebra):
+    @classmethod
+    def natural_inner_product(cls,X,Y):
+        Xu = cls.real_unembed(X)
+        Yu = cls.real_unembed(Y)
+        tr = (Xu*Yu).trace()
+        if tr in RLF:
+            # It's real already.
+            return tr
+
+        # Otherwise, try the thing that works for complex numbers; and
+        # if that doesn't work, the thing that works for quaternions.
+        try:
+            return tr.vector()[0] # real part, imag part is index 1
+        except AttributeError:
+            # A quaternions doesn't have a vector() method, but does
+            # have coefficient_tuple() method that returns the
+            # coefficients of 1, i, j, and k -- in that order.
+            return tr.coefficient_tuple()[0]
+
+
+class RealSymmetricEJA(MatrixEuclideanJordanAlgebra):
     """
     The rank-n simple EJA consisting of real symmetric n-by-n
     matrices, the usual symmetric Jordan product, and the trace inner
@@ -1379,38 +1088,188 @@ class RealSymmetricEJA(FiniteDimensionalEuclideanJordanAlgebra):
         True
 
     """
-    def __init__(self, n, field=QQ, normalize_basis=True, **kwargs):
-        S = _real_symmetric_basis(n, field)
+    @classmethod
+    def _denormalized_basis(cls, n, field):
+        """
+        Return a basis for the space of real symmetric n-by-n matrices.
 
-        if n > 1 and normalize_basis:
-            # We'll need sqrt(2) to normalize the basis, and this
-            # winds up in the multiplication table, so the whole
-            # algebra needs to be over the field extension.
-            R = PolynomialRing(field, 'z')
-            z = R.gen()
-            p = z**2 - 2
-            if p.is_irreducible():
-                field = NumberField(p, 'sqrt2', embedding=RLF(2).sqrt())
-                S = [ s.change_ring(field) for s in S ]
-            self._basis_normalizers = tuple(
-                ~(self.natural_inner_product(s,s).sqrt()) for s in S )
-            S = tuple( s*c for (s,c) in zip(S,self._basis_normalizers) )
+        SETUP::
+
+            sage: from mjo.eja.eja_algebra import RealSymmetricEJA
 
-        Qs = _multiplication_table_from_matrix_basis(S)
+        TESTS::
+
+            sage: set_random_seed()
+            sage: n = ZZ.random_element(1,5)
+            sage: B = RealSymmetricEJA._denormalized_basis(n,QQ)
+            sage: all( M.is_symmetric() for M in  B)
+            True
+
+        """
+        # The basis of symmetric matrices, as matrices, in their R^(n-by-n)
+        # coordinates.
+        S = []
+        for i in xrange(n):
+            for j in xrange(i+1):
+                Eij = matrix(field, n, lambda k,l: k==i and l==j)
+                if i == j:
+                    Sij = Eij
+                else:
+                    Sij = Eij + Eij.transpose()
+                S.append(Sij)
+        return tuple(S)
 
-        fdeja = super(RealSymmetricEJA, self)
-        return fdeja.__init__(field,
-                              Qs,
-                              rank=n,
-                              natural_basis=S,
-                              **kwargs)
 
     @staticmethod
     def _max_test_case_size():
-        return 5
+        return 5 # Dimension 10
+
+    @staticmethod
+    def real_embed(M):
+        """
+        Embed the matrix ``M`` into a space of real matrices.
+
+        The matrix ``M`` can have entries in any field at the moment:
+        the real numbers, complex numbers, or quaternions. And although
+        they are not a field, we can probably support octonions at some
+        point, too. This function returns a real matrix that "acts like"
+        the original with respect to matrix multiplication; i.e.
+
+          real_embed(M*N) = real_embed(M)*real_embed(N)
+
+        """
+        return M
+
+
+    @staticmethod
+    def real_unembed(M):
+        """
+        The inverse of :meth:`real_embed`.
+        """
+        return M
+
+
 
+class ComplexMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
+    @staticmethod
+    def real_embed(M):
+        """
+        Embed the n-by-n complex matrix ``M`` into the space of real
+        matrices of size 2n-by-2n via the map the sends each entry `z = a +
+        bi` to the block matrix ``[[a,b],[-b,a]]``.
+
+        SETUP::
+
+            sage: from mjo.eja.eja_algebra import \
+            ....:   ComplexMatrixEuclideanJordanAlgebra
+
+        EXAMPLES::
+
+            sage: F = QuadraticField(-1, 'i')
+            sage: x1 = F(4 - 2*i)
+            sage: x2 = F(1 + 2*i)
+            sage: x3 = F(-i)
+            sage: x4 = F(6)
+            sage: M = matrix(F,2,[[x1,x2],[x3,x4]])
+            sage: ComplexMatrixEuclideanJordanAlgebra.real_embed(M)
+            [ 4 -2| 1  2]
+            [ 2  4|-2  1]
+            [-----+-----]
+            [ 0 -1| 6  0]
+            [ 1  0| 0  6]
+
+        TESTS:
 
-class ComplexHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra):
+        Embedding is a homomorphism (isomorphism, in fact)::
+
+            sage: set_random_seed()
+            sage: n_max = ComplexMatrixEuclideanJordanAlgebra._max_test_case_size()
+            sage: n = ZZ.random_element(n_max)
+            sage: F = QuadraticField(-1, 'i')
+            sage: X = random_matrix(F, n)
+            sage: Y = random_matrix(F, n)
+            sage: Xe = ComplexMatrixEuclideanJordanAlgebra.real_embed(X)
+            sage: Ye = ComplexMatrixEuclideanJordanAlgebra.real_embed(Y)
+            sage: XYe = ComplexMatrixEuclideanJordanAlgebra.real_embed(X*Y)
+            sage: Xe*Ye == XYe
+            True
+
+        """
+        n = M.nrows()
+        if M.ncols() != n:
+            raise ValueError("the matrix 'M' must be square")
+        field = M.base_ring()
+        blocks = []
+        for z in M.list():
+            a = z.vector()[0] # real part, I guess
+            b = z.vector()[1] # imag part, I guess
+            blocks.append(matrix(field, 2, [[a,b],[-b,a]]))
+
+        # We can drop the imaginaries here.
+        return matrix.block(field.base_ring(), n, blocks)
+
+
+    @staticmethod
+    def real_unembed(M):
+        """
+        The inverse of _embed_complex_matrix().
+
+        SETUP::
+
+            sage: from mjo.eja.eja_algebra import \
+            ....:   ComplexMatrixEuclideanJordanAlgebra
+
+        EXAMPLES::
+
+            sage: A = matrix(QQ,[ [ 1,  2,   3,  4],
+            ....:                 [-2,  1,  -4,  3],
+            ....:                 [ 9,  10, 11, 12],
+            ....:                 [-10, 9, -12, 11] ])
+            sage: ComplexMatrixEuclideanJordanAlgebra.real_unembed(A)
+            [  2*i + 1   4*i + 3]
+            [ 10*i + 9 12*i + 11]
+
+        TESTS:
+
+        Unembedding is the inverse of embedding::
+
+            sage: set_random_seed()
+            sage: F = QuadraticField(-1, 'i')
+            sage: M = random_matrix(F, 3)
+            sage: Me = ComplexMatrixEuclideanJordanAlgebra.real_embed(M)
+            sage: ComplexMatrixEuclideanJordanAlgebra.real_unembed(Me) == M
+            True
+
+        """
+        n = ZZ(M.nrows())
+        if M.ncols() != n:
+            raise ValueError("the matrix 'M' must be square")
+        if not n.mod(2).is_zero():
+            raise ValueError("the matrix 'M' must be a complex embedding")
+
+        field = M.base_ring() # This should already have sqrt2
+        R = PolynomialRing(field, 'z')
+        z = R.gen()
+        F = NumberField(z**2 + 1,'i', embedding=CLF(-1).sqrt())
+        i = F.gen()
+
+        # Go top-left to bottom-right (reading order), converting every
+        # 2-by-2 block we see to a single complex element.
+        elements = []
+        for k in xrange(n/2):
+            for j in xrange(n/2):
+                submat = M[2*k:2*k+2,2*j:2*j+2]
+                if submat[0,0] != submat[1,1]:
+                    raise ValueError('bad on-diagonal submatrix')
+                if submat[0,1] != -submat[1,0]:
+                    raise ValueError('bad off-diagonal submatrix')
+                z = submat[0,0] + submat[0,1]*i
+                elements.append(z)
+
+        return matrix(F, n/2, elements)
+
+
+class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra):
     """
     The rank-n simple EJA consisting of complex Hermitian n-by-n
     matrices over the real numbers, the usual symmetric Jordan product,
@@ -1482,47 +1341,195 @@ class ComplexHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra):
         True
 
     """
-    def __init__(self, n, field=QQ, normalize_basis=True, **kwargs):
-        S = _complex_hermitian_basis(n, field)
+    @classmethod
+    def _denormalized_basis(cls, n, field):
+        """
+        Returns a basis for the space of complex Hermitian n-by-n matrices.
 
-        if n > 1 and normalize_basis:
-            # We'll need sqrt(2) to normalize the basis, and this
-            # winds up in the multiplication table, so the whole
-            # algebra needs to be over the field extension.
-            R = PolynomialRing(field, 'z')
-            z = R.gen()
-            p = z**2 - 2
-            if p.is_irreducible():
-                field = NumberField(p, 'sqrt2', embedding=RLF(2).sqrt())
-                S = [ s.change_ring(field) for s in S ]
-            self._basis_normalizers = tuple(
-                ~(self.natural_inner_product(s,s).sqrt()) for s in S )
-            S = tuple( s*c for (s,c) in zip(S,self._basis_normalizers) )
+        Why do we embed these? Basically, because all of numerical linear
+        algebra assumes that you're working with vectors consisting of `n`
+        entries from a field and scalars from the same field. There's no way
+        to tell SageMath that (for example) the vectors contain complex
+        numbers, while the scalar field is real.
 
-        Qs = _multiplication_table_from_matrix_basis(S)
+        SETUP::
 
-        fdeja = super(ComplexHermitianEJA, self)
-        return fdeja.__init__(field,
-                              Qs,
-                              rank=n,
-                              natural_basis=S,
-                              **kwargs)
+            sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
+
+        TESTS::
+
+            sage: set_random_seed()
+            sage: n = ZZ.random_element(1,5)
+            sage: field = QuadraticField(2, 'sqrt2')
+            sage: B = ComplexHermitianEJA._denormalized_basis(n, field)
+            sage: all( M.is_symmetric() for M in  B)
+            True
 
+        """
+        R = PolynomialRing(field, 'z')
+        z = R.gen()
+        F = NumberField(z**2 + 1, 'I', embedding=CLF(-1).sqrt())
+        I = F.gen()
 
+        # This is like the symmetric case, but we need to be careful:
+        #
+        #   * We want conjugate-symmetry, not just symmetry.
+        #   * The diagonal will (as a result) be real.
+        #
+        S = []
+        for i in xrange(n):
+            for j in xrange(i+1):
+                Eij = matrix(F, n, lambda k,l: k==i and l==j)
+                if i == j:
+                    Sij = cls.real_embed(Eij)
+                    S.append(Sij)
+                else:
+                    # The second one has a minus because it's conjugated.
+                    Sij_real = cls.real_embed(Eij + Eij.transpose())
+                    S.append(Sij_real)
+                    Sij_imag = cls.real_embed(I*Eij - I*Eij.transpose())
+                    S.append(Sij_imag)
+
+        # Since we embedded these, we can drop back to the "field" that we
+        # started with instead of the complex extension "F".
+        return tuple( s.change_ring(field) for s in S )
+
+
+
+class QuaternionMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
     @staticmethod
-    def _max_test_case_size():
-        return 4
+    def real_embed(M):
+        """
+        Embed the n-by-n quaternion matrix ``M`` into the space of real
+        matrices of size 4n-by-4n by first sending each quaternion entry `z
+        = a + bi + cj + dk` to the block-complex matrix ``[[a + bi,
+        c+di],[-c + di, a-bi]]`, and then embedding those into a real
+        matrix.
+
+        SETUP::
+
+            sage: from mjo.eja.eja_algebra import \
+            ....:   QuaternionMatrixEuclideanJordanAlgebra
+
+        EXAMPLES::
+
+            sage: Q = QuaternionAlgebra(QQ,-1,-1)
+            sage: i,j,k = Q.gens()
+            sage: x = 1 + 2*i + 3*j + 4*k
+            sage: M = matrix(Q, 1, [[x]])
+            sage: QuaternionMatrixEuclideanJordanAlgebra.real_embed(M)
+            [ 1  2  3  4]
+            [-2  1 -4  3]
+            [-3  4  1 -2]
+            [-4 -3  2  1]
+
+        Embedding is a homomorphism (isomorphism, in fact)::
+
+            sage: set_random_seed()
+            sage: n_max = QuaternionMatrixEuclideanJordanAlgebra._max_test_case_size()
+            sage: n = ZZ.random_element(n_max)
+            sage: Q = QuaternionAlgebra(QQ,-1,-1)
+            sage: X = random_matrix(Q, n)
+            sage: Y = random_matrix(Q, n)
+            sage: Xe = QuaternionMatrixEuclideanJordanAlgebra.real_embed(X)
+            sage: Ye = QuaternionMatrixEuclideanJordanAlgebra.real_embed(Y)
+            sage: XYe = QuaternionMatrixEuclideanJordanAlgebra.real_embed(X*Y)
+            sage: Xe*Ye == XYe
+            True
+
+        """
+        quaternions = M.base_ring()
+        n = M.nrows()
+        if M.ncols() != n:
+            raise ValueError("the matrix 'M' must be square")
+
+        F = QuadraticField(-1, 'i')
+        i = F.gen()
+
+        blocks = []
+        for z in M.list():
+            t = z.coefficient_tuple()
+            a = t[0]
+            b = t[1]
+            c = t[2]
+            d = t[3]
+            cplxM = matrix(F, 2, [[ a + b*i, c + d*i],
+                                 [-c + d*i, a - b*i]])
+            realM = ComplexMatrixEuclideanJordanAlgebra.real_embed(cplxM)
+            blocks.append(realM)
+
+        # We should have real entries by now, so use the realest field
+        # we've got for the return value.
+        return matrix.block(quaternions.base_ring(), n, blocks)
+
+
 
     @staticmethod
-    def natural_inner_product(X,Y):
-        Xu = _unembed_complex_matrix(X)
-        Yu = _unembed_complex_matrix(Y)
-        # The trace need not be real; consider Xu = (i*I) and Yu = I.
-        return ((Xu*Yu).trace()).vector()[0] # real part, I guess
+    def real_unembed(M):
+        """
+        The inverse of _embed_quaternion_matrix().
+
+        SETUP::
+
+            sage: from mjo.eja.eja_algebra import \
+            ....:   QuaternionMatrixEuclideanJordanAlgebra
+
+        EXAMPLES::
+
+            sage: M = matrix(QQ, [[ 1,  2,  3,  4],
+            ....:                 [-2,  1, -4,  3],
+            ....:                 [-3,  4,  1, -2],
+            ....:                 [-4, -3,  2,  1]])
+            sage: QuaternionMatrixEuclideanJordanAlgebra.real_unembed(M)
+            [1 + 2*i + 3*j + 4*k]
+
+        TESTS:
 
+        Unembedding is the inverse of embedding::
 
+            sage: set_random_seed()
+            sage: Q = QuaternionAlgebra(QQ, -1, -1)
+            sage: M = random_matrix(Q, 3)
+            sage: Me = QuaternionMatrixEuclideanJordanAlgebra.real_embed(M)
+            sage: QuaternionMatrixEuclideanJordanAlgebra.real_unembed(Me) == M
+            True
 
-class QuaternionHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra):
+        """
+        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")
+
+        # Use the base ring of the matrix to ensure that its entries can be
+        # multiplied by elements of the quaternion algebra.
+        field = M.base_ring()
+        Q = QuaternionAlgebra(field,-1,-1)
+        i,j,k = Q.gens()
+
+        # Go top-left to bottom-right (reading order), converting every
+        # 4-by-4 block we see to a 2-by-2 complex block, to a 1-by-1
+        # quaternion block.
+        elements = []
+        for l in xrange(n/4):
+            for m in xrange(n/4):
+                submat = ComplexMatrixEuclideanJordanAlgebra.real_unembed(
+                    M[4*l:4*l+4,4*m:4*m+4] )
+                if submat[0,0] != submat[1,1].conjugate():
+                    raise ValueError('bad on-diagonal submatrix')
+                if submat[0,1] != -submat[1,0].conjugate():
+                    raise ValueError('bad off-diagonal submatrix')
+                z  = submat[0,0].vector()[0]   # real part
+                z += submat[0,0].vector()[1]*i # imag part
+                z += submat[0,1].vector()[0]*j # real part
+                z += submat[0,1].vector()[1]*k # imag part
+                elements.append(z)
+
+        return matrix(Q, n/4, elements)
+
+
+
+class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra):
     """
     The rank-n simple EJA consisting of self-adjoint n-by-n quaternion
     matrices, the usual symmetric Jordan product, and the
@@ -1594,45 +1601,57 @@ class QuaternionHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra):
         True
 
     """
-    def __init__(self, n, field=QQ, normalize_basis=True, **kwargs):
-        S = _quaternion_hermitian_basis(n, field)
+    @classmethod
+    def _denormalized_basis(cls, n, field):
+        """
+        Returns a basis for the space of quaternion Hermitian n-by-n matrices.
 
-        if n > 1 and normalize_basis:
-            # We'll need sqrt(2) to normalize the basis, and this
-            # winds up in the multiplication table, so the whole
-            # algebra needs to be over the field extension.
-            R = PolynomialRing(field, 'z')
-            z = R.gen()
-            p = z**2 - 2
-            if p.is_irreducible():
-                field = NumberField(p, 'sqrt2', embedding=RLF(2).sqrt())
-                S = [ s.change_ring(field) for s in S ]
-            self._basis_normalizers = tuple(
-                ~(self.natural_inner_product(s,s).sqrt()) for s in S )
-            S = tuple( s*c for (s,c) in zip(S,self._basis_normalizers) )
+        Why do we embed these? Basically, because all of numerical
+        linear algebra assumes that you're working with vectors consisting
+        of `n` entries from a field and scalars from the same field. There's
+        no way to tell SageMath that (for example) the vectors contain
+        complex numbers, while the scalar field is real.
 
-        Qs = _multiplication_table_from_matrix_basis(S)
+        SETUP::
 
-        fdeja = super(QuaternionHermitianEJA, self)
-        return fdeja.__init__(field,
-                              Qs,
-                              rank=n,
-                              natural_basis=S,
-                              **kwargs)
+            sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
 
-    @staticmethod
-    def _max_test_case_size():
-        return 3
+        TESTS::
 
-    @staticmethod
-    def natural_inner_product(X,Y):
-        Xu = _unembed_quaternion_matrix(X)
-        Yu = _unembed_quaternion_matrix(Y)
-        # The trace need not be real; consider Xu = (i*I) and Yu = I.
-        # The result will be a quaternion algebra element, which doesn't
-        # have a "vector" method, but does have coefficient_tuple() method
-        # that returns the coefficients of 1, i, j, and k -- in that order.
-        return ((Xu*Yu).trace()).coefficient_tuple()[0]
+            sage: set_random_seed()
+            sage: n = ZZ.random_element(1,5)
+            sage: B = QuaternionHermitianEJA._denormalized_basis(n,QQ)
+            sage: all( M.is_symmetric() for M in B )
+            True
+
+        """
+        Q = QuaternionAlgebra(QQ,-1,-1)
+        I,J,K = Q.gens()
+
+        # This is like the symmetric case, but we need to be careful:
+        #
+        #   * 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 = cls.real_embed(Eij)
+                    S.append(Sij)
+                else:
+                    # The second, third, and fourth ones have a minus
+                    # because they're conjugated.
+                    Sij_real = cls.real_embed(Eij + Eij.transpose())
+                    S.append(Sij_real)
+                    Sij_I = cls.real_embed(I*Eij - I*Eij.transpose())
+                    S.append(Sij_I)
+                    Sij_J = cls.real_embed(J*Eij - J*Eij.transpose())
+                    S.append(Sij_J)
+                    Sij_K = cls.real_embed(K*Eij - K*Eij.transpose())
+                    S.append(Sij_K)
+        return tuple(S)