- n = ZZ(M.nrows())
- if M.ncols() != n:
- raise ValueError("the matrix 'M' must be square")
- if not n.mod(4).is_zero():
- raise ValueError("the matrix 'M' must be a complex embedding")
-
- Q = QuaternionAlgebra(QQ,-1,-1)
- i,j,k = Q.gens()
-
- # Go top-left to bottom-right (reading order), converting every
- # 4-by-4 block we see to a 2-by-2 complex block, to a 1-by-1
- # quaternion block.
- elements = []
- for l in xrange(n/4):
- for m in xrange(n/4):
- submat = _unembed_complex_matrix(M[4*l:4*l+4,4*m:4*m+4])
- if submat[0,0] != submat[1,1].conjugate():
- raise ValueError('bad on-diagonal submatrix')
- if submat[0,1] != -submat[1,0].conjugate():
- raise ValueError('bad off-diagonal submatrix')
- z = submat[0,0].real() + submat[0,0].imag()*i
- z += submat[0,1].real()*j + submat[0,1].imag()*k
- elements.append(z)
-
- return matrix(Q, n/4, elements)
-
-
-# The usual inner product on R^n.
-def _usual_ip(x,y):
- return x.vector().inner_product(y.vector())
-
-# The inner product used for the real symmetric simple EJA.
-# We keep it as a separate function because e.g. the complex
-# algebra uses the same inner product, except divided by 2.
-def _matrix_ip(X,Y):
- X_mat = X.natural_representation()
- Y_mat = Y.natural_representation()
- return (X_mat*Y_mat).trace()
-
-
-class RealSymmetricEJA(FiniteDimensionalEuclideanJordanAlgebra):
+ @cached_method
+ def _charpoly_coefficients(self):
+ r"""
+ Override the parent method with something that tries to compute
+ over a faster (non-extension) field.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import JordanSpinEJA
+
+ EXAMPLES:
+
+ The base ring of the resulting polynomial coefficients is what
+ it should be, and not the rationals (unless the algebra was
+ already over the rationals)::
+
+ sage: J = JordanSpinEJA(3)
+ sage: J._charpoly_coefficients()
+ (X1^2 - X2^2 - X3^2, -2*X1)
+ sage: a0 = J._charpoly_coefficients()[0]
+ sage: J.base_ring()
+ Algebraic Real Field
+ sage: a0.base_ring()
+ Algebraic Real Field
+
+ """
+ if self.base_ring() is QQ:
+ # There's no need to construct *another* algebra over the
+ # rationals if this one is already over the rationals.
+ superclass = super(RationalBasisEuclideanJordanAlgebra, self)
+ return superclass._charpoly_coefficients()
+
+ mult_table = tuple(
+ map(lambda x: x.to_vector(), ls)
+ for ls in self._multiplication_table
+ )
+
+ # Do the computation over the rationals. The answer will be
+ # the same, because our basis coordinates are (essentially)
+ # rational.
+ J = FiniteDimensionalEuclideanJordanAlgebra(QQ,
+ mult_table,
+ check_field=False,
+ check_axioms=False)
+ a = J._charpoly_coefficients()
+ return tuple(map(lambda x: x.change_ring(self.base_ring()), a))
+
+
+class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
+ @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
+
+ def __init__(self, field, basis, normalize_basis=True, **kwargs):
+ """
+ Compared to the superclass constructor, we take a basis instead of
+ a multiplication table because the latter can be computed in terms
+ of the former when the product is known (like it is here).
+ """
+ # Used in this class's fast _charpoly_coefficients() override.
+ self._basis_normalizers = None
+
+ # We're going to loop through this a few times, so now's a good
+ # time to ensure that it isn't a generator expression.
+ basis = tuple(basis)
+
+ if len(basis) > 1 and normalize_basis:
+ # We'll need sqrt(2) to normalize the basis, and this
+ # winds up in the multiplication table, so the whole
+ # algebra needs to be over the field extension.
+ R = PolynomialRing(field, 'z')
+ z = R.gen()
+ p = z**2 - 2
+ if p.is_irreducible():
+ field = field.extension(p, 'sqrt2', embedding=RLF(2).sqrt())
+ basis = tuple( s.change_ring(field) for s in basis )
+ self._basis_normalizers = tuple(
+ ~(self.natural_inner_product(s,s).sqrt()) for s in basis )
+ basis = tuple(s*c for (s,c) in zip(basis,self._basis_normalizers))
+
+ Qs = self.multiplication_table_from_matrix_basis(basis)
+
+ super(MatrixEuclideanJordanAlgebra, self).__init__(field,
+ Qs,
+ natural_basis=basis,
+ **kwargs)
+
+
+ @cached_method
+ def _charpoly_coefficients(self):
+ r"""
+ Override the parent method with something that tries to compute
+ over a faster (non-extension) field.
+ """
+ if self._basis_normalizers is None or self.base_ring() is QQ:
+ # We didn't normalize, or the basis we started with had
+ # entries in a nice field already. Just compute the thing.
+ return super(MatrixEuclideanJordanAlgebra, self)._charpoly_coefficients()
+
+ basis = ( (b/n) for (b,n) in zip(self.natural_basis(),
+ self._basis_normalizers) )
+
+ # Do this over the rationals and convert back at the end.
+ # Only works because we know the entries of the basis are
+ # integers. The argument ``check_axioms=False`` is required
+ # because the trace inner-product method for this
+ # class is a stub and can't actually be checked.
+ J = MatrixEuclideanJordanAlgebra(QQ,
+ basis,
+ normalize_basis=False,
+ check_field=False,
+ check_axioms=False)
+ a = J._charpoly_coefficients()
+
+ # Unfortunately, changing the basis does change the
+ # coefficients of the characteristic polynomial, but since
+ # these are really the coefficients of the "characteristic
+ # polynomial of" function, everything is still nice and
+ # unevaluated. It's therefore "obvious" how scaling the
+ # basis affects the coordinate variables X1, X2, et
+ # cetera. Scaling the first basis vector up by "n" adds a
+ # factor of 1/n into every "X1" term, for example. So here
+ # we simply undo the basis_normalizer scaling that we
+ # performed earlier.
+ #
+ # The a[0] access here is safe because trivial algebras
+ # won't have any basis normalizers and therefore won't
+ # make it to this "else" branch.
+ XS = a[0].parent().gens()
+ subs_dict = { XS[i]: self._basis_normalizers[i]*XS[i]
+ for i in range(len(XS)) }
+ 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):
+ """
+ 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)
+
+ """
+ raise NotImplementedError
+
+
+ @staticmethod
+ def real_unembed(M):
+ """
+ The inverse of :meth:`real_embed`.
+ """
+ raise NotImplementedError
+
+
+ @classmethod
+ def natural_inner_product(cls,X,Y):
+ Xu = cls.real_unembed(X)
+ Yu = cls.real_unembed(Y)
+ tr = (Xu*Yu).trace()
+
+ try:
+ # Works in QQ, AA, RDF, et cetera.
+ return tr.real()
+ except AttributeError:
+ # A quaternion doesn't have a real() 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 RealMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
+ @staticmethod
+ def real_embed(M):
+ """
+ The identity function, for embedding real matrices into real
+ matrices.
+ """
+ return M
+
+ @staticmethod
+ def real_unembed(M):
+ """
+ The identity function, for unembedding real matrices from real
+ matrices.
+ """
+ return M
+
+
+class RealSymmetricEJA(RealMatrixEuclideanJordanAlgebra):