what can be supported in a general Jordan Algebra.
"""
-from sage.categories.magmatic_algebras import MagmaticAlgebras
+from sage.categories.finite_dimensional_algebras_with_basis import FiniteDimensionalAlgebrasWithBasis
+from sage.categories.morphism import SetMorphism
from sage.structure.element import is_Matrix
from sage.structure.category_object import normalize_names
from sage.algebras.finite_dimensional_algebras.finite_dimensional_algebra import FiniteDimensionalAlgebra
from sage.algebras.finite_dimensional_algebras.finite_dimensional_algebra_element import FiniteDimensionalAlgebraElement
+from sage.algebras.finite_dimensional_algebras.finite_dimensional_algebra_morphism import FiniteDimensionalAlgebraMorphism, FiniteDimensionalAlgebraHomset
+
+
+class FiniteDimensionalEuclideanJordanAlgebraHomset(FiniteDimensionalAlgebraHomset):
+
+ def has_coerce_map_from(self, S):
+ """
+ EXAMPLES::
+
+ sage: J = RealSymmetricEJA(2)
+ sage: H = J.Hom(J)
+ sage: H.has_coerce_map_from(QQ)
+ True
+
+ """
+ try:
+ # The Homset classes override has_coerce_map_from() with
+ # something that crashes when it's given e.g. QQ.
+ if S.is_subring(self.codomain().base_ring()):
+ return True
+ except:
+ pclass = super(FiniteDimensionalEuclideanJordanAlgebraHomset, self)
+ return pclass.has_coerce_map_from(S)
+
+
+ def _coerce_map_from_(self, S):
+ """
+ EXAMPLES::
+
+ sage: J = RealSymmetricEJA(2)
+ sage: H = J.Hom(J)
+ sage: H.coerce(2)
+ Morphism from Euclidean Jordan algebra of degree 3 over Rational
+ Field to Euclidean Jordan algebra of degree 3 over Rational Field
+ given by matrix
+ [2 0 0]
+ [0 2 0]
+ [0 0 2]
+
+ """
+ C = self.codomain()
+ R = C.base_ring()
+ if S.is_subring(R):
+ h = S.hom(self.codomain())
+ return SetMorphism(Hom(S,C), lambda x: h(x).operator())
+
+
+ def __call__(self, x):
+ """
+ EXAMPLES::
+
+ sage: J = RealSymmetricEJA(2)
+ sage: H = J.Hom(J)
+ sage: H(2)
+ Morphism from Euclidean Jordan algebra of degree 3 over Rational
+ Field to Euclidean Jordan algebra of degree 3 over Rational Field
+ given by matrix
+ [2 0 0]
+ [0 2 0]
+ [0 0 2]
+
+ """
+ if x in self.base_ring():
+ cols = self.domain().dimension()
+ rows = self.codomain().dimension()
+ x = x*identity_matrix(self.codomain().base_ring(), rows, cols)
+ return FiniteDimensionalEuclideanJordanAlgebraMorphism(self, x)
+
+
+ def one(self):
+ """
+ Return the identity morphism, but as a member of the right
+ space (so that we can add it, multiply it, etc.)
+ """
+ cols = self.domain().dimension()
+ rows = self.codomain().dimension()
+ mat = identity_matrix(self.base_ring(), rows, cols)
+ return FiniteDimensionalEuclideanJordanAlgebraMorphism(self, mat)
+
+
+
+class FiniteDimensionalEuclideanJordanAlgebraMorphism(FiniteDimensionalAlgebraMorphism):
+ """
+ A linear map between two finite-dimensional EJAs.
+
+ This is a very thin wrapper around FiniteDimensionalAlgebraMorphism
+ that does only a few things:
+
+ 1. Avoids the ``unitary`` and ``check`` arguments to the constructor
+ that will always be ``False``. This is necessary because these
+ are homomorphisms with respect to ADDITION, but the SageMath
+ machinery wants to check that they're homomorphisms with respect
+ to (Jordan) MULTIPLICATION. That obviously doesn't work.
+
+ 2. Inputs and outputs the underlying matrix with respect to COLUMN
+ vectors, unlike the parent class.
+
+ 3. Allows us to add, subtract, negate, multiply (compose), and
+ invert morphisms in the obvious way.
+
+ If this seems a bit heavyweight, it is. I would have been happy to
+ use a the ring morphism that underlies the finite-dimensional
+ algebra morphism, but they don't seem to be callable on elements of
+ our EJA, and you can't add/multiply/etc. them.
+ """
+ def _add_(self, other):
+ """
+ Add two EJA morphisms in the obvious way.
+
+ EXAMPLES::
+
+ sage: J = RealSymmetricEJA(3)
+ sage: x = J.zero()
+ sage: y = J.one()
+ sage: x.operator() + y.operator()
+ Morphism from Euclidean Jordan algebra of degree 6 over Rational
+ Field to Euclidean Jordan algebra of degree 6 over Rational Field
+ given by matrix
+ [1 0 0 0 0 0]
+ [0 1 0 0 0 0]
+ [0 0 1 0 0 0]
+ [0 0 0 1 0 0]
+ [0 0 0 0 1 0]
+ [0 0 0 0 0 1]
+
+ TESTS::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: y = J.random_element()
+ sage: (x.operator() + y.operator()) in J.Hom(J)
+ True
+
+ """
+ P = self.parent()
+ if not other in P:
+ raise ValueError("summands must live in the same space")
+
+ return FiniteDimensionalEuclideanJordanAlgebraMorphism(
+ P,
+ self.matrix() + other.matrix() )
+
+
+ def __init__(self, parent, f):
+ FiniteDimensionalAlgebraMorphism.__init__(self,
+ parent,
+ f.transpose(),
+ unitary=False,
+ check=False)
+
+
+ def __invert__(self):
+ """
+ EXAMPLES::
+
+ sage: J = RealSymmetricEJA(2)
+ sage: x = J.linear_combination(zip(range(len(J.gens())), J.gens()))
+ sage: x.is_invertible()
+ True
+ sage: ~x.operator()
+ Morphism from Euclidean Jordan algebra of degree 3 over Rational
+ Field to Euclidean Jordan algebra of degree 3 over Rational Field
+ given by matrix
+ [-3/2 2 -1/2]
+ [ 1 0 0]
+ [-1/2 0 1/2]
+ sage: x.operator_matrix().inverse()
+ [-3/2 2 -1/2]
+ [ 1 0 0]
+ [-1/2 0 1/2]
+
+ TESTS::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: not x.is_invertible() or (
+ ....: (~x.operator()).matrix() == x.operator_matrix().inverse() )
+ True
+
+ """
+ A = self.matrix()
+ if not A.is_invertible():
+ raise ValueError("morphism is not invertible")
+
+ P = self.parent()
+ return FiniteDimensionalEuclideanJordanAlgebraMorphism(self.parent(),
+ A.inverse())
+
+ def _lmul_(self, right):
+ """
+ Compose two EJA morphisms using multiplicative notation.
+
+ EXAMPLES::
+
+ sage: J = RealSymmetricEJA(2)
+ sage: x = J.zero()
+ sage: y = J.one()
+ sage: x.operator() * y.operator()
+ Morphism from Euclidean Jordan algebra of degree 3 over Rational
+ Field to Euclidean Jordan algebra of degree 3 over Rational Field
+ given by matrix
+ [0 0 0]
+ [0 0 0]
+ [0 0 0]
+
+ ::
+
+ sage: J = RealSymmetricEJA(2)
+ sage: x = J.linear_combination(zip(range(len(J.gens())), J.gens()))
+ sage: x.operator()
+ Morphism from Euclidean Jordan algebra of degree 3 over Rational
+ Field to Euclidean Jordan algebra of degree 3 over Rational Field
+ given by matrix
+ [ 0 1 0]
+ [1/2 1 1/2]
+ [ 0 1 2]
+ sage: 2*x.operator()
+ Morphism from Euclidean Jordan algebra of degree 3 over Rational
+ Field to Euclidean Jordan algebra of degree 3 over Rational Field
+ given by matrix
+ [0 2 0]
+ [1 2 1]
+ [0 2 4]
+ sage: x.operator()*2
+ Morphism from Euclidean Jordan algebra of degree 3 over Rational
+ Field to Euclidean Jordan algebra of degree 3 over Rational Field
+ given by matrix
+ [0 2 0]
+ [1 2 1]
+ [0 2 4]
+
+ TESTS::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: y = J.random_element()
+ sage: (x.operator() * y.operator()) in J.Hom(J)
+ True
+
+ """
+ try:
+ # I think the morphism classes break the coercion framework
+ # somewhere along the way, so we have to do this ourselves.
+ right = self.parent().coerce(right)
+ except:
+ pass
+
+ if not right.codomain() is self.domain():
+ raise ValueError("(co)domains must agree for composition")
+
+ return FiniteDimensionalEuclideanJordanAlgebraMorphism(
+ self.parent(),
+ self.matrix()*right.matrix() )
+
+ __mul__ = _lmul_
+
+
+ def __pow__(self, n):
+ """
+
+ TESTS::
+
+ sage: J = JordanSpinEJA(4)
+ sage: e0,e1,e2,e3 = J.gens()
+ sage: x = -5/2*e0 + 1/2*e2 + 20*e3
+ sage: Qx = x.quadratic_representation()
+ sage: Qx^0
+ Morphism from Euclidean Jordan algebra of degree 4 over Rational
+ Field to Euclidean Jordan algebra of degree 4 over Rational Field
+ given by matrix
+ [1 0 0 0]
+ [0 1 0 0]
+ [0 0 1 0]
+ [0 0 0 1]
+ sage: (x^0).quadratic_representation() == Qx^0
+ True
+
+ """
+ if n == 0:
+ # We get back the stupid identity morphism which doesn't
+ # live in the right space.
+ return self.parent().one()
+ elif n == 1:
+ return self
+ else:
+ return FiniteDimensionalAlgebraMorphism.__pow__(self,n)
+
+
+ def _neg_(self):
+ """
+ Negate this morphism.
+
+ EXAMPLES::
+
+ sage: J = RealSymmetricEJA(2)
+ sage: x = J.one()
+ sage: -x.operator()
+ Morphism from Euclidean Jordan algebra of degree 3 over Rational
+ Field to Euclidean Jordan algebra of degree 3 over Rational Field
+ given by matrix
+ [-1 0 0]
+ [ 0 -1 0]
+ [ 0 0 -1]
+
+ TESTS::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: -x.operator() in J.Hom(J)
+ True
+
+ """
+ return FiniteDimensionalEuclideanJordanAlgebraMorphism(
+ self.parent(),
+ -self.matrix() )
+
+
+ def _repr_(self):
+ """
+ We override only the representation that is shown to the user,
+ because we want the matrix to be with respect to COLUMN vectors.
+
+ TESTS:
+
+ Ensure that we see the transpose of the underlying matrix object:
+
+ sage: J = RealSymmetricEJA(3)
+ sage: x = J.linear_combination(zip(range(len(J.gens())), J.gens()))
+ sage: L = x.operator()
+ sage: L
+ Morphism from Euclidean Jordan algebra of degree 6 over Rational
+ Field to Euclidean Jordan algebra of degree 6 over Rational Field
+ given by matrix
+ [ 0 1 2 0 0 0]
+ [1/2 3/2 2 1/2 1 0]
+ [ 1 2 5/2 0 1/2 1]
+ [ 0 1 0 3 4 0]
+ [ 0 1 1/2 2 4 2]
+ [ 0 0 2 0 4 5]
+ sage: L._matrix
+ [ 0 1/2 1 0 0 0]
+ [ 1 3/2 2 1 1 0]
+ [ 2 2 5/2 0 1/2 2]
+ [ 0 1/2 0 3 2 0]
+ [ 0 1 1/2 4 4 4]
+ [ 0 0 1 0 2 5]
+
+ """
+ return "Morphism from {} to {} given by matrix\n{}".format(
+ self.domain(), self.codomain(), self.matrix())
+
+
+ def __sub__(self, other):
+ """
+ Subtract one morphism from another using addition and negation.
+
+ EXAMPLES::
+
+ sage: J = RealSymmetricEJA(2)
+ sage: L1 = J.one().operator()
+ sage: L1 - L1
+ Morphism from Euclidean Jordan algebra of degree 3 over Rational
+ Field to Euclidean Jordan algebra of degree 3 over Rational
+ Field given by matrix
+ [0 0 0]
+ [0 0 0]
+ [0 0 0]
+
+ TESTS::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: y = J.random_element()
+ sage: x.operator() - y.operator() in J.Hom(J)
+ True
+
+ """
+ return self + (-other)
+
+
+ def matrix(self):
+ """
+ Return the matrix of this morphism with respect to a left-action
+ on column vectors.
+ """
+ return FiniteDimensionalAlgebraMorphism.matrix(self).transpose()
+
class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
@staticmethod
names='e',
assume_associative=False,
category=None,
- rank=None):
+ rank=None,
+ natural_basis=None):
n = len(mult_table)
mult_table = [b.base_extend(field) for b in mult_table]
for b in mult_table:
raise ValueError("input is not a multiplication table")
mult_table = tuple(mult_table)
- cat = MagmaticAlgebras(field).FiniteDimensional().WithBasis()
+ cat = FiniteDimensionalAlgebrasWithBasis(field)
cat.or_subcategory(category)
if assume_associative:
cat = cat.Associative()
assume_associative=assume_associative,
names=names,
category=cat,
- rank=rank)
+ rank=rank,
+ natural_basis=natural_basis)
+
+
+ def _Hom_(self, B, cat):
+ """
+ Construct a homset of ``self`` and ``B``.
+ """
+ return FiniteDimensionalEuclideanJordanAlgebraHomset(self,
+ B,
+ category=cat)
- def __init__(self, field,
+ def __init__(self,
+ field,
mult_table,
names='e',
assume_associative=False,
category=None,
- rank=None):
+ rank=None,
+ natural_basis=None):
+ """
+ EXAMPLES:
+
+ By definition, Jordan multiplication commutes::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: y = J.random_element()
+ sage: x*y == y*x
+ True
+
+ """
self._rank = rank
+ self._natural_basis = natural_basis
+ self._multiplication_table = mult_table
fda = super(FiniteDimensionalEuclideanJordanAlgebra, self)
fda.__init__(field,
mult_table,
fmt = "Euclidean Jordan algebra of degree {} over {}"
return fmt.format(self.degree(), self.base_ring())
+
+ def _a_regular_element(self):
+ """
+ Guess a regular element. Needed to compute the basis for our
+ characteristic polynomial coefficients.
+ """
+ gs = self.gens()
+ z = self.sum( (i+1)*gs[i] for i in range(len(gs)) )
+ if not z.is_regular():
+ raise ValueError("don't know a regular element")
+ return z
+
+
+ @cached_method
+ def _charpoly_basis_space(self):
+ """
+ Return the vector space spanned by the basis used in our
+ characteristic polynomial coefficients. This is used not only to
+ compute those coefficients, but also any time we need to
+ evaluate the coefficients (like when we compute the trace or
+ determinant).
+ """
+ z = self._a_regular_element()
+ V = z.vector().parent().ambient_vector_space()
+ V1 = V.span_of_basis( (z**k).vector() for k in range(self.rank()) )
+ b = (V1.basis() + V1.complement().basis())
+ return V.span_of_basis(b)
+
+
+ @cached_method
+ def _charpoly_coeff(self, i):
+ """
+ Return the coefficient polynomial "a_{i}" of this algebra's
+ general characteristic polynomial.
+
+ Having this be a separate cached method lets us compute and
+ store the trace/determinant (a_{r-1} and a_{0} respectively)
+ separate from the entire characteristic polynomial.
+ """
+ (A_of_x, x, xr, detA) = self._charpoly_matrix_system()
+ R = A_of_x.base_ring()
+ if i >= self.rank():
+ # Guaranteed by theory
+ return R.zero()
+
+ # Danger: the in-place modification is done for performance
+ # reasons (reconstructing a matrix with huge polynomial
+ # entries is slow), but I don't know how cached_method works,
+ # so it's highly possible that we're modifying some global
+ # list variable by reference, here. In other words, you
+ # probably shouldn't call this method twice on the same
+ # algebra, at the same time, in two threads
+ Ai_orig = A_of_x.column(i)
+ A_of_x.set_column(i,xr)
+ numerator = A_of_x.det()
+ A_of_x.set_column(i,Ai_orig)
+
+ # We're relying on the theory here to ensure that each a_i is
+ # indeed back in R, and the added negative signs are to make
+ # the whole charpoly expression sum to zero.
+ return R(-numerator/detA)
+
+
+ @cached_method
+ def _charpoly_matrix_system(self):
+ """
+ Compute the matrix whose entries A_ij are polynomials in
+ X1,...,XN, the vector ``x`` of variables X1,...,XN, the vector
+ corresponding to `x^r` and the determinent of the matrix A =
+ [A_ij]. In other words, all of the fixed (cachable) data needed
+ to compute the coefficients of the characteristic polynomial.
+ """
+ r = self.rank()
+ n = self.dimension()
+
+ # Construct a new algebra over a multivariate polynomial ring...
+ names = ['X' + str(i) for i in range(1,n+1)]
+ R = PolynomialRing(self.base_ring(), names)
+ J = FiniteDimensionalEuclideanJordanAlgebra(R,
+ self._multiplication_table,
+ rank=r)
+
+ idmat = identity_matrix(J.base_ring(), n)
+
+ W = self._charpoly_basis_space()
+ W = W.change_ring(R.fraction_field())
+
+ # Starting with the standard coordinates x = (X1,X2,...,Xn)
+ # and then converting the entries to W-coordinates allows us
+ # to pass in the standard coordinates to the charpoly and get
+ # back the right answer. Specifically, with x = (X1,X2,...,Xn),
+ # we have
+ #
+ # W.coordinates(x^2) eval'd at (standard z-coords)
+ # =
+ # W-coords of (z^2)
+ # =
+ # W-coords of (standard coords of x^2 eval'd at std-coords of z)
+ #
+ # We want the middle equivalent thing in our matrix, but use
+ # the first equivalent thing instead so that we can pass in
+ # standard coordinates.
+ x = J(vector(R, R.gens()))
+ l1 = [column_matrix(W.coordinates((x**k).vector())) for k in range(r)]
+ l2 = [idmat.column(k-1).column() for k in range(r+1, n+1)]
+ A_of_x = block_matrix(R, 1, n, (l1 + l2))
+ xr = W.coordinates((x**r).vector())
+ return (A_of_x, x, xr, A_of_x.det())
+
+
+ @cached_method
+ def characteristic_polynomial(self):
+ """
+
+ .. WARNING::
+
+ This implementation doesn't guarantee that the polynomial
+ denominator in the coefficients is not identically zero, so
+ theoretically it could crash. The way that this is handled
+ in e.g. Faraut and Koranyi is to use a basis that guarantees
+ the denominator is non-zero. But, doing so requires knowledge
+ of at least one regular element, and we don't even know how
+ to do that. The trade-off is that, if we use the standard basis,
+ the resulting polynomial will accept the "usual" coordinates. In
+ other words, we don't have to do a change of basis before e.g.
+ computing the trace or determinant.
+
+ EXAMPLES:
+
+ The characteristic polynomial in the spin algebra is given in
+ Alizadeh, Example 11.11::
+
+ sage: J = JordanSpinEJA(3)
+ sage: p = J.characteristic_polynomial(); p
+ X1^2 - X2^2 - X3^2 + (-2*t)*X1 + t^2
+ sage: xvec = J.one().vector()
+ sage: p(*xvec)
+ t^2 - 2*t + 1
+
+ """
+ r = self.rank()
+ n = self.dimension()
+
+ # The list of coefficient polynomials a_1, a_2, ..., a_n.
+ a = [ self._charpoly_coeff(i) for i in range(n) ]
+
+ # We go to a bit of trouble here to reorder the
+ # indeterminates, so that it's easier to evaluate the
+ # characteristic polynomial at x's coordinates and get back
+ # something in terms of t, which is what we want.
+ R = a[0].parent()
+ S = PolynomialRing(self.base_ring(),'t')
+ t = S.gen(0)
+ S = PolynomialRing(S, R.variable_names())
+ t = S(t)
+
+ # Note: all entries past the rth should be zero. The
+ # coefficient of the highest power (x^r) is 1, but it doesn't
+ # appear in the solution vector which contains coefficients
+ # for the other powers (to make them sum to x^r).
+ if (r < n):
+ a[r] = 1 # corresponds to x^r
+ else:
+ # When the rank is equal to the dimension, trying to
+ # assign a[r] goes out-of-bounds.
+ a.append(1) # corresponds to x^r
+
+ return sum( a[k]*(t**k) for k in range(len(a)) )
+
+
+ def inner_product(self, x, y):
+ """
+ The inner product associated with this Euclidean Jordan algebra.
+
+ Defaults to the trace inner product, but can be overridden by
+ subclasses if they are sure that the necessary properties are
+ satisfied.
+
+ EXAMPLES:
+
+ The inner product must satisfy its axiom for this algebra to truly
+ be a Euclidean Jordan Algebra::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: y = J.random_element()
+ sage: z = J.random_element()
+ sage: (x*y).inner_product(z) == y.inner_product(x*z)
+ True
+
+ """
+ if (not x in self) or (not y in self):
+ raise TypeError("arguments must live in this algebra")
+ return x.trace_inner_product(y)
+
+
+ def natural_basis(self):
+ """
+ Return a more-natural representation of this algebra's basis.
+
+ Every finite-dimensional Euclidean Jordan Algebra is a direct
+ sum of five simple algebras, four of which comprise Hermitian
+ matrices. This method returns the original "natural" basis
+ for our underlying vector space. (Typically, the natural basis
+ is used to construct the multiplication table in the first place.)
+
+ Note that this will always return a matrix. The standard basis
+ in `R^n` will be returned as `n`-by-`1` column matrices.
+
+ EXAMPLES::
+
+ sage: J = RealSymmetricEJA(2)
+ sage: J.basis()
+ Family (e0, e1, e2)
+ sage: J.natural_basis()
+ (
+ [1 0] [0 1] [0 0]
+ [0 0], [1 0], [0 1]
+ )
+
+ ::
+
+ sage: J = JordanSpinEJA(2)
+ sage: J.basis()
+ Family (e0, e1)
+ sage: J.natural_basis()
+ (
+ [1] [0]
+ [0], [1]
+ )
+
+ """
+ if self._natural_basis is None:
+ return tuple( b.vector().column() for b in self.basis() )
+ else:
+ return self._natural_basis
+
+
def rank(self):
"""
Return the rank of this EJA.
class Element(FiniteDimensionalAlgebraElement):
"""
An element of a Euclidean Jordan algebra.
+ """
- Since EJAs are commutative, the "right multiplication" matrix is
- also the left multiplication matrix and must be symmetric::
+ def __dir__(self):
+ """
+ Oh man, I should not be doing this. This hides the "disabled"
+ methods ``left_matrix`` and ``matrix`` from introspection;
+ in particular it removes them from tab-completion.
+ """
+ return filter(lambda s: s not in ['left_matrix', 'matrix'],
+ dir(self.__class__) )
- sage: set_random_seed()
- sage: n = ZZ.random_element(1,10).abs()
- sage: J = eja_rn(5)
- sage: J.random_element().matrix().is_symmetric()
- True
- sage: J = eja_ln(5)
- sage: J.random_element().matrix().is_symmetric()
- True
- """
+ def __init__(self, A, elt=None):
+ """
+ EXAMPLES:
+
+ The identity in `S^n` is converted to the identity in the EJA::
+
+ sage: J = RealSymmetricEJA(3)
+ sage: I = identity_matrix(QQ,3)
+ sage: J(I) == J.one()
+ True
+
+ This skew-symmetric matrix can't be represented in the EJA::
+
+ sage: J = RealSymmetricEJA(3)
+ sage: A = matrix(QQ,3, lambda i,j: i-j)
+ sage: J(A)
+ Traceback (most recent call last):
+ ...
+ ArithmeticError: vector is not in free module
+
+ """
+ # Goal: if we're given a matrix, and if it lives in our
+ # parent algebra's "natural ambient space," convert it
+ # into an algebra element.
+ #
+ # The catch is, we make a recursive call after converting
+ # the given matrix into a vector that lives in the algebra.
+ # This we need to try the parent class initializer first,
+ # to avoid recursing forever if we're given something that
+ # already fits into the algebra, but also happens to live
+ # in the parent's "natural ambient space" (this happens with
+ # vectors in R^n).
+ try:
+ FiniteDimensionalAlgebraElement.__init__(self, A, elt)
+ except ValueError:
+ natural_basis = A.natural_basis()
+ if elt in natural_basis[0].matrix_space():
+ # Thanks for nothing! Matrix spaces aren't vector
+ # spaces in Sage, so we have to figure out its
+ # natural-basis coordinates ourselves.
+ V = VectorSpace(elt.base_ring(), elt.nrows()**2)
+ W = V.span( _mat2vec(s) for s in natural_basis )
+ coords = W.coordinates(_mat2vec(elt))
+ FiniteDimensionalAlgebraElement.__init__(self, A, coords)
def __pow__(self, n):
"""
instead of column vectors! We, on the other hand, assume column
vectors everywhere.
- EXAMPLES:
+ EXAMPLES::
sage: set_random_seed()
- sage: J = eja_ln(5)
- sage: x = J.random_element()
- sage: x.matrix()*x.vector() == (x**2).vector()
+ sage: x = random_eja().random_element()
+ sage: x.operator_matrix()*x.vector() == (x^2).vector()
+ True
+
+ A few examples of power-associativity::
+
+ sage: set_random_seed()
+ sage: x = random_eja().random_element()
+ sage: x*(x*x)*(x*x) == x^5
+ True
+ sage: (x*x)*(x*x*x) == x^5
+ True
+
+ We also know that powers operator-commute (Koecher, Chapter
+ III, Corollary 1)::
+
+ sage: set_random_seed()
+ sage: x = random_eja().random_element()
+ sage: m = ZZ.random_element(0,10)
+ sage: n = ZZ.random_element(0,10)
+ sage: Lxm = (x^m).operator_matrix()
+ sage: Lxn = (x^n).operator_matrix()
+ sage: Lxm*Lxn == Lxn*Lxm
True
"""
elif n == 1:
return self
else:
- return A.element_class(A, (self.matrix()**(n-1))*self.vector())
+ return A( (self.operator_matrix()**(n-1))*self.vector() )
+
+
+ def apply_univariate_polynomial(self, p):
+ """
+ Apply the univariate polynomial ``p`` to this element.
+
+ A priori, SageMath won't allow us to apply a univariate
+ polynomial to an element of an EJA, because we don't know
+ that EJAs are rings (they are usually not associative). Of
+ course, we know that EJAs are power-associative, so the
+ operation is ultimately kosher. This function sidesteps
+ the CAS to get the answer we want and expect.
+
+ EXAMPLES::
+
+ sage: R = PolynomialRing(QQ, 't')
+ sage: t = R.gen(0)
+ sage: p = t^4 - t^3 + 5*t - 2
+ sage: J = RealCartesianProductEJA(5)
+ sage: J.one().apply_univariate_polynomial(p) == 3*J.one()
+ True
+
+ TESTS:
+
+ We should always get back an element of the algebra::
+
+ sage: set_random_seed()
+ sage: p = PolynomialRing(QQ, 't').random_element()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: x.apply_univariate_polynomial(p) in J
+ True
+
+ """
+ if len(p.variables()) > 1:
+ raise ValueError("not a univariate polynomial")
+ P = self.parent()
+ R = P.base_ring()
+ # Convert the coeficcients to the parent's base ring,
+ # because a priori they might live in an (unnecessarily)
+ # larger ring for which P.sum() would fail below.
+ cs = [ R(c) for c in p.coefficients(sparse=False) ]
+ return P.sum( cs[k]*(self**k) for k in range(len(cs)) )
def characteristic_polynomial(self):
"""
- Return my characteristic polynomial (if I'm a regular
- element).
+ Return the characteristic polynomial of this element.
+
+ EXAMPLES:
+
+ The rank of `R^3` is three, and the minimal polynomial of
+ the identity element is `(t-1)` from which it follows that
+ the characteristic polynomial should be `(t-1)^3`::
+
+ sage: J = RealCartesianProductEJA(3)
+ sage: J.one().characteristic_polynomial()
+ t^3 - 3*t^2 + 3*t - 1
+
+ Likewise, the characteristic of the zero element in the
+ rank-three algebra `R^{n}` should be `t^{3}`::
+
+ sage: J = RealCartesianProductEJA(3)
+ sage: J.zero().characteristic_polynomial()
+ t^3
+
+ The characteristic polynomial of an element should evaluate
+ to zero on that element::
+
+ sage: set_random_seed()
+ sage: x = RealCartesianProductEJA(3).random_element()
+ sage: p = x.characteristic_polynomial()
+ sage: x.apply_univariate_polynomial(p)
+ 0
+
+ """
+ p = self.parent().characteristic_polynomial()
+ return p(*self.vector())
+
- Eventually this should be implemented in terms of the parent
- algebra's characteristic polynomial that works for ALL
- elements.
+ def inner_product(self, other):
"""
- if self.is_regular():
- return self.minimal_polynomial()
- else:
- raise NotImplementedError('irregular element')
+ Return the parent algebra's inner product of myself and ``other``.
+
+ EXAMPLES:
+
+ The inner product in the Jordan spin algebra is the usual
+ inner product on `R^n` (this example only works because the
+ basis for the Jordan algebra is the standard basis in `R^n`)::
+
+ sage: J = JordanSpinEJA(3)
+ sage: x = vector(QQ,[1,2,3])
+ sage: y = vector(QQ,[4,5,6])
+ sage: x.inner_product(y)
+ 32
+ sage: J(x).inner_product(J(y))
+ 32
+
+ The inner product on `S^n` is `<X,Y> = trace(X*Y)`, where
+ multiplication is the usual matrix multiplication in `S^n`,
+ so the inner product of the identity matrix with itself
+ should be the `n`::
+
+ sage: J = RealSymmetricEJA(3)
+ sage: J.one().inner_product(J.one())
+ 3
+
+ Likewise, the inner product on `C^n` is `<X,Y> =
+ Re(trace(X*Y))`, where we must necessarily take the real
+ part because the product of Hermitian matrices may not be
+ Hermitian::
+
+ sage: J = ComplexHermitianEJA(3)
+ sage: J.one().inner_product(J.one())
+ 3
+
+ Ditto for the quaternions::
+
+ sage: J = QuaternionHermitianEJA(3)
+ sage: J.one().inner_product(J.one())
+ 3
+
+ TESTS:
+
+ Ensure that we can always compute an inner product, and that
+ it gives us back a real number::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: y = J.random_element()
+ sage: x.inner_product(y) in RR
+ True
+
+ """
+ P = self.parent()
+ if not other in P:
+ raise TypeError("'other' must live in the same algebra")
+
+ return P.inner_product(self, other)
+
+
+ def operator_commutes_with(self, other):
+ """
+ Return whether or not this element operator-commutes
+ with ``other``.
+
+ EXAMPLES:
+
+ The definition of a Jordan algebra says that any element
+ operator-commutes with its square::
+
+ sage: set_random_seed()
+ sage: x = random_eja().random_element()
+ sage: x.operator_commutes_with(x^2)
+ True
+
+ TESTS:
+
+ Test Lemma 1 from Chapter III of Koecher::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: u = J.random_element()
+ sage: v = J.random_element()
+ sage: lhs = u.operator_commutes_with(u*v)
+ sage: rhs = v.operator_commutes_with(u^2)
+ sage: lhs == rhs
+ True
+
+ """
+ if not other in self.parent():
+ raise TypeError("'other' must live in the same algebra")
+
+ A = self.operator_matrix()
+ B = other.operator_matrix()
+ return (A*B == B*A)
def det(self):
EXAMPLES::
- sage: J = eja_ln(2)
+ sage: J = JordanSpinEJA(2)
sage: e0,e1 = J.gens()
- sage: x = e0 + e1
+ sage: x = sum( J.gens() )
sage: x.det()
0
- sage: J = eja_ln(3)
+
+ ::
+
+ sage: J = JordanSpinEJA(3)
sage: e0,e1,e2 = J.gens()
- sage: x = e0 + e1 + e2
+ sage: x = sum( J.gens() )
sage: x.det()
-1
+ TESTS:
+
+ An element is invertible if and only if its determinant is
+ non-zero::
+
+ sage: set_random_seed()
+ sage: x = random_eja().random_element()
+ sage: x.is_invertible() == (x.det() != 0)
+ True
+
"""
- cs = self.characteristic_polynomial().coefficients(sparse=False)
- r = len(cs) - 1
- if r >= 0:
- return cs[0] * (-1)**r
- else:
- raise ValueError('charpoly had no coefficients')
+ P = self.parent()
+ r = P.rank()
+ p = P._charpoly_coeff(0)
+ # The _charpoly_coeff function already adds the factor of
+ # -1 to ensure that _charpoly_coeff(0) is really what
+ # appears in front of t^{0} in the charpoly. However,
+ # we want (-1)^r times THAT for the determinant.
+ return ((-1)**r)*p(*self.vector())
+
+
+ def inverse(self):
+ """
+ Return the Jordan-multiplicative inverse of this element.
+
+ ALGORITHM:
+
+ We appeal to the quadratic representation as in Koecher's
+ Theorem 12 in Chapter III, Section 5.
+
+ EXAMPLES:
+
+ The inverse in the spin factor algebra is given in Alizadeh's
+ Example 11.11::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,10)
+ sage: J = JordanSpinEJA(n)
+ sage: x = J.random_element()
+ sage: while not x.is_invertible():
+ ....: x = J.random_element()
+ sage: x_vec = x.vector()
+ sage: x0 = x_vec[0]
+ sage: x_bar = x_vec[1:]
+ sage: coeff = ~(x0^2 - x_bar.inner_product(x_bar))
+ sage: inv_vec = x_vec.parent()([x0] + (-x_bar).list())
+ sage: x_inverse = coeff*inv_vec
+ sage: x.inverse() == J(x_inverse)
+ True
+
+ TESTS:
+
+ The identity element is its own inverse::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: J.one().inverse() == J.one()
+ True
+
+ If an element has an inverse, it acts like one::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: (not x.is_invertible()) or (x.inverse()*x == J.one())
+ True
+
+ The inverse of the inverse is what we started with::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: (not x.is_invertible()) or (x.inverse().inverse() == x)
+ True
+
+ The zero element is never invertible::
+
+ sage: set_random_seed()
+ sage: J = random_eja().zero().inverse()
+ Traceback (most recent call last):
+ ...
+ ValueError: element is not invertible
+
+ """
+ if not self.is_invertible():
+ raise ValueError("element is not invertible")
+
+ return (~self.quadratic_representation())(self)
+
+
+ def is_invertible(self):
+ """
+ Return whether or not this element is invertible.
+
+ We can't use the superclass method because it relies on
+ the algebra being associative.
+
+ ALGORITHM:
+
+ The usual way to do this is to check if the determinant is
+ zero, but we need the characteristic polynomial for the
+ determinant. The minimal polynomial is a lot easier to get,
+ so we use Corollary 2 in Chapter V of Koecher to check
+ whether or not the paren't algebra's zero element is a root
+ of this element's minimal polynomial.
+
+ TESTS:
+
+ The identity element is always invertible::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: J.one().is_invertible()
+ True
+
+ The zero element is never invertible::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: J.zero().is_invertible()
+ False
+
+ """
+ zero = self.parent().zero()
+ p = self.minimal_polynomial()
+ return not (p(zero) == zero)
def is_nilpotent(self):
The identity element is never nilpotent::
sage: set_random_seed()
- sage: n = ZZ.random_element(2,10).abs()
- sage: J = eja_rn(n)
- sage: J.one().is_nilpotent()
- False
- sage: J = eja_ln(n)
- sage: J.one().is_nilpotent()
+ sage: random_eja().one().is_nilpotent()
False
The additive identity is always nilpotent::
sage: set_random_seed()
- sage: n = ZZ.random_element(2,10).abs()
- sage: J = eja_rn(n)
- sage: J.zero().is_nilpotent()
- True
- sage: J = eja_ln(n)
- sage: J.zero().is_nilpotent()
+ sage: random_eja().zero().is_nilpotent()
True
"""
The identity element always has degree one, but any element
linearly-independent from it is regular::
- sage: J = eja_ln(5)
+ sage: J = JordanSpinEJA(5)
sage: J.one().is_regular()
False
sage: e0, e1, e2, e3, e4 = J.gens() # e0 is the identity
EXAMPLES::
- sage: J = eja_ln(4)
+ sage: J = JordanSpinEJA(4)
sage: J.one().degree()
1
sage: e0,e1,e2,e3 = J.gens()
aren't multiples of the identity are regular::
sage: set_random_seed()
- sage: n = ZZ.random_element(1,10).abs()
- sage: J = eja_ln(n)
+ sage: n = ZZ.random_element(1,10)
+ sage: J = JordanSpinEJA(n)
sage: x = J.random_element()
sage: x == x.coefficient(0)*J.one() or x.degree() == 2
True
return self.span_of_powers().dimension()
- def matrix(self):
+ def left_matrix(self):
+ """
+ Our parent class defines ``left_matrix`` and ``matrix``
+ methods whose names are misleading. We don't want them.
+ """
+ raise NotImplementedError("use operator_matrix() instead")
+
+ matrix = left_matrix
+
+
+ def minimal_polynomial(self):
+ """
+ Return the minimal polynomial of this element,
+ as a function of the variable `t`.
+
+ ALGORITHM:
+
+ We restrict ourselves to the associative subalgebra
+ generated by this element, and then return the minimal
+ polynomial of this element's operator matrix (in that
+ subalgebra). This works by Baes Proposition 2.3.16.
+
+ TESTS:
+
+ The minimal polynomial of the identity and zero elements are
+ always the same::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: J.one().minimal_polynomial()
+ t - 1
+ sage: J.zero().minimal_polynomial()
+ t
+
+ The degree of an element is (by one definition) the degree
+ of its minimal polynomial::
+
+ sage: set_random_seed()
+ sage: x = random_eja().random_element()
+ sage: x.degree() == x.minimal_polynomial().degree()
+ True
+
+ The minimal polynomial and the characteristic polynomial coincide
+ and are known (see Alizadeh, Example 11.11) for all elements of
+ the spin factor algebra that aren't scalar multiples of the
+ identity::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(2,10)
+ sage: J = JordanSpinEJA(n)
+ sage: y = J.random_element()
+ sage: while y == y.coefficient(0)*J.one():
+ ....: y = J.random_element()
+ sage: y0 = y.vector()[0]
+ sage: y_bar = y.vector()[1:]
+ sage: actual = y.minimal_polynomial()
+ sage: t = PolynomialRing(J.base_ring(),'t').gen(0)
+ sage: expected = t^2 - 2*y0*t + (y0^2 - norm(y_bar)^2)
+ sage: bool(actual == expected)
+ True
+
+ The minimal polynomial should always kill its element::
+
+ sage: set_random_seed()
+ sage: x = random_eja().random_element()
+ sage: p = x.minimal_polynomial()
+ sage: x.apply_univariate_polynomial(p)
+ 0
+
+ """
+ V = self.span_of_powers()
+ assoc_subalg = self.subalgebra_generated_by()
+ # Mis-design warning: the basis used for span_of_powers()
+ # and subalgebra_generated_by() must be the same, and in
+ # the same order!
+ elt = assoc_subalg(V.coordinates(self.vector()))
+
+ # We get back a symbolic polynomial in 'x' but want a real
+ # polynomial in 't'.
+ p_of_x = elt.operator_matrix().minimal_polynomial()
+ return p_of_x.change_variable_name('t')
+
+
+ def natural_representation(self):
+ """
+ Return a more-natural representation of this element.
+
+ Every finite-dimensional Euclidean Jordan Algebra is a
+ direct sum of five simple algebras, four of which comprise
+ Hermitian matrices. This method returns the original
+ "natural" representation of this element as a Hermitian
+ matrix, if it has one. If not, you get the usual representation.
+
+ EXAMPLES::
+
+ sage: J = ComplexHermitianEJA(3)
+ sage: J.one()
+ e0 + e5 + e8
+ sage: J.one().natural_representation()
+ [1 0 0 0 0 0]
+ [0 1 0 0 0 0]
+ [0 0 1 0 0 0]
+ [0 0 0 1 0 0]
+ [0 0 0 0 1 0]
+ [0 0 0 0 0 1]
+
+ ::
+
+ sage: J = QuaternionHermitianEJA(3)
+ sage: J.one()
+ e0 + e9 + e14
+ sage: J.one().natural_representation()
+ [1 0 0 0 0 0 0 0 0 0 0 0]
+ [0 1 0 0 0 0 0 0 0 0 0 0]
+ [0 0 1 0 0 0 0 0 0 0 0 0]
+ [0 0 0 1 0 0 0 0 0 0 0 0]
+ [0 0 0 0 1 0 0 0 0 0 0 0]
+ [0 0 0 0 0 1 0 0 0 0 0 0]
+ [0 0 0 0 0 0 1 0 0 0 0 0]
+ [0 0 0 0 0 0 0 1 0 0 0 0]
+ [0 0 0 0 0 0 0 0 1 0 0 0]
+ [0 0 0 0 0 0 0 0 0 1 0 0]
+ [0 0 0 0 0 0 0 0 0 0 1 0]
+ [0 0 0 0 0 0 0 0 0 0 0 1]
+
+ """
+ B = self.parent().natural_basis()
+ W = B[0].matrix_space()
+ return W.linear_combination(zip(self.vector(), B))
+
+
+ def operator(self):
+ """
+ Return the left-multiplication-by-this-element
+ operator on the ambient algebra.
+
+ TESTS::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: y = J.random_element()
+ sage: x.operator()(y) == x*y
+ True
+ sage: y.operator()(x) == x*y
+ True
+
+ """
+ P = self.parent()
+ return FiniteDimensionalEuclideanJordanAlgebraMorphism(
+ Hom(P,P),
+ self.operator_matrix() )
+
+
+
+ def operator_matrix(self):
"""
Return the matrix that represents left- (or right-)
multiplication by this element in the parent algebra.
- We have to override this because the superclass method
- returns a matrix that acts on row vectors (that is, on
- the right).
- """
- fda_elt = FiniteDimensionalAlgebraElement(self.parent(), self)
- return fda_elt.matrix().transpose()
+ We implement this ourselves to work around the fact that
+ our parent class represents everything with row vectors.
+
+ EXAMPLES:
+
+ Test the first polarization identity from my notes, Koecher Chapter
+ III, or from Baes (2.3)::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: y = J.random_element()
+ sage: Lx = x.operator_matrix()
+ sage: Ly = y.operator_matrix()
+ sage: Lxx = (x*x).operator_matrix()
+ sage: Lxy = (x*y).operator_matrix()
+ sage: bool(2*Lx*Lxy + Ly*Lxx == 2*Lxy*Lx + Lxx*Ly)
+ True
+
+ Test the second polarization identity from my notes or from
+ Baes (2.4)::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: y = J.random_element()
+ sage: z = J.random_element()
+ sage: Lx = x.operator_matrix()
+ sage: Ly = y.operator_matrix()
+ sage: Lz = z.operator_matrix()
+ sage: Lzy = (z*y).operator_matrix()
+ sage: Lxy = (x*y).operator_matrix()
+ sage: Lxz = (x*z).operator_matrix()
+ sage: bool(Lx*Lzy + Lz*Lxy + Ly*Lxz == Lzy*Lx + Lxy*Lz + Lxz*Ly)
+ True
+
+ Test the third polarization identity from my notes or from
+ Baes (2.5)::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: u = J.random_element()
+ sage: y = J.random_element()
+ sage: z = J.random_element()
+ sage: Lu = u.operator_matrix()
+ sage: Ly = y.operator_matrix()
+ sage: Lz = z.operator_matrix()
+ sage: Lzy = (z*y).operator_matrix()
+ sage: Luy = (u*y).operator_matrix()
+ sage: Luz = (u*z).operator_matrix()
+ sage: Luyz = (u*(y*z)).operator_matrix()
+ sage: lhs = Lu*Lzy + Lz*Luy + Ly*Luz
+ sage: rhs = Luyz + Ly*Lu*Lz + Lz*Lu*Ly
+ sage: bool(lhs == rhs)
+ True
+
+ """
+ fda_elt = FiniteDimensionalAlgebraElement(self.parent(), self)
+ return fda_elt.matrix().transpose()
+
+
+ def quadratic_representation(self, other=None):
+ """
+ Return the quadratic representation of this element.
+
+ EXAMPLES:
+
+ The explicit form in the spin factor algebra is given by
+ Alizadeh's Example 11.12::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,10)
+ sage: J = JordanSpinEJA(n)
+ sage: x = J.random_element()
+ sage: x_vec = x.vector()
+ sage: x0 = x_vec[0]
+ sage: x_bar = x_vec[1:]
+ sage: A = matrix(QQ, 1, [x_vec.inner_product(x_vec)])
+ sage: B = 2*x0*x_bar.row()
+ sage: C = 2*x0*x_bar.column()
+ sage: D = identity_matrix(QQ, n-1)
+ sage: D = (x0^2 - x_bar.inner_product(x_bar))*D
+ sage: D = D + 2*x_bar.tensor_product(x_bar)
+ sage: Q = block_matrix(2,2,[A,B,C,D])
+ sage: Q == x.quadratic_representation().matrix()
+ True
+
+ Test all of the properties from Theorem 11.2 in Alizadeh::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: y = J.random_element()
+ sage: Lx = x.operator()
+ sage: Lxx = (x*x).operator()
+ sage: Qx = x.quadratic_representation()
+ sage: Qy = y.quadratic_representation()
+ sage: Qxy = x.quadratic_representation(y)
+ sage: Qex = J.one().quadratic_representation(x)
+ sage: n = ZZ.random_element(10)
+ sage: Qxn = (x^n).quadratic_representation()
+
+ Property 1:
+
+ sage: 2*Qxy == (x+y).quadratic_representation() - Qx - Qy
+ True
+
+ Property 2:
+
+ sage: alpha = QQ.random_element()
+ sage: (alpha*x).quadratic_representation() == (alpha^2)*Qx
+ True
+
+ Property 3:
+
+ sage: not x.is_invertible() or ( Qx(x.inverse()) == x )
+ True
+
+ sage: not x.is_invertible() or (
+ ....: ~Qx
+ ....: ==
+ ....: x.inverse().quadratic_representation() )
+ True
+
+ sage: Qxy(J.one()) == x*y
+ True
+
+ Property 4:
+
+ sage: not x.is_invertible() or (
+ ....: x.quadratic_representation(x.inverse())*Qx
+ ....: == Qx*x.quadratic_representation(x.inverse()) )
+ True
+
+ sage: not x.is_invertible() or (
+ ....: x.quadratic_representation(x.inverse())*Qx
+ ....: ==
+ ....: 2*x.operator()*Qex - Qx )
+ True
+
+ sage: 2*x.operator()*Qex - Qx == Lxx
+ True
+ Property 5:
- def minimal_polynomial(self):
- """
- EXAMPLES::
+ sage: Qy(x).quadratic_representation() == Qy*Qx*Qy
+ True
- sage: set_random_seed()
- sage: n = ZZ.random_element(1,10).abs()
- sage: J = eja_rn(n)
- sage: x = J.random_element()
- sage: x.degree() == x.minimal_polynomial().degree()
+ Property 6:
+
+ sage: Qxn == (Qx)^n
True
- ::
+ Property 7:
- sage: set_random_seed()
- sage: n = ZZ.random_element(1,10).abs()
- sage: J = eja_ln(n)
- sage: x = J.random_element()
- sage: x.degree() == x.minimal_polynomial().degree()
+ sage: not x.is_invertible() or (
+ ....: Qx*x.inverse().operator() == Lx )
True
- The minimal polynomial and the characteristic polynomial coincide
- and are known (see Alizadeh, Example 11.11) for all elements of
- the spin factor algebra that aren't scalar multiples of the
- identity::
+ Property 8:
- sage: set_random_seed()
- sage: n = ZZ.random_element(2,10).abs()
- sage: J = eja_ln(n)
- sage: y = J.random_element()
- sage: while y == y.coefficient(0)*J.one():
- ....: y = J.random_element()
- sage: y0 = y.vector()[0]
- sage: y_bar = y.vector()[1:]
- sage: actual = y.minimal_polynomial()
- sage: x = SR.symbol('x', domain='real')
- sage: expected = x^2 - 2*y0*x + (y0^2 - norm(y_bar)^2)
- sage: bool(actual == expected)
+ sage: not x.operator_commutes_with(y) or (
+ ....: Qx(y)^n == Qxn(y^n) )
True
"""
- # The element we're going to call "minimal_polynomial()" on.
- # Either myself, interpreted as an element of a finite-
- # dimensional algebra, or an element of an associative
- # subalgebra.
- elt = None
+ if other is None:
+ other=self
+ elif not other in self.parent():
+ raise TypeError("'other' must live in the same algebra")
- if self.parent().is_associative():
- elt = FiniteDimensionalAlgebraElement(self.parent(), self)
- else:
- V = self.span_of_powers()
- assoc_subalg = self.subalgebra_generated_by()
- # Mis-design warning: the basis used for span_of_powers()
- # and subalgebra_generated_by() must be the same, and in
- # the same order!
- elt = assoc_subalg(V.coordinates(self.vector()))
-
- # Recursive call, but should work since elt lives in an
- # associative algebra.
- return elt.minimal_polynomial()
+ L = self.operator()
+ M = other.operator()
+ return ( L*M + M*L - (self*other).operator() )
def span_of_powers(self):
# The dimension of the subalgebra can't be greater than
# the big algebra, so just put everything into a list
# and let span() get rid of the excess.
- V = self.vector().parent()
+ #
+ # We do the extra ambient_vector_space() in case we're messing
+ # with polynomials and the direct parent is a module.
+ V = self.vector().parent().ambient_vector_space()
return V.span( (self**d).vector() for d in xrange(V.dimension()) )
TESTS::
sage: set_random_seed()
- sage: n = ZZ.random_element(1,10).abs()
- sage: J = eja_rn(n)
- sage: x = J.random_element()
- sage: x.subalgebra_generated_by().is_associative()
- True
- sage: J = eja_ln(n)
- sage: x = J.random_element()
+ sage: x = random_eja().random_element()
sage: x.subalgebra_generated_by().is_associative()
True
Squaring in the subalgebra should be the same thing as
squaring in the superalgebra::
- sage: J = eja_ln(5)
- sage: x = J.random_element()
+ sage: set_random_seed()
+ sage: x = random_eja().random_element()
sage: u = x.subalgebra_generated_by().random_element()
- sage: u.matrix()*u.vector() == (u**2).vector()
+ sage: u.operator_matrix()*u.vector() == (u**2).vector()
True
"""
TESTS::
sage: set_random_seed()
- sage: J = eja_rn(5)
- sage: c = J.random_element().subalgebra_idempotent()
- sage: c^2 == c
- True
- sage: J = eja_ln(5)
- sage: c = J.random_element().subalgebra_idempotent()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: while x.is_nilpotent():
+ ....: x = J.random_element()
+ sage: c = x.subalgebra_idempotent()
sage: c^2 == c
True
s = 0
minimal_dim = V.dimension()
for i in xrange(1, V.dimension()):
- this_dim = (u**i).matrix().image().dimension()
+ this_dim = (u**i).operator_matrix().image().dimension()
if this_dim < minimal_dim:
minimal_dim = this_dim
s = i
# Beware, solve_right() means that we're using COLUMN vectors.
# Our FiniteDimensionalAlgebraElement superclass uses rows.
u_next = u**(s+1)
- A = u_next.matrix()
+ A = u_next.operator_matrix()
c_coordinates = A.solve_right(u_next.vector())
# Now c_coordinates is the idempotent we want, but it's in
EXAMPLES::
- sage: J = eja_ln(3)
- sage: e0,e1,e2 = J.gens()
- sage: x = e0 + e1 + e2
+ sage: J = JordanSpinEJA(3)
+ sage: x = sum(J.gens())
sage: x.trace()
2
+ ::
+
+ sage: J = RealCartesianProductEJA(5)
+ sage: J.one().trace()
+ 5
+
+ TESTS:
+
+ The trace of an element is a real number::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: J.random_element().trace() in J.base_ring()
+ True
+
"""
- cs = self.characteristic_polynomial().coefficients(sparse=False)
- if len(cs) >= 2:
- return -1*cs[-2]
- else:
- raise ValueError('charpoly had fewer than 2 coefficients')
+ P = self.parent()
+ r = P.rank()
+ p = P._charpoly_coeff(r-1)
+ # The _charpoly_coeff function already adds the factor of
+ # -1 to ensure that _charpoly_coeff(r-1) is really what
+ # appears in front of t^{r-1} in the charpoly. However,
+ # we want the negative of THAT for the trace.
+ return -p(*self.vector())
+
+
+ def trace_inner_product(self, other):
+ """
+ Return the trace inner product of myself and ``other``.
+
+ TESTS:
+
+ The trace inner product is commutative::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: x = J.random_element(); y = J.random_element()
+ sage: x.trace_inner_product(y) == y.trace_inner_product(x)
+ True
+
+ The trace inner product is bilinear::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: y = J.random_element()
+ sage: z = J.random_element()
+ sage: a = QQ.random_element();
+ sage: actual = (a*(x+z)).trace_inner_product(y)
+ sage: expected = ( a*x.trace_inner_product(y) +
+ ....: a*z.trace_inner_product(y) )
+ sage: actual == expected
+ True
+ sage: actual = x.trace_inner_product(a*(y+z))
+ sage: expected = ( a*x.trace_inner_product(y) +
+ ....: a*x.trace_inner_product(z) )
+ sage: actual == expected
+ True
+
+ The trace inner product satisfies the compatibility
+ condition in the definition of a Euclidean Jordan algebra::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: y = J.random_element()
+ sage: z = J.random_element()
+ sage: (x*y).trace_inner_product(z) == y.trace_inner_product(x*z)
+ True
+
+ """
+ if not other in self.parent():
+ raise TypeError("'other' must live in the same algebra")
+
+ return (self*other).trace()
-def eja_rn(dimension, field=QQ):
+class RealCartesianProductEJA(FiniteDimensionalEuclideanJordanAlgebra):
"""
Return the Euclidean Jordan Algebra corresponding to the set
`R^n` under the Hadamard product.
+ Note: this is nothing more than the Cartesian product of ``n``
+ copies of the spin algebra. Once Cartesian product algebras
+ are implemented, this can go.
+
EXAMPLES:
This multiplication table can be verified by hand::
- sage: J = eja_rn(3)
+ sage: J = RealCartesianProductEJA(3)
sage: e0,e1,e2 = J.gens()
sage: e0*e0
e0
e2
"""
- # The FiniteDimensionalAlgebra constructor takes a list of
- # matrices, the ith representing right multiplication by the ith
- # basis element in the vector space. So if e_1 = (1,0,0), then
- # right (Hadamard) multiplication of x by e_1 picks out the first
- # component of x; and likewise for the ith basis element e_i.
- Qs = [ matrix(field, dimension, dimension, lambda k,j: 1*(k == j == i))
- for i in xrange(dimension) ]
+ @staticmethod
+ def __classcall_private__(cls, n, field=QQ):
+ # The FiniteDimensionalAlgebra constructor takes a list of
+ # matrices, the ith representing right multiplication by the ith
+ # basis element in the vector space. So if e_1 = (1,0,0), then
+ # right (Hadamard) multiplication of x by e_1 picks out the first
+ # component of x; and likewise for the ith basis element e_i.
+ Qs = [ matrix(field, n, n, lambda k,j: 1*(k == j == i))
+ for i in xrange(n) ]
- return FiniteDimensionalEuclideanJordanAlgebra(field,Qs,rank=dimension)
+ fdeja = super(RealCartesianProductEJA, cls)
+ return fdeja.__classcall_private__(cls, field, Qs, rank=n)
+ def inner_product(self, x, y):
+ return _usual_ip(x,y)
-def eja_ln(dimension, field=QQ):
+
+def random_eja():
"""
- Return the Jordan algebra corresponding to the Lorentz "ice cream"
- cone of the given ``dimension``.
+ Return a "random" finite-dimensional Euclidean Jordan Algebra.
- EXAMPLES:
+ ALGORITHM:
- This multiplication table can be verified by hand::
+ For now, we choose a random natural number ``n`` (greater than zero)
+ and then give you back one of the following:
- sage: J = eja_ln(4)
- sage: e0,e1,e2,e3 = J.gens()
- sage: e0*e0
- e0
- sage: e0*e1
- e1
- sage: e0*e2
- e2
- sage: e0*e3
- e3
- sage: e1*e2
- 0
- sage: e1*e3
- 0
- sage: e2*e3
- 0
+ * The cartesian product of the rational numbers ``n`` times; this is
+ ``QQ^n`` with the Hadamard product.
- In one dimension, this is the reals under multiplication::
+ * The Jordan spin algebra on ``QQ^n``.
- sage: J1 = eja_ln(1)
- sage: J2 = eja_rn(1)
- sage: J1 == J2
- True
+ * The ``n``-by-``n`` rational symmetric matrices with the symmetric
+ product.
- """
- Qs = []
- id_matrix = identity_matrix(field,dimension)
- for i in xrange(dimension):
- ei = id_matrix.column(i)
- Qi = zero_matrix(field,dimension)
- Qi.set_row(0, ei)
- Qi.set_column(0, ei)
- Qi += diagonal_matrix(dimension, [ei[0]]*dimension)
- # The addition of the diagonal matrix adds an extra ei[0] in the
- # upper-left corner of the matrix.
- Qi[0,0] = Qi[0,0] * ~field(2)
- Qs.append(Qi)
-
- # The rank of the spin factor algebra is two, UNLESS we're in a
- # one-dimensional ambient space (the rank is bounded by the
- # ambient dimension).
- rank = min(dimension,2)
- return FiniteDimensionalEuclideanJordanAlgebra(field,Qs,rank=rank)
-
-
-def eja_sn(dimension, field=QQ):
- """
- Return the simple Jordan algebra of ``dimension``-by-``dimension``
- symmetric matrices over ``field``.
+ * The ``n``-by-``n`` complex-rational Hermitian matrices embedded
+ in the space of ``2n``-by-``2n`` real symmetric matrices.
- EXAMPLES::
+ * The ``n``-by-``n`` quaternion-rational Hermitian matrices embedded
+ in the space of ``4n``-by-``4n`` real symmetric matrices.
- sage: J = eja_sn(2)
- sage: e0, e1, e2 = J.gens()
- sage: e0*e0
- e0
- sage: e1*e1
- e0 + e2
- sage: e2*e2
- e2
+ Later this might be extended to return Cartesian products of the
+ EJAs above.
+
+ TESTS::
+
+ sage: random_eja()
+ Euclidean Jordan algebra of degree...
"""
- Qs = []
- # 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.
- V = VectorSpace(field, dimension**2)
+ # The max_n component lets us choose different upper bounds on the
+ # value "n" that gets passed to the constructor. This is needed
+ # because e.g. R^{10} is reasonable to test, while the Hermitian
+ # 10-by-10 quaternion matrices are not.
+ (constructor, max_n) = choice([(RealCartesianProductEJA, 6),
+ (JordanSpinEJA, 6),
+ (RealSymmetricEJA, 5),
+ (ComplexHermitianEJA, 4),
+ (QuaternionHermitianEJA, 3)])
+ n = ZZ.random_element(1, max_n)
+ return constructor(n, field=QQ)
+
+
+def _real_symmetric_basis(n, field=QQ):
+ """
+ Return a basis for the space of real symmetric n-by-n matrices.
+ """
# The basis of symmetric matrices, as matrices, in their R^(n-by-n)
# coordinates.
S = []
-
- for i in xrange(dimension):
+ for i in xrange(n):
for j in xrange(i+1):
- Eij = matrix(field, dimension, lambda k,l: k==i and l==j)
+ Eij = matrix(field, n, lambda k,l: k==i and l==j)
if i == j:
Sij = Eij
else:
+ # Beware, orthogonal but not normalized!
Sij = Eij + Eij.transpose()
S.append(Sij)
+ return tuple(S)
+
+
+def _complex_hermitian_basis(n, field=QQ):
+ """
+ Returns a basis for the space of complex Hermitian n-by-n matrices.
+
+ TESTS::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,5)
+ sage: all( M.is_symmetric() for M in _complex_hermitian_basis(n) )
+ True
+
+ """
+ F = QuadraticField(-1, 'I')
+ 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(field, n, lambda k,l: k==i and l==j)
+ if i == j:
+ Sij = _embed_complex_matrix(Eij)
+ S.append(Sij)
+ else:
+ # Beware, orthogonal but not normalized! 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)
+ return tuple(S)
+
+
+def _quaternion_hermitian_basis(n, field=QQ):
+ """
+ Returns a basis for the space of quaternion Hermitian n-by-n matrices.
+
+ TESTS::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,5)
+ sage: all( M.is_symmetric() for M in _quaternion_hermitian_basis(n) )
+ 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 _mat2vec(m):
+ return vector(m.base_ring(), m.list())
+
+def _vec2mat(v):
+ return matrix(v.base_ring(), sqrt(v.degree()), v.list())
+
+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. A reordered copy
+ of the basis is also returned to work around the fact that
+ the ``span()`` in this function will change the order of the basis
+ from what we think it is, to... something else.
+ """
+ # 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()
- def mat2vec(m):
- return vector(field, m.list())
+ V = VectorSpace(field, dimension**2)
+ W = V.span( _mat2vec(s) for s in basis )
- W = V.span( mat2vec(s) for s in S )
+ # Taking the span above reorders our basis (thanks, jerk!) so we
+ # need to put our "matrix basis" in the same order as the
+ # (reordered) vector basis.
+ S = tuple( _vec2mat(b) for b in W.basis() )
+ Qs = []
for s in S:
# Brute force the multiplication-by-s matrix by looping
# through all elements of the basis and doing the computation
# why we're computing rows here and not columns.
Q_rows = []
for t in S:
- this_row = mat2vec((s*t + t*s)/2)
+ this_row = _mat2vec((s*t + t*s)/2)
Q_rows.append(W.coordinates(this_row))
- Q = matrix(field,Q_rows)
+ Q = matrix(field, W.dimension(), Q_rows)
Qs.append(Q)
- return FiniteDimensionalEuclideanJordanAlgebra(field,Qs,rank=dimension)
+ return (Qs, S)
+
+
+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]]``.
+
+ 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 = ZZ.random_element(5)
+ 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.real()
+ b = z.imag()
+ blocks.append(matrix(field, 2, [[a,b],[-b,a]]))
+
+ # We can drop the imaginaries here.
+ return block_matrix(field.base_ring(), n, blocks)
+
+
+def _unembed_complex_matrix(M):
+ """
+ The inverse of _embed_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
+
+ """
+ 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")
+
+ F = QuadraticField(-1, 'i')
+ 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.
+
+ 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: _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)::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(5)
+ 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
+
+ """
+ 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 block_matrix(quaternions.base_ring(), n, blocks)
+
+
+def _unembed_quaternion_matrix(M):
+ """
+ The inverse of _embed_quaternion_matrix().
+
+ EXAMPLES::
+
+ 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]
+
+ TESTS:
+
+ Unembedding is the inverse of embedding::
+
+ 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")
+
+ 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):
+ """
+ The rank-n simple EJA consisting of real symmetric n-by-n
+ matrices, the usual symmetric Jordan product, and the trace inner
+ product. It has dimension `(n^2 + n)/2` over the reals.
+
+ EXAMPLES::
+
+ sage: J = RealSymmetricEJA(2)
+ sage: e0, e1, e2 = J.gens()
+ sage: e0*e0
+ e0
+ sage: e1*e1
+ e0 + e2
+ sage: e2*e2
+ e2
+
+ TESTS:
+
+ The degree of this algebra is `(n^2 + n) / 2`::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,5)
+ sage: J = RealSymmetricEJA(n)
+ sage: J.degree() == (n^2 + n)/2
+ True
+
+ The Jordan multiplication is what we think it is::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,5)
+ sage: J = RealSymmetricEJA(n)
+ sage: x = J.random_element()
+ sage: y = J.random_element()
+ sage: actual = (x*y).natural_representation()
+ sage: X = x.natural_representation()
+ sage: Y = y.natural_representation()
+ sage: expected = (X*Y + Y*X)/2
+ sage: actual == expected
+ True
+ sage: J(expected) == x*y
+ True
+
+ """
+ @staticmethod
+ def __classcall_private__(cls, n, field=QQ):
+ S = _real_symmetric_basis(n, field=field)
+ (Qs, T) = _multiplication_table_from_matrix_basis(S)
+
+ fdeja = super(RealSymmetricEJA, cls)
+ return fdeja.__classcall_private__(cls,
+ field,
+ Qs,
+ rank=n,
+ natural_basis=T)
+
+ def inner_product(self, x, y):
+ return _matrix_ip(x,y)
+
+
+class ComplexHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra):
+ """
+ The rank-n simple EJA consisting of complex Hermitian n-by-n
+ matrices over the real numbers, the usual symmetric Jordan product,
+ and the real-part-of-trace inner product. It has dimension `n^2` over
+ the reals.
+
+ TESTS:
+
+ The degree of this algebra is `n^2`::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,5)
+ sage: J = ComplexHermitianEJA(n)
+ sage: J.degree() == n^2
+ True
+
+ The Jordan multiplication is what we think it is::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,5)
+ sage: J = ComplexHermitianEJA(n)
+ sage: x = J.random_element()
+ sage: y = J.random_element()
+ sage: actual = (x*y).natural_representation()
+ sage: X = x.natural_representation()
+ sage: Y = y.natural_representation()
+ sage: expected = (X*Y + Y*X)/2
+ sage: actual == expected
+ True
+ sage: J(expected) == x*y
+ True
+
+ """
+ @staticmethod
+ def __classcall_private__(cls, n, field=QQ):
+ S = _complex_hermitian_basis(n)
+ (Qs, T) = _multiplication_table_from_matrix_basis(S)
+
+ fdeja = super(ComplexHermitianEJA, cls)
+ return fdeja.__classcall_private__(cls,
+ field,
+ Qs,
+ rank=n,
+ natural_basis=T)
+
+ def inner_product(self, x, y):
+ # Since a+bi on the diagonal is represented as
+ #
+ # a + bi = [ a b ]
+ # [ -b a ],
+ #
+ # we'll double-count the "a" entries if we take the trace of
+ # the embedding.
+ return _matrix_ip(x,y)/2
+
+
+class QuaternionHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra):
+ """
+ The rank-n simple EJA consisting of self-adjoint n-by-n quaternion
+ matrices, the usual symmetric Jordan product, and the
+ real-part-of-trace inner product. It has dimension `2n^2 - n` over
+ the reals.
+
+ TESTS:
+
+ The degree of this algebra is `n^2`::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,5)
+ sage: J = QuaternionHermitianEJA(n)
+ sage: J.degree() == 2*(n^2) - n
+ True
+
+ The Jordan multiplication is what we think it is::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,5)
+ sage: J = QuaternionHermitianEJA(n)
+ sage: x = J.random_element()
+ sage: y = J.random_element()
+ sage: actual = (x*y).natural_representation()
+ sage: X = x.natural_representation()
+ sage: Y = y.natural_representation()
+ sage: expected = (X*Y + Y*X)/2
+ sage: actual == expected
+ True
+ sage: J(expected) == x*y
+ True
+
+ """
+ @staticmethod
+ def __classcall_private__(cls, n, field=QQ):
+ S = _quaternion_hermitian_basis(n)
+ (Qs, T) = _multiplication_table_from_matrix_basis(S)
+
+ fdeja = super(QuaternionHermitianEJA, cls)
+ return fdeja.__classcall_private__(cls,
+ field,
+ Qs,
+ rank=n,
+ natural_basis=T)
+
+ def inner_product(self, x, y):
+ # Since a+bi+cj+dk on the diagonal is represented as
+ #
+ # a + bi +cj + dk = [ a b c d]
+ # [ -b a -d c]
+ # [ -c d a -b]
+ # [ -d -c b a],
+ #
+ # we'll quadruple-count the "a" entries if we take the trace of
+ # the embedding.
+ return _matrix_ip(x,y)/4
+
+
+class JordanSpinEJA(FiniteDimensionalEuclideanJordanAlgebra):
+ """
+ The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
+ with the usual inner product and jordan product ``x*y =
+ (<x_bar,y_bar>, x0*y_bar + y0*x_bar)``. It has dimension `n` over
+ the reals.
+
+ EXAMPLES:
+
+ This multiplication table can be verified by hand::
+
+ sage: J = JordanSpinEJA(4)
+ sage: e0,e1,e2,e3 = J.gens()
+ sage: e0*e0
+ e0
+ sage: e0*e1
+ e1
+ sage: e0*e2
+ e2
+ sage: e0*e3
+ e3
+ sage: e1*e2
+ 0
+ sage: e1*e3
+ 0
+ sage: e2*e3
+ 0
+
+ """
+ @staticmethod
+ def __classcall_private__(cls, n, field=QQ):
+ Qs = []
+ id_matrix = identity_matrix(field, n)
+ for i in xrange(n):
+ ei = id_matrix.column(i)
+ Qi = zero_matrix(field, n)
+ Qi.set_row(0, ei)
+ Qi.set_column(0, ei)
+ Qi += diagonal_matrix(n, [ei[0]]*n)
+ # The addition of the diagonal matrix adds an extra ei[0] in the
+ # upper-left corner of the matrix.
+ Qi[0,0] = Qi[0,0] * ~field(2)
+ Qs.append(Qi)
+
+ # The rank of the spin algebra is two, unless we're in a
+ # one-dimensional ambient space (because the rank is bounded by
+ # the ambient dimension).
+ fdeja = super(JordanSpinEJA, cls)
+ return fdeja.__classcall_private__(cls, field, Qs, rank=min(n,2))
+
+ def inner_product(self, x, y):
+ return _usual_ip(x,y)