+ def natural_basis_space(self):
+ """
+ Return the matrix space in which this algebra's natural basis
+ elements live.
+ """
+ if self._natural_basis is None or len(self._natural_basis) == 0:
+ return MatrixSpace(self.base_ring(), self.dimension(), 1)
+ else:
+ 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):
+ """
+ Return the unit element of this algebra.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (HadamardEJA,
+ ....: random_eja)
+
+ EXAMPLES::
+
+ sage: J = HadamardEJA(5)
+ sage: J.one()
+ e0 + e1 + e2 + e3 + e4
+
+ TESTS:
+
+ The identity element acts like the identity::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: J.one()*x == x and x*J.one() == x
+ True
+
+ The matrix of the unit element's operator is the identity::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: actual = J.one().operator().matrix()
+ sage: expected = matrix.identity(J.base_ring(), J.dimension())
+ sage: actual == expected
+ True
+
+ """
+ # We can brute-force compute the matrices of the operators
+ # that correspond to the basis elements of this algebra.
+ # If some linear combination of those basis elements is the
+ # algebra identity, then the same linear combination of
+ # their matrices has to be the identity matrix.
+ #
+ # Of course, matrices aren't vectors in sage, so we have to
+ # appeal to the "long vectors" isometry.
+ oper_vecs = [ _mat2vec(g.operator().matrix()) for g in self.gens() ]
+
+ # Now we use basis linear algebra to find the coefficients,
+ # of the matrices-as-vectors-linear-combination, which should
+ # work for the original algebra basis too.
+ A = matrix.column(self.base_ring(), oper_vecs)
+
+ # We used the isometry on the left-hand side already, but we
+ # still need to do it for the right-hand side. Recall that we
+ # wanted something that summed to the identity matrix.
+ b = _mat2vec( matrix.identity(self.base_ring(), self.dimension()) )
+
+ # Now if there's an identity element in the algebra, this should work.
+ coeffs = A.solve_right(b)
+ return self.linear_combination(zip(self.gens(), coeffs))
+
+
+ def peirce_decomposition(self, c):
+ """
+ The Peirce decomposition of this algebra relative to the
+ idempotent ``c``.
+
+ In the future, this can be extended to a complete system of
+ orthogonal idempotents.
+
+ INPUT:
+
+ - ``c`` -- an idempotent of this algebra.
+
+ OUTPUT:
+
+ A triple (J0, J5, J1) containing two subalgebras and one subspace
+ of this algebra,
+
+ - ``J0`` -- the algebra on the eigenspace of ``c.operator()``
+ corresponding to the eigenvalue zero.
+
+ - ``J5`` -- the eigenspace (NOT a subalgebra) of ``c.operator()``
+ corresponding to the eigenvalue one-half.
+
+ - ``J1`` -- the algebra on the eigenspace of ``c.operator()``
+ corresponding to the eigenvalue one.
+
+ These are the only possible eigenspaces for that operator, and this
+ algebra is a direct sum of them. The spaces ``J0`` and ``J1`` are
+ orthogonal, and are subalgebras of this algebra with the appropriate
+ restrictions.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import random_eja, RealSymmetricEJA
+
+ EXAMPLES:
+
+ The canonical example comes from the symmetric matrices, which
+ decompose into diagonal and off-diagonal parts::
+
+ sage: J = RealSymmetricEJA(3)
+ sage: C = matrix(QQ, [ [1,0,0],
+ ....: [0,1,0],
+ ....: [0,0,0] ])
+ sage: c = J(C)
+ sage: J0,J5,J1 = J.peirce_decomposition(c)
+ sage: J0
+ Euclidean Jordan algebra of dimension 1...
+ sage: J5
+ Vector space of degree 6 and dimension 2...
+ sage: J1
+ Euclidean Jordan algebra of dimension 3...
+
+ TESTS:
+
+ Every algebra decomposes trivially with respect to its identity
+ element::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: J0,J5,J1 = J.peirce_decomposition(J.one())
+ sage: J0.dimension() == 0 and J5.dimension() == 0
+ True
+ sage: J1.superalgebra() == J and J1.dimension() == J.dimension()
+ True
+
+ The identity elements in the two subalgebras are the
+ projections onto their respective subspaces of the
+ superalgebra's identity element::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: if not J.is_trivial():
+ ....: while x.is_nilpotent():
+ ....: x = J.random_element()
+ sage: c = x.subalgebra_idempotent()
+ sage: J0,J5,J1 = J.peirce_decomposition(c)
+ sage: J1(c) == J1.one()
+ True
+ sage: J0(J.one() - c) == J0.one()
+ True
+
+ """
+ if not c.is_idempotent():
+ raise ValueError("element is not idempotent: %s" % c)
+
+ # 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 = FiniteDimensionalEuclideanJordanSubalgebra(self, ())
+ J0 = trivial # eigenvalue zero
+ J5 = VectorSpace(self.base_ring(), 0) # eigenvalue one-half
+ J1 = trivial # eigenvalue one
+
+ for (eigval, eigspace) in c.operator().matrix().right_eigenspaces():
+ if eigval == ~(self.base_ring()(2)):
+ J5 = eigspace
+ else:
+ gens = tuple( self.from_vector(b) for b in eigspace.basis() )
+ subalg = FiniteDimensionalEuclideanJordanSubalgebra(self, gens)
+ if eigval == 0:
+ J0 = subalg
+ elif eigval == 1:
+ J1 = subalg
+ else:
+ raise ValueError("unexpected eigenvalue: %s" % eigval)
+
+ return (J0, J5, J1)
+
+
+ def random_elements(self, count):
+ """
+ Return ``count`` random elements as a tuple.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import JordanSpinEJA
+
+ EXAMPLES::
+
+ sage: J = JordanSpinEJA(3)
+ sage: x,y,z = J.random_elements(3)
+ sage: all( [ x in J, y in J, z in J ])
+ True
+ sage: len( J.random_elements(10) ) == 10
+ True
+
+ """
+ return tuple( self.random_element() for idx in range(count) )
+
+
+ def _rank_computation1(self):
+ r"""
+ Compute the rank of this algebra using highly suspicious voodoo.
+
+ ALGORITHM:
+
+ We first compute the basis representation of the operator L_x
+ using polynomial indeterminates are placeholders for the
+ coordinates of "x", which is arbitrary. We then use that
+ matrix to compute the (polynomial) entries of x^0, x^1, ...,
+ x^d,... for increasing values of "d", starting at zero. The
+ idea is that. If we also add "coefficient variables" a_0,
+ a_1,... to the ring, we can form the linear combination
+ a_0*x^0 + ... + a_d*x^d = 0, and ask what dimension the
+ solution space has as an affine variety. When "d" is smaller
+ than the rank, we expect that dimension to be the number of
+ coordinates of "x", since we can set *those* to whatever we
+ want, but linear independence forces the coefficients a_i to
+ be zero. Eventually, when "d" passes the rank, the dimension
+ of the solution space begins to grow, because we can *still*
+ set the coordinates of "x" arbitrarily, but now there are some
+ coefficients that make the sum zero as well. So, when the
+ dimension of the variety jumps, we return the corresponding
+ "d" as the rank of the algebra. This appears to work.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (HadamardEJA,
+ ....: JordanSpinEJA,
+ ....: RealSymmetricEJA,
+ ....: ComplexHermitianEJA,
+ ....: QuaternionHermitianEJA)
+
+ EXAMPLES::
+
+ sage: J = HadamardEJA(5)
+ sage: J._rank_computation() == J.rank()
+ True
+ sage: J = JordanSpinEJA(5)
+ sage: J._rank_computation() == J.rank()
+ True
+ sage: J = RealSymmetricEJA(4)
+ sage: J._rank_computation() == J.rank()
+ True
+ sage: J = ComplexHermitianEJA(3)
+ sage: J._rank_computation() == J.rank()
+ True
+ sage: J = QuaternionHermitianEJA(2)
+ sage: J._rank_computation() == J.rank()
+ True
+
+ """
+ n = self.dimension()
+ var_names = [ "X" + str(z) for z in range(1,n+1) ]
+ d = 0
+ ideal_dim = len(var_names)
+ def L_x_i_j(i,j):
+ # From a result in my book, these are the entries of the
+ # basis representation of L_x.
+ return sum( vars[d+k]*self.monomial(k).operator().matrix()[i,j]
+ for k in range(n) )
+
+ while ideal_dim == len(var_names):
+ coeff_names = [ "a" + str(z) for z in range(d) ]
+ R = PolynomialRing(self.base_ring(), coeff_names + var_names)
+ vars = R.gens()
+ L_x = matrix(R, n, n, L_x_i_j)
+ x_powers = [ vars[k]*(L_x**k)*self.one().to_vector()
+ for k in range(d) ]
+ eqs = [ sum(x_powers[k][j] for k in range(d)) for j in range(n) ]
+ ideal_dim = R.ideal(eqs).dimension()
+ d += 1
+
+ # Subtract one because we increment one too many times, and
+ # subtract another one because "d" is one greater than the
+ # answer anyway; when d=3, we go up to x^2.
+ return d-2
+
+ def _rank_computation2(self):
+ r"""
+ Instead of using the dimension of an ideal, find the rank of a
+ matrix containing polynomials.
+ """
+ n = self.dimension()
+ var_names = [ "X" + str(z) for z in range(1,n+1) ]
+ R = PolynomialRing(self.base_ring(), var_names)
+ vars = R.gens()
+
+ def L_x_i_j(i,j):
+ # From a result in my book, these are the entries of the
+ # basis representation of L_x.
+ return sum( vars[k]*self.monomial(k).operator().matrix()[i,j]
+ for k in range(n) )
+
+ L_x = matrix(R, n, n, L_x_i_j)
+ x_powers = [ (vars[k]*(L_x**k)*self.one().to_vector()).row()
+ for k in range(n) ]
+
+ from sage.matrix.constructor import block_matrix
+ M = block_matrix(n,1,x_powers)
+ return M.rank()
+
+ def _rank_computation3(self):
+ r"""
+ Similar to :meth:`_rank_computation2`, but it stops echelonizing
+ as soon as it hits the first zero row.
+ """
+ n = self.dimension()
+ if n == 0:
+ return 0
+ elif n == 1:
+ return 1
+
+ var_names = [ "X" + str(z) for z in range(1,n+1) ]
+ R = PolynomialRing(self.base_ring(), var_names)
+ vars = R.gens()
+
+ def L_x_i_j(i,j):
+ # From a result in my book, these are the entries of the
+ # basis representation of L_x.
+ return sum( vars[k]*self.monomial(k).operator().matrix()[i,j]
+ for k in range(n) )
+
+ L_x = matrix(R, n, n, L_x_i_j)
+ x_powers = [ vars[k]*(L_x**k)*self.one().to_vector()
+ for k in range(n) ]
+
+ # Can assume n >= 2
+ rows = [x_powers[0]]
+ M = matrix(rows)
+ old_rank = 1
+
+ for d in range(1,n):
+ rows = M.rows() + [x_powers[d]]
+ M = matrix(rows)
+ M.echelonize()
+ new_rank = M.rank()
+ if new_rank == old_rank:
+ return new_rank
+ else:
+ old_rank = new_rank
+