+ 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...
+ sage: J0.one().to_matrix()
+ [0 0 0]
+ [0 0 0]
+ [0 0 1]
+ sage: orig_df = AA.options.display_format
+ sage: AA.options.display_format = 'radical'
+ sage: J.from_vector(J5.basis()[0]).to_matrix()
+ [ 0 0 1/2*sqrt(2)]
+ [ 0 0 0]
+ [1/2*sqrt(2) 0 0]
+ sage: J.from_vector(J5.basis()[1]).to_matrix()
+ [ 0 0 0]
+ [ 0 0 1/2*sqrt(2)]
+ [ 0 1/2*sqrt(2) 0]
+ sage: AA.options.display_format = orig_df
+ sage: J1.one().to_matrix()
+ [1 0 0]
+ [0 1 0]
+ [0 0 0]
+
+ 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 decomposition is into eigenspaces, and its components are
+ therefore necessarily orthogonal. Moreover, 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: ipsum = 0
+ sage: for (w,y,z) in zip(J0.basis(), J5.basis(), J1.basis()):
+ ....: w = w.superalgebra_element()
+ ....: y = J.from_vector(y)
+ ....: z = z.superalgebra_element()
+ ....: ipsum += w.inner_product(y).abs()
+ ....: ipsum += w.inner_product(z).abs()
+ ....: ipsum += y.inner_product(z).abs()
+ sage: ipsum
+ 0
+ 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 = self.subalgebra(())
+ 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 = self.subalgebra(gens, check_axioms=False)
+ if eigval == 0:
+ J0 = subalg
+ elif eigval == 1:
+ J1 = subalg
+ else:
+ raise ValueError("unexpected eigenvalue: %s" % eigval)
+
+ return (J0, J5, J1)
+
+
+ def random_element(self, thorough=False):
+ r"""
+ Return a random element of this algebra.
+
+ Our algebra superclass method only returns a linear
+ combination of at most two basis elements. We instead
+ want the vector space "random element" method that
+ returns a more diverse selection.
+
+ INPUT:
+
+ - ``thorough`` -- (boolean; default False) whether or not we
+ should generate irrational coefficients for the random
+ element when our base ring is irrational; this slows the
+ algebra operations to a crawl, but any truly random method
+ should include them
+
+ """
+ # For a general base ring... maybe we can trust this to do the
+ # right thing? Unlikely, but.
+ V = self.vector_space()
+ v = V.random_element()
+
+ if self.base_ring() is AA:
+ # The "random element" method of the algebraic reals is
+ # stupid at the moment, and only returns integers between
+ # -2 and 2, inclusive:
+ #
+ # https://trac.sagemath.org/ticket/30875
+ #
+ # Instead, we implement our own "random vector" method,
+ # and then coerce that into the algebra. We use the vector
+ # space degree here instead of the dimension because a
+ # subalgebra could (for example) be spanned by only two
+ # vectors, each with five coordinates. We need to
+ # generate all five coordinates.
+ if thorough:
+ v *= QQbar.random_element().real()
+ else:
+ v *= QQ.random_element()
+
+ return self.from_vector(V.coordinate_vector(v))
+
+ def random_elements(self, count, thorough=False):
+ """
+ Return ``count`` random elements as a tuple.
+
+ INPUT:
+
+ - ``thorough`` -- (boolean; default False) whether or not we
+ should generate irrational coefficients for the random
+ elements when our base ring is irrational; this slows the
+ algebra operations to a crawl, but any truly random method
+ should include them
+
+ 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(thorough)
+ for idx in range(count) )
+
+
+ @cached_method
+ def _charpoly_coefficients(self):
+ r"""
+ The `r` polynomial coefficients of the "characteristic polynomial
+ of" function.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import random_eja
+
+ TESTS:
+
+ The theory shows that these are all homogeneous polynomials of
+ a known degree::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: all(p.is_homogeneous() for p in J._charpoly_coefficients())
+ True
+
+ """
+ n = self.dimension()
+ R = self.coordinate_polynomial_ring()
+ vars = R.gens()
+ F = R.fraction_field()
+
+ 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.gens()[k].operator().matrix()[i,j]
+ for k in range(n) )
+
+ L_x = matrix(F, n, n, L_x_i_j)
+
+ r = None
+ if self.rank.is_in_cache():
+ r = self.rank()
+ # There's no need to pad the system with redundant
+ # columns if we *know* they'll be redundant.
+ n = r
+
+ # Compute an extra power in case the rank is equal to
+ # the dimension (otherwise, we would stop at x^(r-1)).
+ x_powers = [ (L_x**k)*self.one().to_vector()
+ for k in range(n+1) ]
+ A = matrix.column(F, x_powers[:n])
+ AE = A.extended_echelon_form()
+ E = AE[:,n:]
+ A_rref = AE[:,:n]
+ if r is None:
+ r = A_rref.rank()
+ b = x_powers[r]
+
+ # The theory says that only the first "r" coefficients are
+ # nonzero, and they actually live in the original polynomial
+ # ring and not the fraction field. We negate them because in
+ # the actual characteristic polynomial, they get moved to the
+ # other side where x^r lives. We don't bother to trim A_rref
+ # down to a square matrix and solve the resulting system,
+ # because the upper-left r-by-r portion of A_rref is
+ # guaranteed to be the identity matrix, so e.g.
+ #
+ # A_rref.solve_right(Y)
+ #
+ # would just be returning Y.
+ return (-E*b)[:r].change_ring(R)
+
+ @cached_method
+ def rank(self):
+ r"""
+ Return the rank of this EJA.
+
+ This is a cached method because we know the rank a priori for
+ all of the algebras we can construct. Thus we can avoid the
+ expensive ``_charpoly_coefficients()`` call unless we truly
+ need to compute the whole characteristic polynomial.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (HadamardEJA,
+ ....: JordanSpinEJA,