specifically those where u^2 + v^2 = 0 implies that u = v = 0. They
are used in optimization, and have some additional nice methods beyond
what can be supported in a general Jordan Algebra.
-"""
+SETUP::
+
+ sage: from mjo.eja.eja_algebra import random_eja
+
+EXAMPLES::
+
+ sage: random_eja()
+ Euclidean Jordan algebra of dimension...
+
+"""
+
+from itertools import repeat
-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.quatalg.quaternion_algebra import QuaternionAlgebra
-from sage.categories.finite_dimensional_algebras_with_basis import FiniteDimensionalAlgebrasWithBasis
-from sage.functions.other import sqrt
+from sage.categories.magmatic_algebras import MagmaticAlgebras
+from sage.categories.sets_cat import cartesian_product
+from sage.combinat.free_module import (CombinatorialFreeModule,
+ CombinatorialFreeModule_CartesianProduct)
from sage.matrix.constructor import matrix
+from sage.matrix.matrix_space import MatrixSpace
from sage.misc.cachefunc import cached_method
-from sage.misc.prandom import choice
-from sage.modules.free_module import VectorSpace
-from sage.modules.free_module_element import vector
-from sage.rings.integer_ring import ZZ
-from sage.rings.number_field.number_field import QuadraticField
-from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
-from sage.rings.rational_field import QQ
-from sage.structure.element import is_Matrix
-from sage.structure.category_object import normalize_names
+from sage.misc.table import table
+from sage.modules.free_module import FreeModule, VectorSpace
+from sage.rings.all import (ZZ, QQ, AA, QQbar, RR, RLF, CLF,
+ PolynomialRing,
+ QuadraticField)
+from mjo.eja.eja_element import FiniteDimensionalEJAElement
+from mjo.eja.eja_operator import FiniteDimensionalEJAOperator
+from mjo.eja.eja_utils import _all2list, _mat2vec
+
+class FiniteDimensionalEJA(CombinatorialFreeModule):
+ r"""
+ A finite-dimensional Euclidean Jordan algebra.
+
+ INPUT:
+
+ - basis -- a tuple of basis elements in "matrix form," which
+ must be the same form as the arguments to ``jordan_product``
+ and ``inner_product``. In reality, "matrix form" can be either
+ vectors, matrices, or a Cartesian product (ordered tuple)
+ of vectors or matrices. All of these would ideally be vector
+ spaces in sage with no special-casing needed; but in reality
+ we turn vectors into column-matrices and Cartesian products
+ `(a,b)` into column matrices `(a,b)^{T}` after converting
+ `a` and `b` themselves.
+
+ - jordan_product -- function of two elements (in matrix form)
+ that returns their jordan product in this algebra; this will
+ be applied to ``basis`` to compute a multiplication table for
+ the algebra.
+
+ - inner_product -- function of two elements (in matrix form) that
+ returns their inner product. This will be applied to ``basis`` to
+ compute an inner-product table (basically a matrix) for this algebra.
+
+ """
+ Element = FiniteDimensionalEJAElement
-from mjo.eja.eja_operator import FiniteDimensionalEuclideanJordanAlgebraOperator
+ def __init__(self,
+ basis,
+ jordan_product,
+ inner_product,
+ field=AA,
+ orthonormalize=True,
+ associative=False,
+ cartesian_product=False,
+ check_field=True,
+ check_axioms=True,
+ prefix='e'):
+
+ # Keep track of whether or not the matrix basis consists of
+ # tuples, since we need special cases for them damned near
+ # everywhere. This is INDEPENDENT of whether or not the
+ # algebra is a cartesian product, since a subalgebra of a
+ # cartesian product will have a basis of tuples, but will not
+ # in general itself be a cartesian product algebra.
+ self._matrix_basis_is_cartesian = False
+ n = len(basis)
+ if n > 0:
+ if hasattr(basis[0], 'cartesian_factors'):
+ self._matrix_basis_is_cartesian = True
+
+ if check_field:
+ if not field.is_subring(RR):
+ # Note: this does return true for the real algebraic
+ # field, the rationals, and any quadratic field where
+ # we've specified a real embedding.
+ raise ValueError("scalar field is not real")
+
+ # If the basis given to us wasn't over the field that it's
+ # supposed to be over, fix that. Or, you know, crash.
+ if not cartesian_product:
+ # The field for a cartesian product algebra comes from one
+ # of its factors and is the same for all factors, so
+ # there's no need to "reapply" it on product algebras.
+ if self._matrix_basis_is_cartesian:
+ # OK since if n == 0, the basis does not consist of tuples.
+ P = basis[0].parent()
+ basis = tuple( P(tuple(b_i.change_ring(field) for b_i in b))
+ for b in basis )
+ else:
+ basis = tuple( b.change_ring(field) for b in basis )
+
+
+ if check_axioms:
+ # Check commutativity of the Jordan and inner-products.
+ # This has to be done before we build the multiplication
+ # and inner-product tables/matrices, because we take
+ # advantage of symmetry in the process.
+ if not all( jordan_product(bi,bj) == jordan_product(bj,bi)
+ for bi in basis
+ for bj in basis ):
+ raise ValueError("Jordan product is not commutative")
+
+ if not all( inner_product(bi,bj) == inner_product(bj,bi)
+ for bi in basis
+ for bj in basis ):
+ raise ValueError("inner-product is not commutative")
+
+
+ category = MagmaticAlgebras(field).FiniteDimensional()
+ category = category.WithBasis().Unital()
+ if associative:
+ # Element subalgebras can take advantage of this.
+ category = category.Associative()
+ if cartesian_product:
+ category = category.CartesianProducts()
+
+ # Call the superclass constructor so that we can use its from_vector()
+ # method to build our multiplication table.
+ CombinatorialFreeModule.__init__(self,
+ field,
+ range(n),
+ prefix=prefix,
+ category=category,
+ bracket=False)
+
+ # Now comes all of the hard work. We'll be constructing an
+ # ambient vector space V that our (vectorized) basis lives in,
+ # as well as a subspace W of V spanned by those (vectorized)
+ # basis elements. The W-coordinates are the coefficients that
+ # we see in things like x = 1*e1 + 2*e2.
+ vector_basis = basis
+
+ degree = 0
+ if n > 0:
+ degree = len(_all2list(basis[0]))
+
+ # Build an ambient space that fits our matrix basis when
+ # written out as "long vectors."
+ V = VectorSpace(field, degree)
+
+ # The matrix that will hole the orthonormal -> unorthonormal
+ # coordinate transformation.
+ self._deortho_matrix = None
+
+ if orthonormalize:
+ # Save a copy of the un-orthonormalized basis for later.
+ # Convert it to ambient V (vector) coordinates while we're
+ # at it, because we'd have to do it later anyway.
+ deortho_vector_basis = tuple( V(_all2list(b)) for b in basis )
+
+ from mjo.eja.eja_utils import gram_schmidt
+ basis = tuple(gram_schmidt(basis, inner_product))
+
+ # Save the (possibly orthonormalized) matrix basis for
+ # later...
+ self._matrix_basis = basis
+
+ # Now create the vector space for the algebra, which will have
+ # its own set of non-ambient coordinates (in terms of the
+ # supplied basis).
+ vector_basis = tuple( V(_all2list(b)) for b in basis )
+ W = V.span_of_basis( vector_basis, check=check_axioms)
+
+ if orthonormalize:
+ # Now "W" is the vector space of our algebra coordinates. The
+ # variables "X1", "X2",... refer to the entries of vectors in
+ # W. Thus to convert back and forth between the orthonormal
+ # coordinates and the given ones, we need to stick the original
+ # basis in W.
+ U = V.span_of_basis( deortho_vector_basis, check=check_axioms)
+ self._deortho_matrix = matrix( U.coordinate_vector(q)
+ for q in vector_basis )
+
+
+ # Now we actually compute the multiplication and inner-product
+ # tables/matrices using the possibly-orthonormalized basis.
+ self._inner_product_matrix = matrix.identity(field, n)
+ self._multiplication_table = [ [0 for j in range(i+1)]
+ for i in range(n) ]
+
+ # Note: the Jordan and inner-products are defined in terms
+ # of the ambient basis. It's important that their arguments
+ # are in ambient coordinates as well.
+ for i in range(n):
+ for j in range(i+1):
+ # ortho basis w.r.t. ambient coords
+ q_i = basis[i]
+ q_j = basis[j]
+
+ # The jordan product returns a matrixy answer, so we
+ # have to convert it to the algebra coordinates.
+ elt = jordan_product(q_i, q_j)
+ elt = W.coordinate_vector(V(_all2list(elt)))
+ self._multiplication_table[i][j] = self.from_vector(elt)
+
+ if not orthonormalize:
+ # If we're orthonormalizing the basis with respect
+ # to an inner-product, then the inner-product
+ # matrix with respect to the resulting basis is
+ # just going to be the identity.
+ ip = inner_product(q_i, q_j)
+ self._inner_product_matrix[i,j] = ip
+ self._inner_product_matrix[j,i] = ip
+
+ self._inner_product_matrix._cache = {'hermitian': True}
+ self._inner_product_matrix.set_immutable()
+
+ if check_axioms:
+ if not self._is_jordanian():
+ raise ValueError("Jordan identity does not hold")
+ if not self._inner_product_is_associative():
+ raise ValueError("inner product is not associative")
+
+
+ def _coerce_map_from_base_ring(self):
+ """
+ Disable the map from the base ring into the algebra.
+ Performing a nonsense conversion like this automatically
+ is counterpedagogical. The fallback is to try the usual
+ element constructor, which should also fail.
-class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra):
- @staticmethod
- def __classcall_private__(cls,
- field,
- mult_table,
- rank,
- names='e',
- assume_associative=False,
- category=None,
- natural_basis=None):
- n = len(mult_table)
- mult_table = [b.base_extend(field) for b in mult_table]
- for b in mult_table:
- b.set_immutable()
- if not (is_Matrix(b) and b.dimensions() == (n, n)):
- raise ValueError("input is not a multiplication table")
- mult_table = tuple(mult_table)
-
- cat = FiniteDimensionalAlgebrasWithBasis(field)
- cat.or_subcategory(category)
- if assume_associative:
- cat = cat.Associative()
-
- names = normalize_names(n, names)
-
- fda = super(FiniteDimensionalEuclideanJordanAlgebra, cls)
- return fda.__classcall__(cls,
- field,
- mult_table,
- rank=rank,
- assume_associative=assume_associative,
- names=names,
- category=cat,
- natural_basis=natural_basis)
+ SETUP::
+ sage: from mjo.eja.eja_algebra import random_eja
- def __init__(self,
- field,
- mult_table,
- rank,
- names='e',
- assume_associative=False,
- category=None,
- natural_basis=None):
+ TESTS::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: J(1)
+ Traceback (most recent call last):
+ ...
+ ValueError: not an element of this algebra
+
+ """
+ return None
+
+
+ def product_on_basis(self, i, j):
+ # We only stored the lower-triangular portion of the
+ # multiplication table.
+ if j <= i:
+ return self._multiplication_table[i][j]
+ else:
+ return self._multiplication_table[j][i]
+
+ 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.
+
SETUP::
- sage: from mjo.eja.eja_algebra import random_eja
+ sage: from mjo.eja.eja_algebra import (random_eja,
+ ....: HadamardEJA,
+ ....: BilinearFormEJA)
EXAMPLES:
- By definition, Jordan multiplication commutes::
+ Our inner product is "associative," which means the following for
+ a symmetric bilinear form::
sage: set_random_seed()
sage: J = random_eja()
+ sage: x,y,z = J.random_elements(3)
+ sage: (x*y).inner_product(z) == y.inner_product(x*z)
+ True
+
+ TESTS:
+
+ Ensure that this is the usual inner product for the algebras
+ over `R^n`::
+
+ sage: set_random_seed()
+ sage: J = HadamardEJA.random_instance()
+ sage: x,y = J.random_elements(2)
+ sage: actual = x.inner_product(y)
+ sage: expected = x.to_vector().inner_product(y.to_vector())
+ sage: actual == expected
+ True
+
+ Ensure that this is one-half of the trace inner-product in a
+ BilinearFormEJA that isn't just the reals (when ``n`` isn't
+ one). This is in Faraut and Koranyi, and also my "On the
+ symmetry..." paper::
+
+ sage: set_random_seed()
+ sage: J = BilinearFormEJA.random_instance()
+ sage: n = J.dimension()
sage: x = J.random_element()
sage: y = J.random_element()
- sage: x*y == y*x
+ sage: (n == 1) or (x.inner_product(y) == (x*y).trace()/2)
True
"""
- self._rank = rank
- self._natural_basis = natural_basis
- self._multiplication_table = mult_table
- fda = super(FiniteDimensionalEuclideanJordanAlgebra, self)
- fda.__init__(field,
- mult_table,
- names=names,
- category=category)
+ B = self._inner_product_matrix
+ return (B*x.to_vector()).inner_product(y.to_vector())
- def _repr_(self):
- """
- Return a string representation of ``self``.
+ def is_associative(self):
+ r"""
+ Return whether or not this algebra's Jordan product is associative.
SETUP::
- sage: from mjo.eja.eja_algebra import JordanSpinEJA
-
- TESTS:
+ sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
- Ensure that it says what we think it says::
+ EXAMPLES::
- sage: JordanSpinEJA(2, field=QQ)
- Euclidean Jordan algebra of degree 2 over Rational Field
- sage: JordanSpinEJA(3, field=RDF)
- Euclidean Jordan algebra of degree 3 over Real Double Field
+ sage: J = ComplexHermitianEJA(3, field=QQ, orthonormalize=False)
+ sage: J.is_associative()
+ False
+ sage: x = sum(J.gens())
+ sage: A = x.subalgebra_generated_by(orthonormalize=False)
+ sage: A.is_associative()
+ True
"""
- fmt = "Euclidean Jordan algebra of degree {} over {}"
- return fmt.format(self.degree(), self.base_ring())
-
+ return "Associative" in self.category().axioms()
+
+ def _is_jordanian(self):
+ r"""
+ Whether or not this algebra's multiplication table respects the
+ Jordan identity `(x^{2})(xy) = x(x^{2}y)`.
+
+ We only check one arrangement of `x` and `y`, so for a
+ ``True`` result to be truly true, you should also check
+ :meth:`is_commutative`. This method should of course always
+ return ``True``, unless this algebra was constructed with
+ ``check_axioms=False`` and passed an invalid multiplication table.
+ """
+ return all( (self.gens()[i]**2)*(self.gens()[i]*self.gens()[j])
+ ==
+ (self.gens()[i])*((self.gens()[i]**2)*self.gens()[j])
+ for i in range(self.dimension())
+ for j in range(self.dimension()) )
+
+ def _inner_product_is_associative(self):
+ r"""
+ Return whether or not this algebra's inner product `B` is
+ associative; that is, whether or not `B(xy,z) = B(x,yz)`.
+
+ This method should of course always return ``True``, unless
+ this algebra was constructed with ``check_axioms=False`` and
+ passed an invalid Jordan or inner-product.
+ """
- def _a_regular_element(self):
+ # Used to check whether or not something is zero in an inexact
+ # ring. This number is sufficient to allow the construction of
+ # QuaternionHermitianEJA(2, field=RDF) with check_axioms=True.
+ epsilon = 1e-16
+
+ for i in range(self.dimension()):
+ for j in range(self.dimension()):
+ for k in range(self.dimension()):
+ x = self.gens()[i]
+ y = self.gens()[j]
+ z = self.gens()[k]
+ diff = (x*y).inner_product(z) - x.inner_product(y*z)
+
+ if self.base_ring().is_exact():
+ if diff != 0:
+ return False
+ else:
+ if diff.abs() > epsilon:
+ return False
+
+ return True
+
+ def _element_constructor_(self, elt):
"""
- Guess a regular element. Needed to compute the basis for our
- characteristic polynomial coefficients.
+ Construct an element of this algebra from its vector or matrix
+ representation.
+
+ This gets called only after the parent element _call_ method
+ fails to find a coercion for the argument.
SETUP::
- sage: from mjo.eja.eja_algebra import random_eja
+ sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
+ ....: HadamardEJA,
+ ....: RealSymmetricEJA)
- TESTS:
+ EXAMPLES:
- Ensure that this hacky method succeeds for every algebra that we
- know how to construct::
+ The identity in `S^n` is converted to the identity in the EJA::
- sage: set_random_seed()
- sage: J = random_eja()
- sage: J._a_regular_element().is_regular()
+ sage: J = RealSymmetricEJA(3)
+ sage: I = matrix.identity(QQ,3)
+ sage: J(I) == J.one()
True
- """
- 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
+ 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):
+ ...
+ ValueError: not an element of this algebra
- @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 = self.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)
+ Tuples work as well, provided that the matrix basis for the
+ algebra consists of them::
+ sage: J1 = HadamardEJA(3)
+ sage: J2 = RealSymmetricEJA(2)
+ sage: J = cartesian_product([J1,J2])
+ sage: J( (J1.matrix_basis()[1], J2.matrix_basis()[2]) )
+ e(0, 1) + e(1, 2)
- @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)
+ TESTS:
+ Ensure that we can convert any element of the two non-matrix
+ simple algebras (whose matrix representations are columns)
+ back and forth faithfully::
+
+ sage: set_random_seed()
+ sage: J = HadamardEJA.random_instance()
+ sage: x = J.random_element()
+ sage: J(x.to_vector().column()) == x
+ True
+ sage: J = JordanSpinEJA.random_instance()
+ sage: x = J.random_element()
+ sage: J(x.to_vector().column()) == x
+ True
- @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.
+ msg = "not an element of this algebra"
+ if elt == 0:
+ # The superclass implementation of random_element()
+ # needs to be able to coerce "0" into the algebra.
+ return self.zero()
+ elif elt in self.base_ring():
+ # Ensure that no base ring -> algebra coercion is performed
+ # by this method. There's some stupidity in sage that would
+ # otherwise propagate to this method; for example, sage thinks
+ # that the integer 3 belongs to the space of 2-by-2 matrices.
+ raise ValueError(msg)
+
+ try:
+ elt = elt.column()
+ except (AttributeError, TypeError):
+ # Try to convert a vector into a column-matrix
+ pass
+
+ if elt not in self.matrix_space():
+ raise ValueError(msg)
+
+ # Thanks for nothing! Matrix spaces aren't vector spaces in
+ # Sage, so we have to figure out its matrix-basis coordinates
+ # ourselves. We use the basis space's ring instead of the
+ # element's ring because the basis space might be an algebraic
+ # closure whereas the base ring of the 3-by-3 identity matrix
+ # could be QQ instead of QQbar.
+ #
+ # And, we also have to handle Cartesian product bases (when
+ # the matric basis consists of tuples) here. The "good news"
+ # is that we're already converting everything to long vectors,
+ # and that strategy works for tuples as well.
+ #
+ # We pass check=False because the matrix basis is "guaranteed"
+ # to be linearly independent... right? Ha ha.
+ elt = _all2list(elt)
+ V = VectorSpace(self.base_ring(), len(elt))
+ W = V.span_of_basis( (V(_all2list(s)) for s in self.matrix_basis()),
+ check=False)
+
+ try:
+ coords = W.coordinate_vector(V(elt))
+ except ArithmeticError: # vector is not in free module
+ raise ValueError(msg)
+
+ return self.from_vector(coords)
+
+ def _repr_(self):
"""
- r = self.rank()
- n = self.dimension()
+ Return a string representation of ``self``.
- # 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)
+ SETUP::
- idmat = matrix.identity(J.base_ring(), n)
+ sage: from mjo.eja.eja_algebra import JordanSpinEJA
- W = self._charpoly_basis_space()
- W = W.change_ring(R.fraction_field())
+ TESTS:
- # 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(W(R.gens()))
- l1 = [matrix.column(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 = matrix.block(R, 1, n, (l1 + l2))
- xr = W.coordinates((x**r).vector())
- return (A_of_x, x, xr, A_of_x.det())
+ Ensure that it says what we think it says::
+
+ sage: JordanSpinEJA(2, field=AA)
+ Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
+ sage: JordanSpinEJA(3, field=RDF)
+ Euclidean Jordan algebra of dimension 3 over Real Double Field
+
+ """
+ fmt = "Euclidean Jordan algebra of dimension {} over {}"
+ return fmt.format(self.dimension(), self.base_ring())
@cached_method
- def characteristic_polynomial(self):
+ def characteristic_polynomial_of(self):
"""
- Return a characteristic polynomial that works for all elements
- of this algebra.
+ Return the algebra's "characteristic polynomial of" function,
+ which is itself a multivariate polynomial that, when evaluated
+ at the coordinates of some algebra element, returns that
+ element's characteristic polynomial.
The resulting polynomial has `n+1` variables, where `n` is the
dimension of this algebra. The first `n` variables correspond to
SETUP::
- sage: from mjo.eja.eja_algebra import JordanSpinEJA
+ sage: from mjo.eja.eja_algebra import JordanSpinEJA, TrivialEJA
EXAMPLES:
Alizadeh, Example 11.11::
sage: J = JordanSpinEJA(3)
- sage: p = J.characteristic_polynomial(); p
+ sage: p = J.characteristic_polynomial_of(); p
X1^2 - X2^2 - X3^2 + (-2*t)*X1 + t^2
- sage: xvec = J.one().vector()
+ sage: xvec = J.one().to_vector()
sage: p(*xvec)
t^2 - 2*t + 1
+ By definition, the characteristic polynomial is a monic
+ degree-zero polynomial in a rank-zero algebra. Note that
+ Cayley-Hamilton is indeed satisfied since the polynomial
+ ``1`` evaluates to the identity element of the algebra on
+ any argument::
+
+ sage: J = TrivialEJA()
+ sage: J.characteristic_polynomial_of()
+ 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) ]
+ # The list of coefficient polynomials a_0, a_1, a_2, ..., a_(r-1).
+ a = self._charpoly_coefficients()
# 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
+ if r > 0:
+ R = a[0].parent()
+ S = PolynomialRing(S, R.variable_names())
+ t = S(t)
+
+ return (t**r + sum( a[k]*(t**k) for k in range(r) ))
+
+ def coordinate_polynomial_ring(self):
+ r"""
+ The multivariate polynomial ring in which this algebra's
+ :meth:`characteristic_polynomial_of` lives.
- return sum( a[k]*(t**k) for k in range(len(a)) )
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (HadamardEJA,
+ ....: RealSymmetricEJA)
+
+ EXAMPLES::
+
+ sage: J = HadamardEJA(2)
+ sage: J.coordinate_polynomial_ring()
+ Multivariate Polynomial Ring in X1, X2...
+ sage: J = RealSymmetricEJA(3,field=QQ,orthonormalize=False)
+ sage: J.coordinate_polynomial_ring()
+ Multivariate Polynomial Ring in X1, X2, X3, X4, X5, X6...
+ """
+ var_names = tuple( "X%d" % z for z in range(1, self.dimension()+1) )
+ return PolynomialRing(self.base_ring(), var_names)
def inner_product(self, x, y):
"""
SETUP::
- sage: from mjo.eja.eja_algebra import random_eja
+ sage: from mjo.eja.eja_algebra import (random_eja,
+ ....: HadamardEJA,
+ ....: BilinearFormEJA)
EXAMPLES:
- The inner product must satisfy its axiom for this algebra to truly
- be a Euclidean Jordan Algebra::
+ Our inner product is "associative," which means the following for
+ a symmetric bilinear form::
sage: set_random_seed()
sage: J = random_eja()
+ sage: x,y,z = J.random_elements(3)
+ sage: (x*y).inner_product(z) == y.inner_product(x*z)
+ True
+
+ TESTS:
+
+ Ensure that this is the usual inner product for the algebras
+ over `R^n`::
+
+ sage: set_random_seed()
+ sage: J = HadamardEJA.random_instance()
+ sage: x,y = J.random_elements(2)
+ sage: actual = x.inner_product(y)
+ sage: expected = x.to_vector().inner_product(y.to_vector())
+ sage: actual == expected
+ True
+
+ Ensure that this is one-half of the trace inner-product in a
+ BilinearFormEJA that isn't just the reals (when ``n`` isn't
+ one). This is in Faraut and Koranyi, and also my "On the
+ symmetry..." paper::
+
+ sage: set_random_seed()
+ sage: J = BilinearFormEJA.random_instance()
+ sage: n = J.dimension()
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)
+ sage: (n == 1) or (x.inner_product(y) == (x*y).trace()/2)
+ True
+ """
+ B = self._inner_product_matrix
+ return (B*x.to_vector()).inner_product(y.to_vector())
+
+
+ def is_trivial(self):
+ """
+ Return whether or not this algebra is trivial.
+
+ A trivial algebra contains only the zero element.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
+ ....: TrivialEJA)
+
+ EXAMPLES::
+
+ sage: J = ComplexHermitianEJA(3)
+ sage: J.is_trivial()
+ False
+
+ ::
+
+ sage: J = TrivialEJA()
+ sage: J.is_trivial()
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)
+ return self.dimension() == 0
+
+
+ def multiplication_table(self):
+ """
+ Return a visual representation of this algebra's multiplication
+ table (on basis elements).
+
+ SETUP::
+ sage: from mjo.eja.eja_algebra import JordanSpinEJA
+
+ EXAMPLES::
+
+ sage: J = JordanSpinEJA(4)
+ sage: J.multiplication_table()
+ +----++----+----+----+----+
+ | * || e0 | e1 | e2 | e3 |
+ +====++====+====+====+====+
+ | e0 || e0 | e1 | e2 | e3 |
+ +----++----+----+----+----+
+ | e1 || e1 | e0 | 0 | 0 |
+ +----++----+----+----+----+
+ | e2 || e2 | 0 | e0 | 0 |
+ +----++----+----+----+----+
+ | e3 || e3 | 0 | 0 | e0 |
+ +----++----+----+----+----+
- def natural_basis(self):
"""
- Return a more-natural representation of this algebra's basis.
+ n = self.dimension()
+ # Prepend the header row.
+ M = [["*"] + list(self.gens())]
+
+ # And to each subsequent row, prepend an entry that belongs to
+ # the left-side "header column."
+ M += [ [self.gens()[i]] + [ self.product_on_basis(i,j)
+ for j in range(n) ]
+ for i in range(n) ]
- 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.)
+ return table(M, header_row=True, header_column=True, frame=True)
- Note that this will always return a matrix. The standard basis
- in `R^n` will be returned as `n`-by-`1` column matrices.
+
+ def matrix_basis(self):
+ """
+ Return an (often more natural) representation of this algebras
+ basis as an ordered tuple of matrices.
+
+ Every finite-dimensional Euclidean Jordan Algebra is a, up to
+ Jordan isomorphism, a direct sum of five simple
+ algebras---four of which comprise Hermitian matrices. And the
+ last type of algebra can of course be thought of as `n`-by-`1`
+ column matrices (ambiguusly called column vectors) to avoid
+ special cases. As a result, matrices (and column vectors) are
+ a natural representation format for Euclidean Jordan algebra
+ elements.
+
+ But, when we construct an algebra from a basis of matrices,
+ those matrix representations are lost in favor of coordinate
+ vectors *with respect to* that basis. We could eventually
+ convert back if we tried hard enough, but having the original
+ representations handy is valuable enough that we simply store
+ them and return them from this method.
+
+ Why implement this for non-matrix algebras? Avoiding special
+ cases for the :class:`BilinearFormEJA` pays with simplicity in
+ its own right. But mainly, we would like to be able to assume
+ that elements of a :class:`CartesianProductEJA` can be displayed
+ nicely, without having to have special classes for direct sums
+ one of whose components was a matrix algebra.
SETUP::
sage: J = RealSymmetricEJA(2)
sage: J.basis()
- Family (e0, e1, e2)
- sage: J.natural_basis()
+ Finite family {0: e0, 1: e1, 2: e2}
+ sage: J.matrix_basis()
(
- [1 0] [0 1] [0 0]
- [0 0], [1 0], [0 1]
+ [1 0] [ 0 0.7071067811865475?] [0 0]
+ [0 0], [0.7071067811865475? 0], [0 1]
)
::
sage: J = JordanSpinEJA(2)
sage: J.basis()
- Family (e0, e1)
- sage: J.natural_basis()
+ Finite family {0: e0, 1: e1}
+ sage: J.matrix_basis()
(
[1] [0]
[0], [1]
)
+ """
+ return self._matrix_basis
+
+ def matrix_space(self):
"""
- if self._natural_basis is None:
- return tuple( b.vector().column() for b in self.basis() )
- else:
- return self._natural_basis
+ Return the matrix space in which this algebra's elements live, if
+ we think of them as matrices (including column vectors of the
+ appropriate size).
+ Generally this will be an `n`-by-`1` column-vector space,
+ except when the algebra is trivial. There it's `n`-by-`n`
+ (where `n` is zero), to ensure that two elements of the matrix
+ space (empty matrices) can be multiplied.
- def rank(self):
+ Matrix algebras override this with something more useful.
"""
- Return the rank of this EJA.
+ if self.is_trivial():
+ return MatrixSpace(self.base_ring(), 0)
+ else:
+ return self.matrix_basis()[0].parent()
- ALGORITHM:
- The author knows of no algorithm to compute the rank of an EJA
- where only the multiplication table is known. In lieu of one, we
- require the rank to be specified when the algebra is created,
- and simply pass along that number here.
+ @cached_method
+ def one(self):
+ """
+ Return the unit element of this algebra.
SETUP::
- sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
- ....: RealSymmetricEJA,
- ....: ComplexHermitianEJA,
- ....: QuaternionHermitianEJA,
+ sage: from mjo.eja.eja_algebra import (HadamardEJA,
....: random_eja)
EXAMPLES:
- The rank of the Jordan spin algebra is always two::
+ We can compute unit element in the Hadamard EJA::
- sage: JordanSpinEJA(2).rank()
- 2
- sage: JordanSpinEJA(3).rank()
- 2
- sage: JordanSpinEJA(4).rank()
- 2
+ sage: J = HadamardEJA(5)
+ sage: J.one()
+ e0 + e1 + e2 + e3 + e4
- The rank of the `n`-by-`n` Hermitian real, complex, or
- quaternion matrices is `n`::
+ The unit element in the Hadamard EJA is inherited in the
+ subalgebras generated by its elements::
- sage: RealSymmetricEJA(2).rank()
- 2
- sage: ComplexHermitianEJA(2).rank()
- 2
- sage: QuaternionHermitianEJA(2).rank()
- 2
- sage: RealSymmetricEJA(5).rank()
- 5
- sage: ComplexHermitianEJA(5).rank()
- 5
- sage: QuaternionHermitianEJA(5).rank()
- 5
+ sage: J = HadamardEJA(5)
+ sage: J.one()
+ e0 + e1 + e2 + e3 + e4
+ sage: x = sum(J.gens())
+ sage: A = x.subalgebra_generated_by(orthonormalize=False)
+ sage: A.one()
+ f0
+ sage: A.one().superalgebra_element()
+ e0 + e1 + e2 + e3 + e4
TESTS:
- Ensure that every EJA that we know how to construct has a
- positive integer rank::
+ The identity element acts like the identity, regardless of
+ whether or not we orthonormalize::
sage: set_random_seed()
- sage: r = random_eja().rank()
- sage: r in ZZ and r > 0
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: J.one()*x == x and x*J.one() == x
+ True
+ sage: A = x.subalgebra_generated_by()
+ sage: y = A.random_element()
+ sage: A.one()*y == y and y*A.one() == y
True
- """
- return self._rank
+ ::
+ sage: set_random_seed()
+ sage: J = random_eja(field=QQ, orthonormalize=False)
+ sage: x = J.random_element()
+ sage: J.one()*x == x and x*J.one() == x
+ True
+ sage: A = x.subalgebra_generated_by(orthonormalize=False)
+ sage: y = A.random_element()
+ sage: A.one()*y == y and y*A.one() == y
+ True
- def vector_space(self):
- """
- Return the vector space that underlies this algebra.
+ The matrix of the unit element's operator is the identity,
+ regardless of the base field and whether or not we
+ orthonormalize::
- SETUP::
+ 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
+ sage: x = J.random_element()
+ sage: A = x.subalgebra_generated_by()
+ sage: actual = A.one().operator().matrix()
+ sage: expected = matrix.identity(A.base_ring(), A.dimension())
+ sage: actual == expected
+ True
- sage: from mjo.eja.eja_algebra import RealSymmetricEJA
+ ::
- EXAMPLES::
+ sage: set_random_seed()
+ sage: J = random_eja(field=QQ, orthonormalize=False)
+ sage: actual = J.one().operator().matrix()
+ sage: expected = matrix.identity(J.base_ring(), J.dimension())
+ sage: actual == expected
+ True
+ sage: x = J.random_element()
+ sage: A = x.subalgebra_generated_by(orthonormalize=False)
+ sage: actual = A.one().operator().matrix()
+ sage: expected = matrix.identity(A.base_ring(), A.dimension())
+ sage: actual == expected
+ True
- sage: J = RealSymmetricEJA(2)
- sage: J.vector_space()
- Vector space of dimension 3 over Rational Field
+ Ensure that the cached unit element (often precomputed by
+ hand) agrees with the computed one::
- """
- return self.zero().vector().parent().ambient_vector_space()
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: cached = J.one()
+ sage: J.one.clear_cache()
+ sage: J.one() == cached
+ True
+ ::
+
+ sage: set_random_seed()
+ sage: J = random_eja(field=QQ, orthonormalize=False)
+ sage: cached = J.one()
+ sage: J.one.clear_cache()
+ sage: J.one() == cached
+ True
- class Element(FiniteDimensionalAlgebraElement):
- """
- An element of a Euclidean Jordan algebra.
"""
+ # 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() ]
- 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__) )
+ # Now we use basic linear algebra to find the coefficients,
+ # of the matrices-as-vectors-linear-combination, which should
+ # work for the original algebra basis too.
+ A = matrix(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()) )
- def __init__(self, A, elt=None):
- """
+ # Now if there's an identity element in the algebra, this
+ # should work. We solve on the left to avoid having to
+ # transpose the matrix "A".
+ return self.from_vector(A.solve_left(b))
- SETUP::
- sage: from mjo.eja.eja_algebra import (RealSymmetricEJA,
- ....: random_eja)
+ def peirce_decomposition(self, c):
+ """
+ The Peirce decomposition of this algebra relative to the
+ idempotent ``c``.
- EXAMPLES:
+ In the future, this can be extended to a complete system of
+ orthogonal idempotents.
- The identity in `S^n` is converted to the identity in the EJA::
+ INPUT:
- sage: J = RealSymmetricEJA(3)
- sage: I = matrix.identity(QQ,3)
- sage: J(I) == J.one()
- True
+ - ``c`` -- an idempotent of this algebra.
- This skew-symmetric matrix can't be represented in the EJA::
+ OUTPUT:
- 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
+ A triple (J0, J5, J1) containing two subalgebras and one subspace
+ of this algebra,
- TESTS:
+ - ``J0`` -- the algebra on the eigenspace of ``c.operator()``
+ corresponding to the eigenvalue zero.
- Ensure that we can convert any element of the parent's
- underlying vector space back into an algebra element whose
- vector representation is what we started with::
+ - ``J5`` -- the eigenspace (NOT a subalgebra) of ``c.operator()``
+ corresponding to the eigenvalue one-half.
- sage: set_random_seed()
- sage: J = random_eja()
- sage: v = J.vector_space().random_element()
- sage: J(v).vector() == v
- True
+ - ``J1`` -- the algebra on the eigenspace of ``c.operator()``
+ corresponding to the eigenvalue one.
- """
- # 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):
- """
- Return ``self`` raised to the power ``n``.
-
- Jordan algebras are always power-associative; see for
- example Faraut and Koranyi, Proposition II.1.2 (ii).
-
- We have to override this because our superclass uses row
- vectors instead of column vectors! We, on the other hand,
- assume column vectors everywhere.
-
- SETUP::
-
- sage: from mjo.eja.eja_algebra import random_eja
-
- TESTS:
-
- The definition of `x^2` is the unambiguous `x*x`::
-
- sage: set_random_seed()
- sage: x = random_eja().random_element()
- sage: x*x == (x^2)
- 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()
- sage: Lxn = (x^n).operator()
- sage: Lxm*Lxn == Lxn*Lxm
- True
-
- """
- if n == 0:
- return self.parent().one()
- elif n == 1:
- return self
- else:
- return (self.operator()**(n-1))(self)
+ 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::
- def apply_univariate_polynomial(self, p):
- """
- Apply the univariate polynomial ``p`` to this element.
+ sage: from mjo.eja.eja_algebra import random_eja, RealSymmetricEJA
- 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:
- SETUP::
+ 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]
- sage: from mjo.eja.eja_algebra import (RealCartesianProductEJA,
- ....: random_eja)
+ TESTS:
- EXAMPLES::
+ Every algebra decomposes trivially with respect to its identity
+ element::
- 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
+ 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
- TESTS:
+ 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::
- We should always get back an element of the algebra::
+ 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
- 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 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)
- """
- 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)) )
+ return (J0, J5, J1)
- def characteristic_polynomial(self):
- """
- Return the characteristic polynomial of this element.
+ def random_element(self, thorough=False):
+ r"""
+ Return a random element of this algebra.
- SETUP::
+ 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.
- sage: from mjo.eja.eja_algebra import RealCartesianProductEJA
+ INPUT:
- EXAMPLES:
+ - ``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
- 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`::
+ """
+ # 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()
- sage: J = RealCartesianProductEJA(3)
- sage: J.one().characteristic_polynomial()
- t^3 - 3*t^2 + 3*t - 1
+ 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()
- Likewise, the characteristic of the zero element in the
- rank-three algebra `R^{n}` should be `t^{3}`::
+ return self.from_vector(V.coordinate_vector(v))
- sage: J = RealCartesianProductEJA(3)
- sage: J.zero().characteristic_polynomial()
- t^3
+ def random_elements(self, count, thorough=False):
+ """
+ Return ``count`` random elements as a tuple.
- TESTS:
+ INPUT:
- The characteristic polynomial of an element should evaluate
- to zero on that element::
+ - ``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
- sage: set_random_seed()
- sage: x = RealCartesianProductEJA(3).random_element()
- sage: p = x.characteristic_polynomial()
- sage: x.apply_univariate_polynomial(p)
- 0
+ SETUP::
- """
- p = self.parent().characteristic_polynomial()
- return p(*self.vector())
+ sage: from mjo.eja.eja_algebra import JordanSpinEJA
+ EXAMPLES::
- def inner_product(self, other):
- """
- Return the parent algebra's inner product of myself and ``other``.
+ 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
- SETUP::
+ """
+ return tuple( self.random_element(thorough)
+ for idx in range(count) )
- sage: from mjo.eja.eja_algebra import (
- ....: ComplexHermitianEJA,
- ....: JordanSpinEJA,
- ....: QuaternionHermitianEJA,
- ....: RealSymmetricEJA,
- ....: random_eja)
- EXAMPLES:
+ @cached_method
+ def _charpoly_coefficients(self):
+ r"""
+ The `r` polynomial coefficients of the "characteristic polynomial
+ of" function.
- 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`)::
+ SETUP::
- 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
+ sage: from mjo.eja.eja_algebra import random_eja
- 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
+ TESTS:
- 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``.
-
- SETUP::
-
- sage: from mjo.eja.eja_algebra import random_eja
-
- 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
-
- 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()
- sage: Ly = y.operator()
- sage: Lxx = (x*x).operator()
- sage: Lxy = (x*y).operator()
- 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()
- sage: Ly = y.operator()
- sage: Lz = z.operator()
- sage: Lzy = (z*y).operator()
- sage: Lxy = (x*y).operator()
- sage: Lxz = (x*z).operator()
- 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()
- sage: Ly = y.operator()
- sage: Lz = z.operator()
- sage: Lzy = (z*y).operator()
- sage: Luy = (u*y).operator()
- sage: Luz = (u*z).operator()
- sage: Luyz = (u*(y*z)).operator()
- sage: lhs = Lu*Lzy + Lz*Luy + Ly*Luz
- sage: rhs = Luyz + Ly*Lu*Lz + Lz*Lu*Ly
- sage: bool(lhs == rhs)
- True
-
- """
- if not other in self.parent():
- raise TypeError("'other' must live in the same algebra")
-
- A = self.operator()
- B = other.operator()
- return (A*B == B*A)
-
-
- def det(self):
- """
- Return my determinant, the product of my eigenvalues.
-
- SETUP::
-
- sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
- ....: random_eja)
-
- EXAMPLES::
-
- sage: J = JordanSpinEJA(2)
- sage: e0,e1 = J.gens()
- sage: x = sum( J.gens() )
- sage: x.det()
- 0
-
- ::
-
- sage: J = JordanSpinEJA(3)
- sage: e0,e1,e2 = J.gens()
- 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
-
- """
- 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:
+ The theory shows that these are all homogeneous polynomials of
+ a known degree::
- We appeal to the quadratic representation as in Koecher's
- Theorem 12 in Chapter III, Section 5.
-
- SETUP::
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: all(p.is_homogeneous() for p in J._charpoly_coefficients())
+ True
- sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
- ....: random_eja)
-
- EXAMPLES:
+ """
+ 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)
- The inverse in the spin factor algebra is given in Alizadeh's
- Example 11.11::
+ @cached_method
+ def rank(self):
+ r"""
+ Return the rank of this EJA.
- 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
+ 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.
- TESTS:
+ SETUP::
- The identity element is its own inverse::
+ sage: from mjo.eja.eja_algebra import (HadamardEJA,
+ ....: JordanSpinEJA,
+ ....: RealSymmetricEJA,
+ ....: ComplexHermitianEJA,
+ ....: QuaternionHermitianEJA,
+ ....: random_eja)
- sage: set_random_seed()
- sage: J = random_eja()
- sage: J.one().inverse() == J.one()
- True
+ EXAMPLES:
- If an element has an inverse, it acts like one::
+ The rank of the Jordan spin algebra is always two::
- 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
+ sage: JordanSpinEJA(2).rank()
+ 2
+ sage: JordanSpinEJA(3).rank()
+ 2
+ sage: JordanSpinEJA(4).rank()
+ 2
- The inverse of the inverse is what we started with::
+ The rank of the `n`-by-`n` Hermitian real, complex, or
+ quaternion matrices is `n`::
- sage: set_random_seed()
- sage: J = random_eja()
- sage: x = J.random_element()
- sage: (not x.is_invertible()) or (x.inverse().inverse() == x)
- True
+ sage: RealSymmetricEJA(4).rank()
+ 4
+ sage: ComplexHermitianEJA(3).rank()
+ 3
+ sage: QuaternionHermitianEJA(2).rank()
+ 2
- The zero element is never invertible::
+ TESTS:
- sage: set_random_seed()
- sage: J = random_eja().zero().inverse()
- Traceback (most recent call last):
- ...
- ValueError: element is not invertible
+ Ensure that every EJA that we know how to construct has a
+ positive integer rank, unless the algebra is trivial in
+ which case its rank will be zero::
- """
- if not self.is_invertible():
- raise ValueError("element is not invertible")
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: r = J.rank()
+ sage: r in ZZ
+ True
+ sage: r > 0 or (r == 0 and J.is_trivial())
+ True
- return (~self.quadratic_representation())(self)
+ Ensure that computing the rank actually works, since the ranks
+ of all simple algebras are known and will be cached by default::
+ sage: set_random_seed() # long time
+ sage: J = random_eja() # long time
+ sage: cached = J.rank() # long time
+ sage: J.rank.clear_cache() # long time
+ sage: J.rank() == cached # long time
+ True
- def is_invertible(self):
- """
- Return whether or not this element is invertible.
+ """
+ return len(self._charpoly_coefficients())
- 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.
+ def subalgebra(self, basis, **kwargs):
+ r"""
+ Create a subalgebra of this algebra from the given basis.
+ """
+ from mjo.eja.eja_subalgebra import FiniteDimensionalEJASubalgebra
+ return FiniteDimensionalEJASubalgebra(self, basis, **kwargs)
- Beware that we can't use the superclass method, because it
- relies on the algebra being associative.
- SETUP::
+ def vector_space(self):
+ """
+ Return the vector space that underlies this algebra.
- sage: from mjo.eja.eja_algebra import random_eja
+ SETUP::
- TESTS:
+ sage: from mjo.eja.eja_algebra import RealSymmetricEJA
- The identity element is always invertible::
+ EXAMPLES::
- sage: set_random_seed()
- sage: J = random_eja()
- sage: J.one().is_invertible()
- True
+ sage: J = RealSymmetricEJA(2)
+ sage: J.vector_space()
+ Vector space of dimension 3 over...
- The zero element is never invertible::
+ """
+ return self.zero().to_vector().parent().ambient_vector_space()
- 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)
+class RationalBasisEJA(FiniteDimensionalEJA):
+ r"""
+ New class for algebras whose supplied basis elements have all rational entries.
- def is_nilpotent(self):
- """
- Return whether or not some power of this element is zero.
+ SETUP::
- ALGORITHM:
+ sage: from mjo.eja.eja_algebra import BilinearFormEJA
- We use Theorem 5 in Chapter III of Koecher, which says that
- an element ``x`` is nilpotent if and only if ``x.operator()``
- is nilpotent. And it is a basic fact of linear algebra that
- an operator on an `n`-dimensional space is nilpotent if and
- only if, when raised to the `n`th power, it equals the zero
- operator (for example, see Axler Corollary 8.8).
+ EXAMPLES:
- SETUP::
+ The supplied basis is orthonormalized by default::
- sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
- ....: random_eja)
+ sage: B = matrix(QQ, [[1, 0, 0], [0, 25, -32], [0, -32, 41]])
+ sage: J = BilinearFormEJA(B)
+ sage: J.matrix_basis()
+ (
+ [1] [ 0] [ 0]
+ [0] [1/5] [32/5]
+ [0], [ 0], [ 5]
+ )
- EXAMPLES::
+ """
+ def __init__(self,
+ basis,
+ jordan_product,
+ inner_product,
+ field=AA,
+ check_field=True,
+ **kwargs):
+
+ if check_field:
+ # Abuse the check_field parameter to check that the entries of
+ # out basis (in ambient coordinates) are in the field QQ.
+ if not all( all(b_i in QQ for b_i in b.list()) for b in basis ):
+ raise TypeError("basis not rational")
+
+ self._rational_algebra = None
+ if field is not QQ:
+ # There's no point in constructing the extra algebra if this
+ # one is already rational.
+ #
+ # Note: the same Jordan and inner-products work here,
+ # because they are necessarily defined with respect to
+ # ambient coordinates and not any particular basis.
+ self._rational_algebra = FiniteDimensionalEJA(
+ basis,
+ jordan_product,
+ inner_product,
+ field=QQ,
+ orthonormalize=False,
+ check_field=False,
+ check_axioms=False)
+
+ super().__init__(basis,
+ jordan_product,
+ inner_product,
+ field=field,
+ check_field=check_field,
+ **kwargs)
- sage: J = JordanSpinEJA(3)
- sage: x = sum(J.gens())
- sage: x.is_nilpotent()
- False
+ @cached_method
+ def _charpoly_coefficients(self):
+ r"""
+ SETUP::
- TESTS:
+ sage: from mjo.eja.eja_algebra import (BilinearFormEJA,
+ ....: JordanSpinEJA)
- The identity element is never nilpotent::
+ EXAMPLES:
- sage: set_random_seed()
- sage: random_eja().one().is_nilpotent()
- False
+ 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)::
- The additive identity is always nilpotent::
+ 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
- sage: set_random_seed()
- sage: random_eja().zero().is_nilpotent()
- True
+ """
+ if self._rational_algebra is None:
+ # There's no need to construct *another* algebra over the
+ # rationals if this one is already over the
+ # rationals. Likewise, if we never orthonormalized our
+ # basis, we might as well just use the given one.
+ return super()._charpoly_coefficients()
+
+ # Do the computation over the rationals. The answer will be
+ # the same, because all we've done is a change of basis.
+ # Then, change back from QQ to our real base ring
+ a = ( a_i.change_ring(self.base_ring())
+ for a_i in self._rational_algebra._charpoly_coefficients() )
+
+ if self._deortho_matrix is None:
+ # This can happen if our base ring was, say, AA and we
+ # chose not to (or didn't need to) orthonormalize. It's
+ # still faster to do the computations over QQ even if
+ # the numbers in the boxes stay the same.
+ return tuple(a)
+
+ # Otherwise, convert the coordinate variables back to the
+ # deorthonormalized ones.
+ R = self.coordinate_polynomial_ring()
+ from sage.modules.free_module_element import vector
+ X = vector(R, R.gens())
+ BX = self._deortho_matrix*X
+
+ subs_dict = { X[i]: BX[i] for i in range(len(X)) }
+ return tuple( a_i.subs(subs_dict) for a_i in a )
+
+class ConcreteEJA(RationalBasisEJA):
+ r"""
+ A class for the Euclidean Jordan algebras that we know by name.
+
+ These are the Jordan algebras whose basis, multiplication table,
+ rank, and so on are known a priori. More to the point, they are
+ the Euclidean Jordan algebras for which we are able to conjure up
+ a "random instance."
- """
- P = self.parent()
- zero_operator = P.zero().operator()
- return self.operator()**P.dimension() == zero_operator
+ SETUP::
+ sage: from mjo.eja.eja_algebra import ConcreteEJA
- def is_regular(self):
- """
- Return whether or not this is a regular element.
+ TESTS:
- SETUP::
+ Our basis is normalized with respect to the algebra's inner
+ product, unless we specify otherwise::
- sage: from mjo.eja.eja_algebra import JordanSpinEJA
+ sage: set_random_seed()
+ sage: J = ConcreteEJA.random_instance()
+ sage: all( b.norm() == 1 for b in J.gens() )
+ True
- EXAMPLES:
+ Since our basis is orthonormal with respect to the algebra's inner
+ product, and since we know that this algebra is an EJA, any
+ left-multiplication operator's matrix will be symmetric because
+ natural->EJA basis representation is an isometry and within the
+ EJA the operator is self-adjoint by the Jordan axiom::
- The identity element always has degree one, but any element
- linearly-independent from it is regular::
+ sage: set_random_seed()
+ sage: J = ConcreteEJA.random_instance()
+ sage: x = J.random_element()
+ sage: x.operator().is_self_adjoint()
+ True
+ """
- sage: J = JordanSpinEJA(5)
- sage: J.one().is_regular()
- False
- sage: e0, e1, e2, e3, e4 = J.gens() # e0 is the identity
- sage: for x in J.gens():
- ....: (J.one() + x).is_regular()
- False
- True
- True
- True
- True
+ @staticmethod
+ def _max_random_instance_size():
+ """
+ Return an integer "size" that is an upper bound on the size of
+ this algebra when it is used in a random test
+ case. Unfortunately, the term "size" is ambiguous -- when
+ dealing with `R^n` under either the Hadamard or Jordan spin
+ product, the "size" refers to the dimension `n`. When dealing
+ with a matrix algebra (real symmetric or complex/quaternion
+ Hermitian), it refers to the size of the matrix, which is far
+ less than the dimension of the underlying vector space.
+
+ This method must be implemented in each subclass.
+ """
+ raise NotImplementedError
- """
- return self.degree() == self.parent().rank()
+ @classmethod
+ def random_instance(cls, *args, **kwargs):
+ """
+ Return a random instance of this type of algebra.
+ This method should be implemented in each subclass.
+ """
+ from sage.misc.prandom import choice
+ eja_class = choice(cls.__subclasses__())
- def degree(self):
- """
- Compute the degree of this element the straightforward way
- according to the definition; by appending powers to a list
- and figuring out its dimension (that is, whether or not
- they're linearly dependent).
+ # These all bubble up to the RationalBasisEJA superclass
+ # constructor, so any (kw)args valid there are also valid
+ # here.
+ return eja_class.random_instance(*args, **kwargs)
- SETUP::
- sage: from mjo.eja.eja_algebra import JordanSpinEJA
+class MatrixEJA:
+ @staticmethod
+ def dimension_over_reals():
+ r"""
+ The dimension of this matrix's base ring over the reals.
- EXAMPLES::
+ The reals are dimension one over themselves, obviously; that's
+ just `\mathbb{R}^{1}`. Likewise, the complex numbers `a + bi`
+ have dimension two. Finally, the quaternions have dimension
+ four over the reals.
- sage: J = JordanSpinEJA(4)
- sage: J.one().degree()
- 1
- sage: e0,e1,e2,e3 = J.gens()
- sage: (e0 - e1).degree()
- 2
+ This is used to determine the size of the matrix returned from
+ :meth:`real_embed`, among other things.
+ """
+ raise NotImplementedError
- In the spin factor algebra (of rank two), all elements that
- aren't multiples of the identity are regular::
+ @classmethod
+ def real_embed(cls,M):
+ """
+ Embed the matrix ``M`` into a space of real matrices.
- sage: set_random_seed()
- 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
+ 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.
- """
- return self.span_of_powers().dimension()
+ real_embed(M*N) = real_embed(M)*real_embed(N)
+ """
+ if M.ncols() != M.nrows():
+ raise ValueError("the matrix 'M' must be square")
+ return M
- 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
+ @classmethod
+ def real_unembed(cls,M):
+ """
+ The inverse of :meth:`real_embed`.
+ """
+ if M.ncols() != M.nrows():
+ raise ValueError("the matrix 'M' must be square")
+ if not ZZ(M.nrows()).mod(cls.dimension_over_reals()).is_zero():
+ raise ValueError("the matrix 'M' must be a real embedding")
+ return M
+ @staticmethod
+ def jordan_product(X,Y):
+ return (X*Y + Y*X)/2
- def minimal_polynomial(self):
- """
- Return the minimal polynomial of this element,
- as a function of the variable `t`.
+ @classmethod
+ def trace_inner_product(cls,X,Y):
+ r"""
+ Compute the trace inner-product of two real-embeddings.
- ALGORITHM:
+ SETUP::
- 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.
-
- SETUP::
-
- sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
- ....: random_eja)
-
- 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()))
- return elt.operator().minimal_polynomial()
-
-
-
- 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.
-
- SETUP::
-
- sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
- ....: QuaternionHermitianEJA)
-
- 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.
-
- SETUP::
-
- sage: from mjo.eja.eja_algebra import random_eja
-
- 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()
- fda_elt = FiniteDimensionalAlgebraElement(P, self)
- return FiniteDimensionalEuclideanJordanAlgebraOperator(
- P,
- P,
- fda_elt.matrix().transpose() )
-
-
- def quadratic_representation(self, other=None):
- """
- Return the quadratic representation of this element.
-
- SETUP::
-
- sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
- ....: random_eja)
-
- 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 = matrix.identity(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 = matrix.block(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 (multiply on the right for :trac:`28272`):
-
- sage: alpha = QQ.random_element()
- sage: (alpha*x).quadratic_representation() == Qx*(alpha^2)
- True
-
- Property 3:
-
- sage: not x.is_invertible() or ( Qx(x.inverse()) == x )
- True
+ sage: from mjo.eja.eja_algebra import (RealSymmetricEJA,
+ ....: ComplexHermitianEJA,
+ ....: QuaternionHermitianEJA)
- sage: not x.is_invertible() or (
- ....: ~Qx
- ....: ==
- ....: x.inverse().quadratic_representation() )
- True
+ EXAMPLES::
- sage: Qxy(J.one()) == x*y
- True
+ This gives the same answer as it would if we computed the trace
+ from the unembedded (original) matrices::
- Property 4:
-
- sage: not x.is_invertible() or (
- ....: x.quadratic_representation(x.inverse())*Qx
- ....: == Qx*x.quadratic_representation(x.inverse()) )
- True
+ sage: set_random_seed()
+ sage: J = RealSymmetricEJA.random_instance()
+ sage: x,y = J.random_elements(2)
+ sage: Xe = x.to_matrix()
+ sage: Ye = y.to_matrix()
+ sage: X = J.real_unembed(Xe)
+ sage: Y = J.real_unembed(Ye)
+ sage: expected = (X*Y).trace()
+ sage: actual = J.trace_inner_product(Xe,Ye)
+ sage: actual == expected
+ 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:
-
- sage: Qy(x).quadratic_representation() == Qy*Qx*Qy
- True
-
- Property 6:
+ ::
- sage: Qxn == (Qx)^n
- True
+ sage: set_random_seed()
+ sage: J = ComplexHermitianEJA.random_instance()
+ sage: x,y = J.random_elements(2)
+ sage: Xe = x.to_matrix()
+ sage: Ye = y.to_matrix()
+ sage: X = J.real_unembed(Xe)
+ sage: Y = J.real_unembed(Ye)
+ sage: expected = (X*Y).trace().real()
+ sage: actual = J.trace_inner_product(Xe,Ye)
+ sage: actual == expected
+ True
- Property 7:
+ ::
- sage: not x.is_invertible() or (
- ....: Qx*x.inverse().operator() == Lx )
- True
+ sage: set_random_seed()
+ sage: J = QuaternionHermitianEJA.random_instance()
+ sage: x,y = J.random_elements(2)
+ sage: Xe = x.to_matrix()
+ sage: Ye = y.to_matrix()
+ sage: X = J.real_unembed(Xe)
+ sage: Y = J.real_unembed(Ye)
+ sage: expected = (X*Y).trace().coefficient_tuple()[0]
+ sage: actual = J.trace_inner_product(Xe,Ye)
+ sage: actual == expected
+ True
- Property 8:
-
- sage: not x.operator_commutes_with(y) or (
- ....: Qx(y)^n == Qxn(y^n) )
- True
-
- """
- if other is None:
- other=self
- elif not other in self.parent():
- raise TypeError("'other' must live in the same algebra")
-
- L = self.operator()
- M = other.operator()
- return ( L*M + M*L - (self*other).operator() )
-
-
- def span_of_powers(self):
- """
- Return the vector space spanned by successive powers of
- this element.
- """
- # 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.
- #
- # We do the extra ambient_vector_space() in case we're messing
- # with polynomials and the direct parent is a module.
- V = self.parent().vector_space()
- return V.span( (self**d).vector() for d in xrange(V.dimension()) )
-
-
- def subalgebra_generated_by(self):
- """
- Return the associative subalgebra of the parent EJA generated
- by this element.
-
- SETUP::
-
- sage: from mjo.eja.eja_algebra import random_eja
-
- TESTS::
-
- sage: set_random_seed()
- sage: x = random_eja().random_element()
- sage: x.subalgebra_generated_by().is_associative()
- True
-
- Squaring in the subalgebra should work the same as in
- the superalgebra::
-
- sage: set_random_seed()
- sage: x = random_eja().random_element()
- sage: u = x.subalgebra_generated_by().random_element()
- sage: u.operator()(u) == u^2
- True
-
- """
- # First get the subspace spanned by the powers of myself...
- V = self.span_of_powers()
- F = self.base_ring()
-
- # Now figure out the entries of the right-multiplication
- # matrix for the successive basis elements b0, b1,... of
- # that subspace.
- mats = []
- for b_right in V.basis():
- eja_b_right = self.parent()(b_right)
- b_right_rows = []
- # The first row of the right-multiplication matrix by
- # b1 is what we get if we apply that matrix to b1. The
- # second row of the right multiplication matrix by b1
- # is what we get when we apply that matrix to b2...
- #
- # IMPORTANT: this assumes that all vectors are COLUMN
- # vectors, unlike our superclass (which uses row vectors).
- for b_left in V.basis():
- eja_b_left = self.parent()(b_left)
- # Multiply in the original EJA, but then get the
- # coordinates from the subalgebra in terms of its
- # basis.
- this_row = V.coordinates((eja_b_left*eja_b_right).vector())
- b_right_rows.append(this_row)
- b_right_matrix = matrix(F, b_right_rows)
- mats.append(b_right_matrix)
-
- # It's an algebra of polynomials in one element, and EJAs
- # are power-associative.
- #
- # TODO: choose generator names intelligently.
- #
- # The rank is the highest possible degree of a minimal polynomial,
- # and is bounded above by the dimension. We know in this case that
- # there's an element whose minimal polynomial has the same degree
- # as the space's dimension, so that must be its rank too.
- return FiniteDimensionalEuclideanJordanAlgebra(
- F,
- mats,
- V.dimension(),
- assume_associative=True,
- names='f')
-
-
- def subalgebra_idempotent(self):
- """
- Find an idempotent in the associative subalgebra I generate
- using Proposition 2.3.5 in Baes.
-
- SETUP::
-
- sage: from mjo.eja.eja_algebra import random_eja
-
- TESTS::
-
- sage: set_random_seed()
- 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
-
- """
- if self.is_nilpotent():
- raise ValueError("this only works with non-nilpotent elements!")
-
- V = self.span_of_powers()
- J = 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!
- u = J(V.coordinates(self.vector()))
-
- # The image of the matrix of left-u^m-multiplication
- # will be minimal for some natural number s...
- s = 0
- minimal_dim = V.dimension()
- for i in xrange(1, V.dimension()):
- this_dim = (u**i).operator().matrix().image().dimension()
- if this_dim < minimal_dim:
- minimal_dim = this_dim
- s = i
-
- # Now minimal_matrix should correspond to the smallest
- # non-zero subspace in Baes's (or really, Koecher's)
- # proposition.
- #
- # However, we need to restrict the matrix to work on the
- # subspace... or do we? Can't we just solve, knowing that
- # A(c) = u^(s+1) should have a solution in the big space,
- # too?
- #
- # Beware, solve_right() means that we're using COLUMN vectors.
- # Our FiniteDimensionalAlgebraElement superclass uses rows.
- u_next = u**(s+1)
- 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
- # the coordinate system of the subalgebra.
- #
- # We need the basis for J, but as elements of the parent algebra.
- #
- basis = [self.parent(v) for v in V.basis()]
- return self.parent().linear_combination(zip(c_coordinates, basis))
+ """
+ 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]
- def trace(self):
- """
- Return my trace, the sum of my eigenvalues.
- SETUP::
+class RealMatrixEJA(MatrixEJA):
+ @staticmethod
+ def dimension_over_reals():
+ return 1
- sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
- ....: RealCartesianProductEJA,
- ....: random_eja)
- EXAMPLES::
+class RealSymmetricEJA(ConcreteEJA, RealMatrixEJA):
+ """
+ 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.
- sage: J = JordanSpinEJA(3)
- sage: x = sum(J.gens())
- sage: x.trace()
- 2
+ SETUP::
- ::
+ sage: from mjo.eja.eja_algebra import RealSymmetricEJA
- sage: J = RealCartesianProductEJA(5)
- sage: J.one().trace()
- 5
+ EXAMPLES::
- TESTS:
+ sage: J = RealSymmetricEJA(2)
+ sage: e0, e1, e2 = J.gens()
+ sage: e0*e0
+ e0
+ sage: e1*e1
+ 1/2*e0 + 1/2*e2
+ sage: e2*e2
+ e2
- The trace of an element is a real number::
+ In theory, our "field" can be any subfield of the reals::
- sage: set_random_seed()
- sage: J = random_eja()
- sage: J.random_element().trace() in J.base_ring()
- True
+ sage: RealSymmetricEJA(2, field=RDF)
+ Euclidean Jordan algebra of dimension 3 over Real Double Field
+ sage: RealSymmetricEJA(2, field=RR)
+ Euclidean Jordan algebra of dimension 3 over Real Field with
+ 53 bits of precision
- """
- 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())
+ TESTS:
+ The dimension of this algebra is `(n^2 + n) / 2`::
- def trace_inner_product(self, other):
- """
- Return the trace inner product of myself and ``other``.
+ sage: set_random_seed()
+ sage: n_max = RealSymmetricEJA._max_random_instance_size()
+ sage: n = ZZ.random_element(1, n_max)
+ sage: J = RealSymmetricEJA(n)
+ sage: J.dimension() == (n^2 + n)/2
+ True
- SETUP::
+ The Jordan multiplication is what we think it is::
- sage: from mjo.eja.eja_algebra import random_eja
+ sage: set_random_seed()
+ sage: J = RealSymmetricEJA.random_instance()
+ sage: x,y = J.random_elements(2)
+ sage: actual = (x*y).to_matrix()
+ sage: X = x.to_matrix()
+ sage: Y = y.to_matrix()
+ sage: expected = (X*Y + Y*X)/2
+ sage: actual == expected
+ True
+ sage: J(expected) == x*y
+ True
- TESTS:
+ We can change the generator prefix::
- The trace inner product is commutative::
+ sage: RealSymmetricEJA(3, prefix='q').gens()
+ (q0, q1, q2, q3, q4, q5)
- 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
+ We can construct the (trivial) algebra of rank zero::
- The trace inner product is bilinear::
+ sage: RealSymmetricEJA(0)
+ Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
- 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
+ """
+ @classmethod
+ def _denormalized_basis(cls, n):
+ """
+ Return a basis for the space of real symmetric n-by-n matrices.
- The trace inner product satisfies the compatibility
- condition in the definition of a Euclidean Jordan algebra::
+ SETUP::
- 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
+ sage: from mjo.eja.eja_algebra import RealSymmetricEJA
- """
- if not other in self.parent():
- raise TypeError("'other' must live in the same algebra")
+ TESTS::
- return (self*other).trace()
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,5)
+ sage: B = RealSymmetricEJA._denormalized_basis(n)
+ sage: all( M.is_symmetric() for M in B)
+ True
+ """
+ # The basis of symmetric matrices, as matrices, in their R^(n-by-n)
+ # coordinates.
+ S = []
+ for i in range(n):
+ for j in range(i+1):
+ Eij = matrix(ZZ, n, lambda k,l: k==i and l==j)
+ if i == j:
+ Sij = Eij
+ else:
+ Sij = Eij + Eij.transpose()
+ S.append(Sij)
+ return tuple(S)
-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.
+ @staticmethod
+ def _max_random_instance_size():
+ return 4 # Dimension 10
- SETUP::
+ @classmethod
+ def random_instance(cls, **kwargs):
+ """
+ Return a random instance of this type of algebra.
+ """
+ n = ZZ.random_element(cls._max_random_instance_size() + 1)
+ return cls(n, **kwargs)
+
+ def __init__(self, n, **kwargs):
+ # We know this is a valid EJA, but will double-check
+ # if the user passes check_axioms=True.
+ if "check_axioms" not in kwargs: kwargs["check_axioms"] = False
+
+ super(RealSymmetricEJA, self).__init__(self._denormalized_basis(n),
+ self.jordan_product,
+ self.trace_inner_product,
+ **kwargs)
+
+ # TODO: this could be factored out somehow, but is left here
+ # because the MatrixEJA is not presently a subclass of the
+ # FDEJA class that defines rank() and one().
+ self.rank.set_cache(n)
+ idV = matrix.identity(ZZ, self.dimension_over_reals()*n)
+ self.one.set_cache(self(idV))
+
+
+
+class ComplexMatrixEJA(MatrixEJA):
+ # A manual dictionary-cache for the complex_extension() method,
+ # since apparently @classmethods can't also be @cached_methods.
+ _complex_extension = {}
+
+ @classmethod
+ def complex_extension(cls,field):
+ r"""
+ The complex field that we embed/unembed, as an extension
+ of the given ``field``.
+ """
+ if field in cls._complex_extension:
+ return cls._complex_extension[field]
+
+ # Sage doesn't know how to adjoin the complex "i" (the root of
+ # x^2 + 1) to a field in a general way. Here, we just enumerate
+ # all of the cases that I have cared to support so far.
+ if field is AA:
+ # Sage doesn't know how to embed AA into QQbar, i.e. how
+ # to adjoin sqrt(-1) to AA.
+ F = QQbar
+ elif not field.is_exact():
+ # RDF or RR
+ F = field.complex_field()
+ else:
+ # Works for QQ and... maybe some other fields.
+ R = PolynomialRing(field, 'z')
+ z = R.gen()
+ F = field.extension(z**2 + 1, 'I', embedding=CLF(-1).sqrt())
- sage: from mjo.eja.eja_algebra import RealCartesianProductEJA
+ cls._complex_extension[field] = F
+ return F
- EXAMPLES:
+ @staticmethod
+ def dimension_over_reals():
+ return 2
- This multiplication table can be verified by hand::
+ @classmethod
+ def real_embed(cls,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]]``.
- sage: J = RealCartesianProductEJA(3)
- sage: e0,e1,e2 = J.gens()
- sage: e0*e0
- e0
- sage: e0*e1
- 0
- sage: e0*e2
- 0
- sage: e1*e1
- e1
- sage: e1*e2
- 0
- sage: e2*e2
- e2
+ SETUP::
- """
- @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) ]
-
- fdeja = super(RealCartesianProductEJA, cls)
- return fdeja.__classcall_private__(cls, field, Qs, rank=n)
+ sage: from mjo.eja.eja_algebra import ComplexMatrixEJA
- def inner_product(self, x, y):
- return _usual_ip(x,y)
+ 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: ComplexMatrixEJA.real_embed(M)
+ [ 4 -2| 1 2]
+ [ 2 4|-2 1]
+ [-----+-----]
+ [ 0 -1| 6 0]
+ [ 1 0| 0 6]
-def random_eja():
- """
- Return a "random" finite-dimensional Euclidean Jordan Algebra.
+ TESTS:
+
+ Embedding is a homomorphism (isomorphism, in fact)::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(3)
+ sage: F = QuadraticField(-1, 'I')
+ sage: X = random_matrix(F, n)
+ sage: Y = random_matrix(F, n)
+ sage: Xe = ComplexMatrixEJA.real_embed(X)
+ sage: Ye = ComplexMatrixEJA.real_embed(Y)
+ sage: XYe = ComplexMatrixEJA.real_embed(X*Y)
+ sage: Xe*Ye == XYe
+ True
+
+ """
+ super(ComplexMatrixEJA,cls).real_embed(M)
+ n = M.nrows()
- ALGORITHM:
+ # We don't need any adjoined elements...
+ field = M.base_ring().base_ring()
- For now, we choose a random natural number ``n`` (greater than zero)
- and then give you back one of the following:
+ blocks = []
+ for z in M.list():
+ a = z.real()
+ b = z.imag()
+ blocks.append(matrix(field, 2, [ [ a, b],
+ [-b, a] ]))
- * The cartesian product of the rational numbers ``n`` times; this is
- ``QQ^n`` with the Hadamard product.
+ return matrix.block(field, n, blocks)
- * The Jordan spin algebra on ``QQ^n``.
- * The ``n``-by-``n`` rational symmetric matrices with the symmetric
- product.
+ @classmethod
+ def real_unembed(cls,M):
+ """
+ The inverse of _embed_complex_matrix().
- * The ``n``-by-``n`` complex-rational Hermitian matrices embedded
- in the space of ``2n``-by-``2n`` real symmetric matrices.
+ SETUP::
- * The ``n``-by-``n`` quaternion-rational Hermitian matrices embedded
- in the space of ``4n``-by-``4n`` real symmetric matrices.
+ sage: from mjo.eja.eja_algebra import ComplexMatrixEJA
- Later this might be extended to return Cartesian products of the
- EJAs above.
+ EXAMPLES::
- SETUP::
+ sage: A = matrix(QQ,[ [ 1, 2, 3, 4],
+ ....: [-2, 1, -4, 3],
+ ....: [ 9, 10, 11, 12],
+ ....: [-10, 9, -12, 11] ])
+ sage: ComplexMatrixEJA.real_unembed(A)
+ [ 2*I + 1 4*I + 3]
+ [ 10*I + 9 12*I + 11]
- sage: from mjo.eja.eja_algebra import random_eja
+ TESTS:
- TESTS::
+ Unembedding is the inverse of embedding::
- sage: random_eja()
- Euclidean Jordan algebra of degree...
+ sage: set_random_seed()
+ sage: F = QuadraticField(-1, 'I')
+ sage: M = random_matrix(F, 3)
+ sage: Me = ComplexMatrixEJA.real_embed(M)
+ sage: ComplexMatrixEJA.real_unembed(Me) == M
+ True
+ """
+ super(ComplexMatrixEJA,cls).real_unembed(M)
+ n = ZZ(M.nrows())
+ d = cls.dimension_over_reals()
+ F = cls.complex_extension(M.base_ring())
+ 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 range(n/d):
+ for j in range(n/d):
+ submat = M[d*k:d*k+d,d*j:d*j+d]
+ 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/d, elements)
+
+
+class ComplexHermitianEJA(ConcreteEJA, ComplexMatrixEJA):
"""
+ 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.
- # 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)
+ SETUP::
+ sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
+ EXAMPLES:
-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(n):
- for j in xrange(i+1):
- Eij = matrix(field, n, lambda k,l: k==i and l==j)
- if i == j:
- Sij = Eij
- else:
- # Beware, orthogonal but not normalized!
- Sij = Eij + Eij.transpose()
- S.append(Sij)
- return tuple(S)
+ In theory, our "field" can be any subfield of the reals::
+ sage: ComplexHermitianEJA(2, field=RDF)
+ Euclidean Jordan algebra of dimension 4 over Real Double Field
+ sage: ComplexHermitianEJA(2, field=RR)
+ Euclidean Jordan algebra of dimension 4 over Real Field with
+ 53 bits of precision
-def _complex_hermitian_basis(n, field=QQ):
- """
- Returns a basis for the space of complex Hermitian n-by-n matrices.
+ TESTS:
- SETUP::
+ The dimension of this algebra is `n^2`::
- sage: from mjo.eja.eja_algebra import _complex_hermitian_basis
+ sage: set_random_seed()
+ sage: n_max = ComplexHermitianEJA._max_random_instance_size()
+ sage: n = ZZ.random_element(1, n_max)
+ sage: J = ComplexHermitianEJA(n)
+ sage: J.dimension() == n^2
+ True
- TESTS::
+ The Jordan multiplication is what we think it is::
sage: set_random_seed()
- sage: n = ZZ.random_element(1,5)
- sage: all( M.is_symmetric() for M in _complex_hermitian_basis(n) )
+ sage: J = ComplexHermitianEJA.random_instance()
+ sage: x,y = J.random_elements(2)
+ sage: actual = (x*y).to_matrix()
+ sage: X = x.to_matrix()
+ sage: Y = y.to_matrix()
+ sage: expected = (X*Y + Y*X)/2
+ sage: actual == expected
+ True
+ sage: J(expected) == x*y
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)
+ We can change the generator prefix::
+ sage: ComplexHermitianEJA(2, prefix='z').gens()
+ (z0, z1, z2, z3)
-def _quaternion_hermitian_basis(n, field=QQ):
- """
- Returns a basis for the space of quaternion Hermitian n-by-n matrices.
+ We can construct the (trivial) algebra of rank zero::
- SETUP::
+ sage: ComplexHermitianEJA(0)
+ Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
- sage: from mjo.eja.eja_algebra import _quaternion_hermitian_basis
+ """
- TESTS::
+ @classmethod
+ def _denormalized_basis(cls, n):
+ """
+ Returns a basis for the space of complex Hermitian n-by-n matrices.
- sage: set_random_seed()
- sage: n = ZZ.random_element(1,5)
- sage: all( M.is_symmetric() for M in _quaternion_hermitian_basis(n) )
- True
+ Why do we embed these? Basically, because all of numerical linear
+ algebra assumes that you're working with vectors consisting of `n`
+ entries from a field and scalars from the same field. There's no way
+ to tell SageMath that (for example) the vectors contain complex
+ numbers, while the scalar field is real.
- """
- 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()
-
- V = VectorSpace(field, dimension**2)
- W = V.span( _mat2vec(s) for s in basis )
-
- # 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
- # to find out what the corresponding row should be. BEWARE:
- # these multiplication tables won't be symmetric! It therefore
- # becomes REALLY IMPORTANT that the underlying algebra
- # constructor uses ROW vectors and not COLUMN vectors. That's
- # why we're computing rows here and not columns.
- Q_rows = []
- for t in S:
- this_row = _mat2vec((s*t + t*s)/2)
- Q_rows.append(W.coordinates(this_row))
- Q = matrix(field, W.dimension(), Q_rows)
- Qs.append(Q)
-
- 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]]``.
+ SETUP::
- SETUP::
+ sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
- sage: from mjo.eja.eja_algebra import _embed_complex_matrix
+ TESTS::
- EXAMPLES::
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,5)
+ sage: B = ComplexHermitianEJA._denormalized_basis(n)
+ sage: all( M.is_symmetric() for M in B)
+ True
- 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]
+ """
+ field = ZZ
+ R = PolynomialRing(field, 'z')
+ z = R.gen()
+ F = field.extension(z**2 + 1, 'I')
+ I = F.gen(1)
- TESTS:
+ # 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 = []
+ Eij = matrix.zero(F,n)
+ for i in range(n):
+ for j in range(i+1):
+ # "build" E_ij
+ Eij[i,j] = 1
+ if i == j:
+ Sij = cls.real_embed(Eij)
+ S.append(Sij)
+ else:
+ # The second one has a minus because it's conjugated.
+ Eij[j,i] = 1 # Eij = Eij + Eij.transpose()
+ Sij_real = cls.real_embed(Eij)
+ S.append(Sij_real)
+ # Eij = I*Eij - I*Eij.transpose()
+ Eij[i,j] = I
+ Eij[j,i] = -I
+ Sij_imag = cls.real_embed(Eij)
+ S.append(Sij_imag)
+ Eij[j,i] = 0
+ # "erase" E_ij
+ Eij[i,j] = 0
+
+ # Since we embedded these, we can drop back to the "field" that we
+ # started with instead of the complex extension "F".
+ return tuple( s.change_ring(field) for s in S )
+
+
+ def __init__(self, n, **kwargs):
+ # We know this is a valid EJA, but will double-check
+ # if the user passes check_axioms=True.
+ if "check_axioms" not in kwargs: kwargs["check_axioms"] = False
+
+ super(ComplexHermitianEJA, self).__init__(self._denormalized_basis(n),
+ self.jordan_product,
+ self.trace_inner_product,
+ **kwargs)
+ # TODO: this could be factored out somehow, but is left here
+ # because the MatrixEJA is not presently a subclass of the
+ # FDEJA class that defines rank() and one().
+ self.rank.set_cache(n)
+ idV = matrix.identity(ZZ, self.dimension_over_reals()*n)
+ self.one.set_cache(self(idV))
- Embedding is a homomorphism (isomorphism, in fact)::
+ @staticmethod
+ def _max_random_instance_size():
+ return 3 # Dimension 9
- 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
+ @classmethod
+ def random_instance(cls, **kwargs):
+ """
+ Return a random instance of this type of algebra.
+ """
+ n = ZZ.random_element(cls._max_random_instance_size() + 1)
+ return cls(n, **kwargs)
- """
- 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]]))
+class QuaternionMatrixEJA(MatrixEJA):
- # We can drop the imaginaries here.
- return matrix.block(field.base_ring(), n, blocks)
+ # A manual dictionary-cache for the quaternion_extension() method,
+ # since apparently @classmethods can't also be @cached_methods.
+ _quaternion_extension = {}
+ @classmethod
+ def quaternion_extension(cls,field):
+ r"""
+ The quaternion field that we embed/unembed, as an extension
+ of the given ``field``.
+ """
+ if field in cls._quaternion_extension:
+ return cls._quaternion_extension[field]
-def _unembed_complex_matrix(M):
- """
- The inverse of _embed_complex_matrix().
+ Q = QuaternionAlgebra(field,-1,-1)
- SETUP::
+ cls._quaternion_extension[field] = Q
+ return Q
- sage: from mjo.eja.eja_algebra import (_embed_complex_matrix,
- ....: _unembed_complex_matrix)
+ @staticmethod
+ def dimension_over_reals():
+ return 4
- EXAMPLES::
+ @classmethod
+ def real_embed(cls,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.
- 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]
+ SETUP::
- TESTS:
+ sage: from mjo.eja.eja_algebra import QuaternionMatrixEJA
- Unembedding is the inverse of embedding::
+ EXAMPLES::
- 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
+ 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: QuaternionMatrixEJA.real_embed(M)
+ [ 1 2 3 4]
+ [-2 1 -4 3]
+ [-3 4 1 -2]
+ [-4 -3 2 1]
- """
- 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.
+ Embedding is a homomorphism (isomorphism, in fact)::
- SETUP::
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(2)
+ sage: Q = QuaternionAlgebra(QQ,-1,-1)
+ sage: X = random_matrix(Q, n)
+ sage: Y = random_matrix(Q, n)
+ sage: Xe = QuaternionMatrixEJA.real_embed(X)
+ sage: Ye = QuaternionMatrixEJA.real_embed(Y)
+ sage: XYe = QuaternionMatrixEJA.real_embed(X*Y)
+ sage: Xe*Ye == XYe
+ True
- sage: from mjo.eja.eja_algebra import _embed_quaternion_matrix
+ """
+ super(QuaternionMatrixEJA,cls).real_embed(M)
+ quaternions = M.base_ring()
+ n = M.nrows()
- EXAMPLES::
+ F = QuadraticField(-1, 'I')
+ i = F.gen()
- 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]
+ blocks = []
+ for z in M.list():
+ t = z.coefficient_tuple()
+ a = t[0]
+ b = t[1]
+ c = t[2]
+ d = t[3]
+ cplxM = matrix(F, 2, [[ a + b*i, c + d*i],
+ [-c + d*i, a - b*i]])
+ realM = ComplexMatrixEJA.real_embed(cplxM)
+ blocks.append(realM)
- Embedding is a homomorphism (isomorphism, in fact)::
+ # We should have real entries by now, so use the realest field
+ # we've got for the return value.
+ return matrix.block(quaternions.base_ring(), n, blocks)
- 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 matrix.block(quaternions.base_ring(), n, blocks)
-
-
-def _unembed_quaternion_matrix(M):
- """
- The inverse of _embed_quaternion_matrix().
- SETUP::
+ @classmethod
+ def real_unembed(cls,M):
+ """
+ The inverse of _embed_quaternion_matrix().
+
+ SETUP::
- sage: from mjo.eja.eja_algebra import (_embed_quaternion_matrix,
- ....: _unembed_quaternion_matrix)
+ sage: from mjo.eja.eja_algebra import QuaternionMatrixEJA
- EXAMPLES::
+ 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]
+ sage: M = matrix(QQ, [[ 1, 2, 3, 4],
+ ....: [-2, 1, -4, 3],
+ ....: [-3, 4, 1, -2],
+ ....: [-4, -3, 2, 1]])
+ sage: QuaternionMatrixEJA.real_unembed(M)
+ [1 + 2*i + 3*j + 4*k]
- TESTS:
+ TESTS:
- Unembedding is the inverse of embedding::
+ 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
+ sage: set_random_seed()
+ sage: Q = QuaternionAlgebra(QQ, -1, -1)
+ sage: M = random_matrix(Q, 3)
+ sage: Me = QuaternionMatrixEJA.real_embed(M)
+ sage: QuaternionMatrixEJA.real_unembed(Me) == M
+ True
- """
- n = ZZ(M.nrows())
- if M.ncols() != n:
- raise ValueError("the matrix 'M' must be square")
- if not n.mod(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.
+ """
+ super(QuaternionMatrixEJA,cls).real_unembed(M)
+ n = ZZ(M.nrows())
+ d = cls.dimension_over_reals()
+
+ # Use the base ring of the matrix to ensure that its entries can be
+ # multiplied by elements of the quaternion algebra.
+ Q = cls.quaternion_extension(M.base_ring())
+ 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 range(n/d):
+ for m in range(n/d):
+ submat = ComplexMatrixEJA.real_unembed(
+ M[d*l:d*l+d,d*m:d*m+d] )
+ 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()
+ z += submat[0,0].imag()*i
+ z += submat[0,1].real()*j
+ z += submat[0,1].imag()*k
+ elements.append(z)
+
+ return matrix(Q, n/d, elements)
+
+
+class QuaternionHermitianEJA(ConcreteEJA, QuaternionMatrixEJA):
+ r"""
+ 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.
SETUP::
- sage: from mjo.eja.eja_algebra import RealSymmetricEJA
+ sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
- EXAMPLES::
+ EXAMPLES:
- sage: J = RealSymmetricEJA(2)
- sage: e0, e1, e2 = J.gens()
- sage: e0*e0
- e0
- sage: e1*e1
- e0 + e2
- sage: e2*e2
- e2
+ In theory, our "field" can be any subfield of the reals::
+
+ sage: QuaternionHermitianEJA(2, field=RDF)
+ Euclidean Jordan algebra of dimension 6 over Real Double Field
+ sage: QuaternionHermitianEJA(2, field=RR)
+ Euclidean Jordan algebra of dimension 6 over Real Field with
+ 53 bits of precision
TESTS:
- The degree of this algebra is `(n^2 + n) / 2`::
+ The dimension of this algebra is `2*n^2 - n`::
sage: set_random_seed()
- sage: n = ZZ.random_element(1,5)
- sage: J = RealSymmetricEJA(n)
- sage: J.degree() == (n^2 + n)/2
+ sage: n_max = QuaternionHermitianEJA._max_random_instance_size()
+ sage: n = ZZ.random_element(1, n_max)
+ sage: J = QuaternionHermitianEJA(n)
+ sage: J.dimension() == 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 = 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: J = QuaternionHermitianEJA.random_instance()
+ sage: x,y = J.random_elements(2)
+ sage: actual = (x*y).to_matrix()
+ sage: X = x.to_matrix()
+ sage: Y = y.to_matrix()
sage: expected = (X*Y + Y*X)/2
sage: actual == expected
True
sage: J(expected) == x*y
True
+ We can change the generator prefix::
+
+ sage: QuaternionHermitianEJA(2, prefix='a').gens()
+ (a0, a1, a2, a3, a4, a5)
+
+ We can construct the (trivial) algebra of rank zero::
+
+ sage: QuaternionHermitianEJA(0)
+ Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
+
"""
- @staticmethod
- def __classcall_private__(cls, n, field=QQ):
- S = _real_symmetric_basis(n, field=field)
- (Qs, T) = _multiplication_table_from_matrix_basis(S)
+ @classmethod
+ def _denormalized_basis(cls, n):
+ """
+ Returns a basis for the space of quaternion Hermitian n-by-n matrices.
- fdeja = super(RealSymmetricEJA, cls)
- return fdeja.__classcall_private__(cls,
- field,
- Qs,
- rank=n,
- natural_basis=T)
+ Why do we embed these? Basically, because all of numerical
+ linear algebra assumes that you're working with vectors consisting
+ of `n` entries from a field and scalars from the same field. There's
+ no way to tell SageMath that (for example) the vectors contain
+ complex numbers, while the scalar field is real.
- def inner_product(self, x, y):
- return _matrix_ip(x,y)
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
+
+ TESTS::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,5)
+ sage: B = QuaternionHermitianEJA._denormalized_basis(n)
+ sage: all( M.is_symmetric() for M in B )
+ True
+
+ """
+ field = ZZ
+ 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 = []
+ Eij = matrix.zero(Q,n)
+ for i in range(n):
+ for j in range(i+1):
+ # "build" E_ij
+ Eij[i,j] = 1
+ if i == j:
+ Sij = cls.real_embed(Eij)
+ S.append(Sij)
+ else:
+ # The second, third, and fourth ones have a minus
+ # because they're conjugated.
+ # Eij = Eij + Eij.transpose()
+ Eij[j,i] = 1
+ Sij_real = cls.real_embed(Eij)
+ S.append(Sij_real)
+ # Eij = I*(Eij - Eij.transpose())
+ Eij[i,j] = I
+ Eij[j,i] = -I
+ Sij_I = cls.real_embed(Eij)
+ S.append(Sij_I)
+ # Eij = J*(Eij - Eij.transpose())
+ Eij[i,j] = J
+ Eij[j,i] = -J
+ Sij_J = cls.real_embed(Eij)
+ S.append(Sij_J)
+ # Eij = K*(Eij - Eij.transpose())
+ Eij[i,j] = K
+ Eij[j,i] = -K
+ Sij_K = cls.real_embed(Eij)
+ S.append(Sij_K)
+ Eij[j,i] = 0
+ # "erase" E_ij
+ Eij[i,j] = 0
+
+ # Since we embedded these, we can drop back to the "field" that we
+ # started with instead of the quaternion algebra "Q".
+ return tuple( s.change_ring(field) for s in S )
+
+
+ def __init__(self, n, **kwargs):
+ # We know this is a valid EJA, but will double-check
+ # if the user passes check_axioms=True.
+ if "check_axioms" not in kwargs: kwargs["check_axioms"] = False
+
+ super(QuaternionHermitianEJA, self).__init__(self._denormalized_basis(n),
+ self.jordan_product,
+ self.trace_inner_product,
+ **kwargs)
+ # TODO: this could be factored out somehow, but is left here
+ # because the MatrixEJA is not presently a subclass of the
+ # FDEJA class that defines rank() and one().
+ self.rank.set_cache(n)
+ idV = matrix.identity(ZZ, self.dimension_over_reals()*n)
+ self.one.set_cache(self(idV))
+
+
+ @staticmethod
+ def _max_random_instance_size():
+ r"""
+ The maximum rank of a random QuaternionHermitianEJA.
+ """
+ return 2 # Dimension 6
+
+ @classmethod
+ def random_instance(cls, **kwargs):
+ """
+ Return a random instance of this type of algebra.
+ """
+ n = ZZ.random_element(cls._max_random_instance_size() + 1)
+ return cls(n, **kwargs)
-class ComplexHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra):
+class HadamardEJA(ConcreteEJA):
"""
- 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.
+ 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.
SETUP::
- sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
+ sage: from mjo.eja.eja_algebra import HadamardEJA
- TESTS:
+ EXAMPLES:
- The degree of this algebra is `n^2`::
+ This multiplication table can be verified by hand::
- sage: set_random_seed()
- sage: n = ZZ.random_element(1,5)
- sage: J = ComplexHermitianEJA(n)
- sage: J.degree() == n^2
- True
+ sage: J = HadamardEJA(3)
+ sage: e0,e1,e2 = J.gens()
+ sage: e0*e0
+ e0
+ sage: e0*e1
+ 0
+ sage: e0*e2
+ 0
+ sage: e1*e1
+ e1
+ sage: e1*e2
+ 0
+ sage: e2*e2
+ e2
- The Jordan multiplication is what we think it is::
+ TESTS:
- 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
+ We can change the generator prefix::
+
+ sage: HadamardEJA(3, prefix='r').gens()
+ (r0, r1, r2)
"""
+ def __init__(self, n, **kwargs):
+ if n == 0:
+ jordan_product = lambda x,y: x
+ inner_product = lambda x,y: x
+ else:
+ def jordan_product(x,y):
+ P = x.parent()
+ return P( xi*yi for (xi,yi) in zip(x,y) )
+
+ def inner_product(x,y):
+ return (x.T*y)[0,0]
+
+ # New defaults for keyword arguments. Don't orthonormalize
+ # because our basis is already orthonormal with respect to our
+ # inner-product. Don't check the axioms, because we know this
+ # is a valid EJA... but do double-check if the user passes
+ # check_axioms=True. Note: we DON'T override the "check_field"
+ # default here, because the user can pass in a field!
+ if "orthonormalize" not in kwargs: kwargs["orthonormalize"] = False
+ if "check_axioms" not in kwargs: kwargs["check_axioms"] = False
+
+ column_basis = tuple( b.column() for b in FreeModule(ZZ, n).basis() )
+ super().__init__(column_basis,
+ jordan_product,
+ inner_product,
+ associative=True,
+ **kwargs)
+ self.rank.set_cache(n)
+
+ if n == 0:
+ self.one.set_cache( self.zero() )
+ else:
+ self.one.set_cache( sum(self.gens()) )
+
@staticmethod
- def __classcall_private__(cls, n, field=QQ):
- S = _complex_hermitian_basis(n)
- (Qs, T) = _multiplication_table_from_matrix_basis(S)
+ def _max_random_instance_size():
+ r"""
+ The maximum dimension of a random HadamardEJA.
+ """
+ return 5
- fdeja = super(ComplexHermitianEJA, cls)
- return fdeja.__classcall_private__(cls,
- field,
- Qs,
- rank=n,
- natural_basis=T)
+ @classmethod
+ def random_instance(cls, **kwargs):
+ """
+ Return a random instance of this type of algebra.
+ """
+ n = ZZ.random_element(cls._max_random_instance_size() + 1)
+ return cls(n, **kwargs)
- 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 BilinearFormEJA(ConcreteEJA):
+ r"""
+ The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
+ with the half-trace inner product and jordan product ``x*y =
+ (<Bx,y>,y_bar>, x0*y_bar + y0*x_bar)`` where `B = 1 \times B22` is
+ a symmetric positive-definite "bilinear form" matrix. Its
+ dimension is the size of `B`, and it has rank two in dimensions
+ larger than two. It reduces to the ``JordanSpinEJA`` when `B` is
+ the identity matrix of order ``n``.
-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.
+ We insist that the one-by-one upper-left identity block of `B` be
+ passed in as well so that we can be passed a matrix of size zero
+ to construct a trivial algebra.
SETUP::
- sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
+ sage: from mjo.eja.eja_algebra import (BilinearFormEJA,
+ ....: JordanSpinEJA)
- TESTS:
+ EXAMPLES:
- The degree of this algebra is `n^2`::
+ When no bilinear form is specified, the identity matrix is used,
+ and the resulting algebra is the Jordan spin algebra::
- sage: set_random_seed()
- sage: n = ZZ.random_element(1,5)
- sage: J = QuaternionHermitianEJA(n)
- sage: J.degree() == 2*(n^2) - n
+ sage: B = matrix.identity(AA,3)
+ sage: J0 = BilinearFormEJA(B)
+ sage: J1 = JordanSpinEJA(3)
+ sage: J0.multiplication_table() == J0.multiplication_table()
True
- The Jordan multiplication is what we think it is::
+ An error is raised if the matrix `B` does not correspond to a
+ positive-definite bilinear form::
+
+ sage: B = matrix.random(QQ,2,3)
+ sage: J = BilinearFormEJA(B)
+ Traceback (most recent call last):
+ ...
+ ValueError: bilinear form is not positive-definite
+ sage: B = matrix.zero(QQ,3)
+ sage: J = BilinearFormEJA(B)
+ Traceback (most recent call last):
+ ...
+ ValueError: bilinear form is not positive-definite
+
+ TESTS:
+
+ We can create a zero-dimensional algebra::
+
+ sage: B = matrix.identity(AA,0)
+ sage: J = BilinearFormEJA(B)
+ sage: J.basis()
+ Finite family {}
+
+ We can check the multiplication condition given in the Jordan, von
+ Neumann, and Wigner paper (and also discussed on my "On the
+ symmetry..." paper). Note that this relies heavily on the standard
+ choice of basis, as does anything utilizing the bilinear form
+ matrix. We opt not to orthonormalize the basis, because if we
+ did, we would have to normalize the `s_{i}` in a similar manner::
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: n = ZZ.random_element(5)
+ sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular')
+ sage: B11 = matrix.identity(QQ,1)
+ sage: B22 = M.transpose()*M
+ sage: B = block_matrix(2,2,[ [B11,0 ],
+ ....: [0, B22 ] ])
+ sage: J = BilinearFormEJA(B, orthonormalize=False)
+ sage: eis = VectorSpace(M.base_ring(), M.ncols()).basis()
+ sage: V = J.vector_space()
+ sage: sis = [ J( V([0] + (M.inverse()*ei).list()).column() )
+ ....: for ei in eis ]
+ sage: actual = [ sis[i]*sis[j]
+ ....: for i in range(n-1)
+ ....: for j in range(n-1) ]
+ sage: expected = [ J.one() if i == j else J.zero()
+ ....: for i in range(n-1)
+ ....: for j in range(n-1) ]
sage: actual == expected
True
- sage: J(expected) == x*y
- True
"""
+ def __init__(self, B, **kwargs):
+ # The matrix "B" is supplied by the user in most cases,
+ # so it makes sense to check whether or not its positive-
+ # definite unless we are specifically asked not to...
+ if ("check_axioms" not in kwargs) or kwargs["check_axioms"]:
+ if not B.is_positive_definite():
+ raise ValueError("bilinear form is not positive-definite")
+
+ # However, all of the other data for this EJA is computed
+ # by us in manner that guarantees the axioms are
+ # satisfied. So, again, unless we are specifically asked to
+ # verify things, we'll skip the rest of the checks.
+ if "check_axioms" not in kwargs: kwargs["check_axioms"] = False
+
+ def inner_product(x,y):
+ return (y.T*B*x)[0,0]
+
+ def jordan_product(x,y):
+ P = x.parent()
+ x0 = x[0,0]
+ xbar = x[1:,0]
+ y0 = y[0,0]
+ ybar = y[1:,0]
+ z0 = inner_product(y,x)
+ zbar = y0*xbar + x0*ybar
+ return P([z0] + zbar.list())
+
+ n = B.nrows()
+ column_basis = tuple( b.column() for b in FreeModule(ZZ, n).basis() )
+ super(BilinearFormEJA, self).__init__(column_basis,
+ jordan_product,
+ inner_product,
+ **kwargs)
+
+ # The rank of this algebra is two, unless we're in a
+ # one-dimensional ambient space (because the rank is bounded
+ # by the ambient dimension).
+ self.rank.set_cache(min(n,2))
+
+ if n == 0:
+ self.one.set_cache( self.zero() )
+ else:
+ self.one.set_cache( self.monomial(0) )
+
@staticmethod
- def __classcall_private__(cls, n, field=QQ):
- S = _quaternion_hermitian_basis(n)
- (Qs, T) = _multiplication_table_from_matrix_basis(S)
+ def _max_random_instance_size():
+ r"""
+ The maximum dimension of a random BilinearFormEJA.
+ """
+ return 5
- fdeja = super(QuaternionHermitianEJA, cls)
- return fdeja.__classcall_private__(cls,
- field,
- Qs,
- rank=n,
- natural_basis=T)
+ @classmethod
+ def random_instance(cls, **kwargs):
+ """
+ Return a random instance of this algebra.
+ """
+ n = ZZ.random_element(cls._max_random_instance_size() + 1)
+ if n.is_zero():
+ B = matrix.identity(ZZ, n)
+ return cls(B, **kwargs)
- 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
+ B11 = matrix.identity(ZZ, 1)
+ M = matrix.random(ZZ, n-1)
+ I = matrix.identity(ZZ, n-1)
+ alpha = ZZ.zero()
+ while alpha.is_zero():
+ alpha = ZZ.random_element().abs()
+ B22 = M.transpose()*M + alpha*I
+
+ from sage.matrix.special import block_matrix
+ B = block_matrix(2,2, [ [B11, ZZ(0) ],
+ [ZZ(0), B22 ] ])
+
+ return cls(B, **kwargs)
-class JordanSpinEJA(FiniteDimensionalEuclideanJordanAlgebra):
+class JordanSpinEJA(BilinearFormEJA):
"""
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
+ (<x,y>, x0*y_bar + y0*x_bar)``. It has dimension `n` over
the reals.
SETUP::
sage: e2*e3
0
+ We can change the generator prefix::
+
+ sage: JordanSpinEJA(2, prefix='B').gens()
+ (B0, B1)
+
+ TESTS:
+
+ Ensure that we have the usual inner product on `R^n`::
+
+ sage: set_random_seed()
+ sage: J = JordanSpinEJA.random_instance()
+ sage: x,y = J.random_elements(2)
+ sage: actual = x.inner_product(y)
+ sage: expected = x.to_vector().inner_product(y.to_vector())
+ sage: actual == expected
+ True
+
"""
+ def __init__(self, n, **kwargs):
+ # This is a special case of the BilinearFormEJA with the
+ # identity matrix as its bilinear form.
+ B = matrix.identity(ZZ, n)
+
+ # Don't orthonormalize because our basis is already
+ # orthonormal with respect to our inner-product.
+ if "orthonormalize" not in kwargs: kwargs["orthonormalize"] = False
+
+ # But also don't pass check_field=False here, because the user
+ # can pass in a field!
+ super(JordanSpinEJA, self).__init__(B, **kwargs)
+
@staticmethod
- def __classcall_private__(cls, n, field=QQ):
- Qs = []
- id_matrix = matrix.identity(field, n)
- for i in xrange(n):
- ei = id_matrix.column(i)
- Qi = matrix.zero(field, n)
- Qi.set_row(0, ei)
- Qi.set_column(0, ei)
- Qi += matrix.diagonal(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 _max_random_instance_size():
+ r"""
+ The maximum dimension of a random JordanSpinEJA.
+ """
+ return 5
- def inner_product(self, x, y):
- return _usual_ip(x,y)
+ @classmethod
+ def random_instance(cls, **kwargs):
+ """
+ Return a random instance of this type of algebra.
+
+ Needed here to override the implementation for ``BilinearFormEJA``.
+ """
+ n = ZZ.random_element(cls._max_random_instance_size() + 1)
+ return cls(n, **kwargs)
+
+
+class TrivialEJA(ConcreteEJA):
+ """
+ The trivial Euclidean Jordan algebra consisting of only a zero element.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import TrivialEJA
+
+ EXAMPLES::
+
+ sage: J = TrivialEJA()
+ sage: J.dimension()
+ 0
+ sage: J.zero()
+ 0
+ sage: J.one()
+ 0
+ sage: 7*J.one()*12*J.one()
+ 0
+ sage: J.one().inner_product(J.one())
+ 0
+ sage: J.one().norm()
+ 0
+ sage: J.one().subalgebra_generated_by()
+ Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
+ sage: J.rank()
+ 0
+
+ """
+ def __init__(self, **kwargs):
+ jordan_product = lambda x,y: x
+ inner_product = lambda x,y: 0
+ basis = ()
+
+ # New defaults for keyword arguments
+ if "orthonormalize" not in kwargs: kwargs["orthonormalize"] = False
+ if "check_axioms" not in kwargs: kwargs["check_axioms"] = False
+
+ super(TrivialEJA, self).__init__(basis,
+ jordan_product,
+ inner_product,
+ **kwargs)
+ # The rank is zero using my definition, namely the dimension of the
+ # largest subalgebra generated by any element.
+ self.rank.set_cache(0)
+ self.one.set_cache( self.zero() )
+
+ @classmethod
+ def random_instance(cls, **kwargs):
+ # We don't take a "size" argument so the superclass method is
+ # inappropriate for us.
+ return cls(**kwargs)
+
+
+class CartesianProductEJA(CombinatorialFreeModule_CartesianProduct,
+ FiniteDimensionalEJA):
+ r"""
+ The external (orthogonal) direct sum of two or more Euclidean
+ Jordan algebras. Every Euclidean Jordan algebra decomposes into an
+ orthogonal direct sum of simple Euclidean Jordan algebras which is
+ then isometric to a Cartesian product, so no generality is lost by
+ providing only this construction.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (random_eja,
+ ....: CartesianProductEJA,
+ ....: HadamardEJA,
+ ....: JordanSpinEJA,
+ ....: RealSymmetricEJA)
+
+ EXAMPLES:
+
+ The Jordan product is inherited from our factors and implemented by
+ our CombinatorialFreeModule Cartesian product superclass::
+
+ sage: set_random_seed()
+ sage: J1 = HadamardEJA(2)
+ sage: J2 = RealSymmetricEJA(2)
+ sage: J = cartesian_product([J1,J2])
+ sage: x,y = J.random_elements(2)
+ sage: x*y in J
+ True
+
+ The ability to retrieve the original factors is implemented by our
+ CombinatorialFreeModule Cartesian product superclass::
+
+ sage: J1 = HadamardEJA(2, field=QQ)
+ sage: J2 = JordanSpinEJA(3, field=QQ)
+ sage: J = cartesian_product([J1,J2])
+ sage: J.cartesian_factors()
+ (Euclidean Jordan algebra of dimension 2 over Rational Field,
+ Euclidean Jordan algebra of dimension 3 over Rational Field)
+
+ You can provide more than two factors::
+
+ sage: J1 = HadamardEJA(2)
+ sage: J2 = JordanSpinEJA(3)
+ sage: J3 = RealSymmetricEJA(3)
+ sage: cartesian_product([J1,J2,J3])
+ Euclidean Jordan algebra of dimension 2 over Algebraic Real
+ Field (+) Euclidean Jordan algebra of dimension 3 over Algebraic
+ Real Field (+) Euclidean Jordan algebra of dimension 6 over
+ Algebraic Real Field
+
+ Rank is additive on a Cartesian product::
+
+ sage: J1 = HadamardEJA(1)
+ sage: J2 = RealSymmetricEJA(2)
+ sage: J = cartesian_product([J1,J2])
+ sage: J1.rank.clear_cache()
+ sage: J2.rank.clear_cache()
+ sage: J.rank.clear_cache()
+ sage: J.rank()
+ 3
+ sage: J.rank() == J1.rank() + J2.rank()
+ True
+
+ The same rank computation works over the rationals, with whatever
+ basis you like::
+
+ sage: J1 = HadamardEJA(1, field=QQ, orthonormalize=False)
+ sage: J2 = RealSymmetricEJA(2, field=QQ, orthonormalize=False)
+ sage: J = cartesian_product([J1,J2])
+ sage: J1.rank.clear_cache()
+ sage: J2.rank.clear_cache()
+ sage: J.rank.clear_cache()
+ sage: J.rank()
+ 3
+ sage: J.rank() == J1.rank() + J2.rank()
+ True
+
+ The product algebra will be associative if and only if all of its
+ components are associative::
+
+ sage: J1 = HadamardEJA(2)
+ sage: J1.is_associative()
+ True
+ sage: J2 = HadamardEJA(3)
+ sage: J2.is_associative()
+ True
+ sage: J3 = RealSymmetricEJA(3)
+ sage: J3.is_associative()
+ False
+ sage: CP1 = cartesian_product([J1,J2])
+ sage: CP1.is_associative()
+ True
+ sage: CP2 = cartesian_product([J1,J3])
+ sage: CP2.is_associative()
+ False
+
+ TESTS:
+
+ All factors must share the same base field::
+
+ sage: J1 = HadamardEJA(2, field=QQ)
+ sage: J2 = RealSymmetricEJA(2)
+ sage: CartesianProductEJA((J1,J2))
+ Traceback (most recent call last):
+ ...
+ ValueError: all factors must share the same base field
+
+ The cached unit element is the same one that would be computed::
+
+ sage: set_random_seed() # long time
+ sage: J1 = random_eja() # long time
+ sage: J2 = random_eja() # long time
+ sage: J = cartesian_product([J1,J2]) # long time
+ sage: actual = J.one() # long time
+ sage: J.one.clear_cache() # long time
+ sage: expected = J.one() # long time
+ sage: actual == expected # long time
+ True
+
+ """
+ Element = FiniteDimensionalEJAElement
+
+
+ def __init__(self, algebras, **kwargs):
+ CombinatorialFreeModule_CartesianProduct.__init__(self,
+ algebras,
+ **kwargs)
+ field = algebras[0].base_ring()
+ if not all( J.base_ring() == field for J in algebras ):
+ raise ValueError("all factors must share the same base field")
+
+ associative = all( m.is_associative() for m in algebras )
+
+ # The definition of matrix_space() and self.basis() relies
+ # only on the stuff in the CFM_CartesianProduct class, which
+ # we've already initialized.
+ Js = self.cartesian_factors()
+ m = len(Js)
+ MS = self.matrix_space()
+ basis = tuple(
+ MS(tuple( self.cartesian_projection(i)(b).to_matrix()
+ for i in range(m) ))
+ for b in self.basis()
+ )
+
+ # Define jordan/inner products that operate on that matrix_basis.
+ def jordan_product(x,y):
+ return MS(tuple(
+ (Js[i](x[i])*Js[i](y[i])).to_matrix() for i in range(m)
+ ))
+
+ def inner_product(x, y):
+ return sum(
+ Js[i](x[i]).inner_product(Js[i](y[i])) for i in range(m)
+ )
+
+ # There's no need to check the field since it already came
+ # from an EJA. Likewise the axioms are guaranteed to be
+ # satisfied, unless the guy writing this class sucks.
+ #
+ # If you want the basis to be orthonormalized, orthonormalize
+ # the factors.
+ FiniteDimensionalEJA.__init__(self,
+ basis,
+ jordan_product,
+ inner_product,
+ field=field,
+ orthonormalize=False,
+ associative=associative,
+ cartesian_product=True,
+ check_field=False,
+ check_axioms=False)
+
+ ones = tuple(J.one() for J in algebras)
+ self.one.set_cache(self._cartesian_product_of_elements(ones))
+ self.rank.set_cache(sum(J.rank() for J in algebras))
+
+ def matrix_space(self):
+ r"""
+ Return the space that our matrix basis lives in as a Cartesian
+ product.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (HadamardEJA,
+ ....: RealSymmetricEJA)
+
+ EXAMPLES::
+
+ sage: J1 = HadamardEJA(1)
+ sage: J2 = RealSymmetricEJA(2)
+ sage: J = cartesian_product([J1,J2])
+ sage: J.matrix_space()
+ The Cartesian product of (Full MatrixSpace of 1 by 1 dense
+ matrices over Algebraic Real Field, Full MatrixSpace of 2
+ by 2 dense matrices over Algebraic Real Field)
+
+ """
+ from sage.categories.cartesian_product import cartesian_product
+ return cartesian_product( [J.matrix_space()
+ for J in self.cartesian_factors()] )
+
+ @cached_method
+ def cartesian_projection(self, i):
+ r"""
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (random_eja,
+ ....: JordanSpinEJA,
+ ....: HadamardEJA,
+ ....: RealSymmetricEJA,
+ ....: ComplexHermitianEJA)
+
+ EXAMPLES:
+
+ The projection morphisms are Euclidean Jordan algebra
+ operators::
+
+ sage: J1 = HadamardEJA(2)
+ sage: J2 = RealSymmetricEJA(2)
+ sage: J = cartesian_product([J1,J2])
+ sage: J.cartesian_projection(0)
+ Linear operator between finite-dimensional Euclidean Jordan
+ algebras represented by the matrix:
+ [1 0 0 0 0]
+ [0 1 0 0 0]
+ Domain: Euclidean Jordan algebra of dimension 2 over Algebraic
+ Real Field (+) Euclidean Jordan algebra of dimension 3 over
+ Algebraic Real Field
+ Codomain: Euclidean Jordan algebra of dimension 2 over Algebraic
+ Real Field
+ sage: J.cartesian_projection(1)
+ Linear operator between finite-dimensional Euclidean Jordan
+ algebras represented by the matrix:
+ [0 0 1 0 0]
+ [0 0 0 1 0]
+ [0 0 0 0 1]
+ Domain: Euclidean Jordan algebra of dimension 2 over Algebraic
+ Real Field (+) Euclidean Jordan algebra of dimension 3 over
+ Algebraic Real Field
+ Codomain: Euclidean Jordan algebra of dimension 3 over Algebraic
+ Real Field
+
+ The projections work the way you'd expect on the vector
+ representation of an element::
+
+ sage: J1 = JordanSpinEJA(2)
+ sage: J2 = ComplexHermitianEJA(2)
+ sage: J = cartesian_product([J1,J2])
+ sage: pi_left = J.cartesian_projection(0)
+ sage: pi_right = J.cartesian_projection(1)
+ sage: pi_left(J.one()).to_vector()
+ (1, 0)
+ sage: pi_right(J.one()).to_vector()
+ (1, 0, 0, 1)
+ sage: J.one().to_vector()
+ (1, 0, 1, 0, 0, 1)
+
+ TESTS:
+
+ The answer never changes::
+
+ sage: set_random_seed()
+ sage: J1 = random_eja()
+ sage: J2 = random_eja()
+ sage: J = cartesian_product([J1,J2])
+ sage: P0 = J.cartesian_projection(0)
+ sage: P1 = J.cartesian_projection(0)
+ sage: P0 == P1
+ True
+
+ """
+ Ji = self.cartesian_factors()[i]
+ # Requires the fix on Trac 31421/31422 to work!
+ Pi = super().cartesian_projection(i)
+ return FiniteDimensionalEJAOperator(self,Ji,Pi.matrix())
+
+ @cached_method
+ def cartesian_embedding(self, i):
+ r"""
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (random_eja,
+ ....: JordanSpinEJA,
+ ....: HadamardEJA,
+ ....: RealSymmetricEJA)
+
+ EXAMPLES:
+
+ The embedding morphisms are Euclidean Jordan algebra
+ operators::
+
+ sage: J1 = HadamardEJA(2)
+ sage: J2 = RealSymmetricEJA(2)
+ sage: J = cartesian_product([J1,J2])
+ sage: J.cartesian_embedding(0)
+ Linear operator between finite-dimensional Euclidean Jordan
+ algebras represented by the matrix:
+ [1 0]
+ [0 1]
+ [0 0]
+ [0 0]
+ [0 0]
+ Domain: Euclidean Jordan algebra of dimension 2 over
+ Algebraic Real Field
+ Codomain: Euclidean Jordan algebra of dimension 2 over
+ Algebraic Real Field (+) Euclidean Jordan algebra of
+ dimension 3 over Algebraic Real Field
+ sage: J.cartesian_embedding(1)
+ Linear operator between finite-dimensional Euclidean Jordan
+ algebras represented by the matrix:
+ [0 0 0]
+ [0 0 0]
+ [1 0 0]
+ [0 1 0]
+ [0 0 1]
+ Domain: Euclidean Jordan algebra of dimension 3 over
+ Algebraic Real Field
+ Codomain: Euclidean Jordan algebra of dimension 2 over
+ Algebraic Real Field (+) Euclidean Jordan algebra of
+ dimension 3 over Algebraic Real Field
+
+ The embeddings work the way you'd expect on the vector
+ representation of an element::
+
+ sage: J1 = JordanSpinEJA(3)
+ sage: J2 = RealSymmetricEJA(2)
+ sage: J = cartesian_product([J1,J2])
+ sage: iota_left = J.cartesian_embedding(0)
+ sage: iota_right = J.cartesian_embedding(1)
+ sage: iota_left(J1.zero()) == J.zero()
+ True
+ sage: iota_right(J2.zero()) == J.zero()
+ True
+ sage: J1.one().to_vector()
+ (1, 0, 0)
+ sage: iota_left(J1.one()).to_vector()
+ (1, 0, 0, 0, 0, 0)
+ sage: J2.one().to_vector()
+ (1, 0, 1)
+ sage: iota_right(J2.one()).to_vector()
+ (0, 0, 0, 1, 0, 1)
+ sage: J.one().to_vector()
+ (1, 0, 0, 1, 0, 1)
+
+ TESTS:
+
+ The answer never changes::
+
+ sage: set_random_seed()
+ sage: J1 = random_eja()
+ sage: J2 = random_eja()
+ sage: J = cartesian_product([J1,J2])
+ sage: E0 = J.cartesian_embedding(0)
+ sage: E1 = J.cartesian_embedding(0)
+ sage: E0 == E1
+ True
+
+ Composing a projection with the corresponding inclusion should
+ produce the identity map, and mismatching them should produce
+ the zero map::
+
+ sage: set_random_seed()
+ sage: J1 = random_eja()
+ sage: J2 = random_eja()
+ sage: J = cartesian_product([J1,J2])
+ sage: iota_left = J.cartesian_embedding(0)
+ sage: iota_right = J.cartesian_embedding(1)
+ sage: pi_left = J.cartesian_projection(0)
+ sage: pi_right = J.cartesian_projection(1)
+ sage: pi_left*iota_left == J1.one().operator()
+ True
+ sage: pi_right*iota_right == J2.one().operator()
+ True
+ sage: (pi_left*iota_right).is_zero()
+ True
+ sage: (pi_right*iota_left).is_zero()
+ True
+
+ """
+ Ji = self.cartesian_factors()[i]
+ # Requires the fix on Trac 31421/31422 to work!
+ Ei = super().cartesian_embedding(i)
+ return FiniteDimensionalEJAOperator(Ji,self,Ei.matrix())
+
+
+
+FiniteDimensionalEJA.CartesianProduct = CartesianProductEJA
+
+random_eja = ConcreteEJA.random_instance
+#def random_eja(*args, **kwargs):
+# from sage.categories.cartesian_product import cartesian_product
+# J1 = HadamardEJA(1, **kwargs)
+# J2 = RealSymmetricEJA(2, **kwargs)
+# J = cartesian_product([J1,J2])
+# return J