what can be supported in a general Jordan Algebra.
"""
+from itertools import repeat
+
from sage.algebras.quatalg.quaternion_algebra import QuaternionAlgebra
from sage.categories.magmatic_algebras import MagmaticAlgebras
from sage.combinat.free_module import CombinatorialFreeModule
from sage.matrix.constructor import matrix
from sage.matrix.matrix_space import MatrixSpace
from sage.misc.cachefunc import cached_method
+from sage.misc.lazy_import import lazy_import
from sage.misc.prandom import choice
from sage.misc.table import table
from sage.modules.free_module import FreeModule, VectorSpace
-from sage.rings.integer_ring import ZZ
-from sage.rings.number_field.number_field import NumberField, QuadraticField
-from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
-from sage.rings.rational_field import QQ
-from sage.rings.real_lazy import CLF, RLF
-from sage.structure.element import is_Matrix
-
+from sage.rings.all import (ZZ, QQ, AA, QQbar, RR, RLF, CLF,
+ PolynomialRing,
+ QuadraticField)
from mjo.eja.eja_element import FiniteDimensionalEuclideanJordanAlgebraElement
+lazy_import('mjo.eja.eja_subalgebra',
+ 'FiniteDimensionalEuclideanJordanSubalgebra')
from mjo.eja.eja_utils import _mat2vec
class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
- # This is an ugly hack needed to prevent the category framework
- # from implementing a coercion from our base ring (e.g. the
- # rationals) into the algebra. First of all -- such a coercion is
- # nonsense to begin with. But more importantly, it tries to do so
- # in the category of rings, and since our algebras aren't
- # associative they generally won't be rings.
- _no_generic_basering_coercion = True
+
+ 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.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import random_eja
+
+ TESTS::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: J(1)
+ Traceback (most recent call last):
+ ...
+ ValueError: not a naturally-represented algebra element
+
+ """
+ return None
def __init__(self,
field,
mult_table,
- rank,
prefix='e',
category=None,
- natural_basis=None):
+ natural_basis=None,
+ check_field=True,
+ check_axioms=True):
"""
SETUP::
- sage: from mjo.eja.eja_algebra import random_eja
+ sage: from mjo.eja.eja_algebra import (
+ ....: FiniteDimensionalEuclideanJordanAlgebra,
+ ....: JordanSpinEJA,
+ ....: random_eja)
EXAMPLES:
sage: set_random_seed()
sage: J = random_eja()
- sage: x = J.random_element()
- sage: y = J.random_element()
+ sage: x,y = J.random_elements(2)
sage: x*y == y*x
True
+ TESTS:
+
+ The ``field`` we're given must be real with ``check_field=True``::
+
+ sage: JordanSpinEJA(2,QQbar)
+ Traceback (most recent call last):
+ ...
+ ValueError: scalar field is not real
+
+ The multiplication table must be square with ``check_axioms=True``::
+
+ sage: FiniteDimensionalEuclideanJordanAlgebra(QQ,((),()))
+ Traceback (most recent call last):
+ ...
+ ValueError: multiplication table is not square
+
"""
- self._rank = rank
- self._natural_basis = natural_basis
+ 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")
+
+ # The multiplication table had better be square
+ n = len(mult_table)
+ if check_axioms:
+ if not all( len(l) == n for l in mult_table ):
+ raise ValueError("multiplication table is not square")
- # TODO: HACK for the charpoly.. needs redesign badly.
- self._basis_normalizers = None
+ self._natural_basis = natural_basis
if category is None:
category = MagmaticAlgebras(field).FiniteDimensional()
fda = super(FiniteDimensionalEuclideanJordanAlgebra, self)
fda.__init__(field,
- range(len(mult_table)),
+ range(n),
prefix=prefix,
category=category)
self.print_options(bracket='')
# long run to have the multiplication table be in terms of
# algebra elements. We do this after calling the superclass
# constructor so that from_vector() knows what to do.
- self._multiplication_table = [ map(lambda x: self.from_vector(x), ls)
- for ls in mult_table ]
-
+ self._multiplication_table = [
+ list(map(lambda x: self.from_vector(x), ls))
+ for ls in mult_table
+ ]
+
+ if check_axioms:
+ if not self._is_commutative():
+ raise ValueError("algebra is not commutative")
+ 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 _element_constructor_(self, elt):
"""
SETUP::
sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
- ....: RealCartesianProductEJA,
+ ....: HadamardEJA,
....: RealSymmetricEJA)
EXAMPLES:
vector representations) back and forth faithfully::
sage: set_random_seed()
- sage: J = RealCartesianProductEJA(5)
+ sage: J = HadamardEJA.random_instance()
sage: x = J.random_element()
sage: J(x.to_vector().column()) == x
True
- sage: J = JordanSpinEJA(5)
+ sage: J = JordanSpinEJA.random_instance()
sage: x = J.random_element()
sage: J(x.to_vector().column()) == x
True
"""
+ msg = "not a naturally-represented algebra element"
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)
natural_basis = self.natural_basis()
basis_space = natural_basis[0].matrix_space()
if elt not in basis_space:
- raise ValueError("not a naturally-represented algebra element")
+ raise ValueError(msg)
# Thanks for nothing! Matrix spaces aren't vector spaces in
# Sage, so we have to figure out its natural-basis coordinates
coords = W.coordinate_vector(_mat2vec(elt))
return self.from_vector(coords)
-
@staticmethod
- def _max_test_case_size():
+ 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
interpreted to be far less than the dimension) should override
with a smaller number.
"""
- return 5
-
+ raise NotImplementedError
def _repr_(self):
"""
Ensure that it says what we think it says::
- sage: JordanSpinEJA(2, field=QQ)
- Euclidean Jordan algebra of dimension 2 over Rational Field
+ 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
def product_on_basis(self, i, j):
return self._multiplication_table[i][j]
- def _a_regular_element(self):
- """
- Guess a regular element. Needed to compute the basis for our
- characteristic polynomial coefficients.
-
- SETUP::
-
- sage: from mjo.eja.eja_algebra import random_eja
-
- TESTS:
-
- Ensure that this hacky method succeeds for every algebra that we
- know how to construct::
-
- sage: set_random_seed()
- sage: J = random_eja()
- sage: J._a_regular_element().is_regular()
- True
+ def _is_commutative(self):
+ r"""
+ Whether or not this algebra's multiplication table 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.
"""
- gs = self.gens()
- z = self.sum( (i+1)*gs[i] for i in range(len(gs)) )
- if not z.is_regular():
- raise ValueError("don't know a regular element")
- return z
-
-
- @cached_method
- def _charpoly_basis_space(self):
- """
- Return the vector space spanned by the basis used in our
- characteristic polynomial coefficients. This is used not only to
- compute those coefficients, but also any time we need to
- evaluate the coefficients (like when we compute the trace or
- determinant).
- """
- z = self._a_regular_element()
- # Don't use the parent vector space directly here in case this
- # happens to be a subalgebra. In that case, we would be e.g.
- # two-dimensional but span_of_basis() would expect three
- # coordinates.
- V = VectorSpace(self.base_ring(), self.vector_space().dimension())
- basis = [ (z**k).to_vector() for k in range(self.rank()) ]
- V1 = V.span_of_basis( basis )
- b = (V1.basis() + V1.complement().basis())
- return V.span_of_basis(b)
-
-
-
- @cached_method
- def _charpoly_coeff(self, i):
- """
- Return the coefficient polynomial "a_{i}" of this algebra's
- general characteristic polynomial.
-
- Having this be a separate cached method lets us compute and
- store the trace/determinant (a_{r-1} and a_{0} respectively)
- separate from the entire characteristic polynomial.
- """
- if self._basis_normalizers is not None:
- # Must be a matrix class?
- # WARNING/TODO: this whole mess is mis-designed.
- n = self.natural_basis_space().nrows()
- field = self.base_ring().base_ring() # yeeeeaaaahhh
- J = self.__class__(n, field, False)
- (_,x,_,_) = J._charpoly_matrix_system()
- p = J._charpoly_coeff(i)
- # p might be missing some vars, have to substitute "optionally"
- pairs = zip(x.base_ring().gens(), self._basis_normalizers)
- substitutions = { v: v*c for (v,c) in pairs }
- return p.subs(substitutions)
-
- (A_of_x, x, xr, detA) = self._charpoly_matrix_system()
- R = A_of_x.base_ring()
- if i >= self.rank():
- # Guaranteed by theory
- return R.zero()
-
- # Danger: the in-place modification is done for performance
- # reasons (reconstructing a matrix with huge polynomial
- # entries is slow), but I don't know how cached_method works,
- # so it's highly possible that we're modifying some global
- # list variable by reference, here. In other words, you
- # probably shouldn't call this method twice on the same
- # algebra, at the same time, in two threads
- Ai_orig = A_of_x.column(i)
- A_of_x.set_column(i,xr)
- numerator = A_of_x.det()
- A_of_x.set_column(i,Ai_orig)
-
- # We're relying on the theory here to ensure that each a_i is
- # indeed back in R, and the added negative signs are to make
- # the whole charpoly expression sum to zero.
- return R(-numerator/detA)
-
-
- @cached_method
- def _charpoly_matrix_system(self):
+ return all( self.product_on_basis(i,j) == self.product_on_basis(i,j)
+ for i in range(self.dimension())
+ for j in range(self.dimension()) )
+
+ 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.
"""
- 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.
+ return all( (self.monomial(i)**2)*(self.monomial(i)*self.monomial(j))
+ ==
+ (self.monomial(i))*((self.monomial(i)**2)*self.monomial(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 multiplication table.
"""
- r = self.rank()
- n = self.dimension()
- # Turn my vector space into a module so that "vectors" can
- # have multivatiate polynomial entries.
- names = tuple('X' + str(i) for i in range(1,n+1))
- R = PolynomialRing(self.base_ring(), names)
-
- # Using change_ring() on the parent's vector space doesn't work
- # here because, in a subalgebra, that vector space has a basis
- # and change_ring() tries to bring the basis along with it. And
- # that doesn't work unless the new ring is a PID, which it usually
- # won't be.
- V = FreeModule(R,n)
-
- # Now let x = (X1,X2,...,Xn) be the vector whose entries are
- # indeterminates...
- x = V(names)
-
- # And figure out the "left multiplication by x" matrix in
- # that setting.
- lmbx_cols = []
- monomial_matrices = [ self.monomial(i).operator().matrix()
- for i in range(n) ] # don't recompute these!
- for k in range(n):
- ek = self.monomial(k).to_vector()
- lmbx_cols.append(
- sum( x[i]*(monomial_matrices[i]*ek)
- for i in range(n) ) )
- Lx = matrix.column(R, lmbx_cols)
-
- # Now we can compute powers of x "symbolically"
- x_powers = [self.one().to_vector(), x]
- for d in range(2, r+1):
- x_powers.append( Lx*(x_powers[-1]) )
-
- idmat = matrix.identity(R, n)
-
- W = self._charpoly_basis_space()
- W = W.change_ring(R.fraction_field())
-
- # Starting with the standard coordinates x = (X1,X2,...,Xn)
- # and then converting the entries to W-coordinates allows us
- # to pass in the standard coordinates to the charpoly and get
- # back the right answer. Specifically, with x = (X1,X2,...,Xn),
- # we have
- #
- # W.coordinates(x^2) eval'd at (standard z-coords)
- # =
- # W-coords of (z^2)
- # =
- # W-coords of (standard coords of x^2 eval'd at std-coords of z)
- #
- # We want the middle equivalent thing in our matrix, but use
- # the first equivalent thing instead so that we can pass in
- # standard coordinates.
- x_powers = [ W.coordinate_vector(xp) for xp in x_powers ]
- l2 = [idmat.column(k-1) for k in range(r+1, n+1)]
- A_of_x = matrix.column(R, n, (x_powers[:r] + l2))
- return (A_of_x, x, x_powers[r], A_of_x.det())
+ # Used to check whether or not something is zero in an inexact
+ # ring. This number is sufficient to allow the construction of
+ # QuaternionHermitianEJA(2, 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.monomial(i)
+ y = self.monomial(j)
+ z = self.monomial(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
@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().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 sum( a[k]*(t**k) for k in range(len(a)) )
+ return (t**r + sum( a[k]*(t**k) for k in range(r) ))
def inner_product(self, x, y):
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 = J.random_element()
- sage: y = J.random_element()
- sage: z = J.random_element()
+ sage: x,y,z = J.random_elements(3)
sage: (x*y).inner_product(z) == y.inner_product(x*z)
True
SETUP::
- sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
+ sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
+ ....: TrivialEJA)
EXAMPLES::
sage: J = ComplexHermitianEJA(3)
sage: J.is_trivial()
False
- sage: A = J.zero().subalgebra_generated_by()
- sage: A.is_trivial()
+
+ ::
+
+ sage: J = TrivialEJA()
+ sage: J.is_trivial()
True
"""
Finite family {0: e0, 1: e1, 2: e2}
sage: J.natural_basis()
(
- [1 0] [ 0 1/2*sqrt2] [0 0]
- [0 0], [1/2*sqrt2 0], [0 1]
+ [1 0] [ 0 0.7071067811865475?] [0 0]
+ [0 0], [0.7071067811865475? 0], [0 1]
)
::
"""
Return the matrix space in which this algebra's natural basis
elements live.
+
+ 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
+ natural basis space (empty matrices) can be multiplied.
"""
- if self._natural_basis is None or len(self._natural_basis) == 0:
+ if self.is_trivial():
+ return MatrixSpace(self.base_ring(), 0)
+ elif self._natural_basis is None or len(self._natural_basis) == 0:
return MatrixSpace(self.base_ring(), self.dimension(), 1)
else:
return self._natural_basis[0].matrix_space()
SETUP::
- sage: from mjo.eja.eja_algebra import (RealCartesianProductEJA,
+ sage: from mjo.eja.eja_algebra import (HadamardEJA,
....: random_eja)
EXAMPLES::
- sage: J = RealCartesianProductEJA(5)
+ sage: J = HadamardEJA(5)
sage: J.one()
e0 + e1 + e2 + e3 + e4
# appeal to the "long vectors" isometry.
oper_vecs = [ _mat2vec(g.operator().matrix()) for g in self.gens() ]
- # Now we use basis linear algebra to find the coefficients,
+ # 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.column(self.base_ring(), oper_vecs)
+ 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()) )
- # Now if there's an identity element in the algebra, this should work.
- coeffs = A.solve_right(b)
- return self.linear_combination(zip(self.gens(), coeffs))
+ # 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))
- def random_element(self):
- # Temporary workaround for https://trac.sagemath.org/ticket/28327
- if self.is_trivial():
- return self.zero()
- else:
- s = super(FiniteDimensionalEuclideanJordanAlgebra, self)
- return s.random_element()
+ def peirce_decomposition(self, c):
+ """
+ The Peirce decomposition of this algebra relative to the
+ idempotent ``c``.
+ In the future, this can be extended to a complete system of
+ orthogonal idempotents.
+
+ INPUT:
+
+ - ``c`` -- an idempotent of this algebra.
+
+ OUTPUT:
+
+ A triple (J0, J5, J1) containing two subalgebras and one subspace
+ of this algebra,
+
+ - ``J0`` -- the algebra on the eigenspace of ``c.operator()``
+ corresponding to the eigenvalue zero.
+
+ - ``J5`` -- the eigenspace (NOT a subalgebra) of ``c.operator()``
+ corresponding to the eigenvalue one-half.
+
+ - ``J1`` -- the algebra on the eigenspace of ``c.operator()``
+ corresponding to the eigenvalue one.
+
+ These are the only possible eigenspaces for that operator, and this
+ algebra is a direct sum of them. The spaces ``J0`` and ``J1`` are
+ orthogonal, and are subalgebras of this algebra with the appropriate
+ restrictions.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import random_eja, RealSymmetricEJA
+
+ EXAMPLES:
+
+ The canonical example comes from the symmetric matrices, which
+ decompose into diagonal and off-diagonal parts::
+
+ sage: J = RealSymmetricEJA(3)
+ sage: C = matrix(QQ, [ [1,0,0],
+ ....: [0,1,0],
+ ....: [0,0,0] ])
+ sage: c = J(C)
+ sage: J0,J5,J1 = J.peirce_decomposition(c)
+ sage: J0
+ Euclidean Jordan algebra of dimension 1...
+ sage: J5
+ Vector space of degree 6 and dimension 2...
+ sage: J1
+ Euclidean Jordan algebra of dimension 3...
+ sage: J0.one().natural_representation()
+ [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]).natural_representation()
+ [ 0 0 1/2*sqrt(2)]
+ [ 0 0 0]
+ [1/2*sqrt(2) 0 0]
+ sage: J.from_vector(J5.basis()[1]).natural_representation()
+ [ 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().natural_representation()
+ [1 0 0]
+ [0 1 0]
+ [0 0 0]
+
+ TESTS:
+
+ Every algebra decomposes trivially with respect to its identity
+ element::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: J0,J5,J1 = J.peirce_decomposition(J.one())
+ sage: J0.dimension() == 0 and J5.dimension() == 0
+ True
+ sage: J1.superalgebra() == J and J1.dimension() == J.dimension()
+ True
+
+ The decomposition is into eigenspaces, and its components are
+ therefore necessarily orthogonal. Moreover, the identity
+ elements in the two subalgebras are the projections onto their
+ respective subspaces of the superalgebra's identity element::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: x = J.random_element()
+ sage: if not J.is_trivial():
+ ....: while x.is_nilpotent():
+ ....: x = J.random_element()
+ sage: c = x.subalgebra_idempotent()
+ sage: J0,J5,J1 = J.peirce_decomposition(c)
+ sage: ipsum = 0
+ sage: for (w,y,z) in zip(J0.basis(), J5.basis(), J1.basis()):
+ ....: w = w.superalgebra_element()
+ ....: y = J.from_vector(y)
+ ....: z = z.superalgebra_element()
+ ....: ipsum += w.inner_product(y).abs()
+ ....: ipsum += w.inner_product(z).abs()
+ ....: ipsum += y.inner_product(z).abs()
+ sage: ipsum
+ 0
+ sage: J1(c) == J1.one()
+ True
+ sage: J0(J.one() - c) == J0.one()
+ True
- def rank(self):
"""
- Return the rank of this EJA.
+ if not c.is_idempotent():
+ raise ValueError("element is not idempotent: %s" % c)
+
+ # Default these to what they should be if they turn out to be
+ # trivial, because eigenspaces_left() won't return eigenvalues
+ # corresponding to trivial spaces (e.g. it returns only the
+ # eigenspace corresponding to lambda=1 if you take the
+ # decomposition relative to the identity element).
+ trivial = FiniteDimensionalEuclideanJordanSubalgebra(self, ())
+ J0 = trivial # eigenvalue zero
+ J5 = VectorSpace(self.base_ring(), 0) # eigenvalue one-half
+ J1 = trivial # eigenvalue one
+
+ for (eigval, eigspace) in c.operator().matrix().right_eigenspaces():
+ if eigval == ~(self.base_ring()(2)):
+ J5 = eigspace
+ else:
+ gens = tuple( self.from_vector(b) for b in eigspace.basis() )
+ subalg = FiniteDimensionalEuclideanJordanSubalgebra(self,
+ gens,
+ check_axioms=False)
+ if eigval == 0:
+ J0 = subalg
+ elif eigval == 1:
+ J1 = subalg
+ else:
+ raise ValueError("unexpected eigenvalue: %s" % eigval)
+
+ return (J0, J5, J1)
+
+
+ def random_element(self, thorough=False):
+ r"""
+ Return a random element of this algebra.
+
+ Our algebra superclass method only returns a linear
+ combination of at most two basis elements. We instead
+ want the vector space "random element" method that
+ returns a more diverse selection.
+
+ INPUT:
+
+ - ``thorough`` -- (boolean; default False) whether or not we
+ should generate irrational coefficients for the random
+ element when our base ring is irrational; this slows the
+ algebra operations to a crawl, but any truly random method
+ should include them
+
+ """
+ # For a general base ring... maybe we can trust this to do the
+ # right thing? Unlikely, but.
+ V = self.vector_space()
+ v = V.random_element()
+
+ if self.base_ring() is AA:
+ # The "random element" method of the algebraic reals is
+ # stupid at the moment, and only returns integers between
+ # -2 and 2, inclusive:
+ #
+ # https://trac.sagemath.org/ticket/30875
+ #
+ # Instead, we implement our own "random vector" method,
+ # and then coerce that into the algebra. We use the vector
+ # space degree here instead of the dimension because a
+ # subalgebra could (for example) be spanned by only two
+ # vectors, each with five coordinates. We need to
+ # generate all five coordinates.
+ if thorough:
+ v *= QQbar.random_element().real()
+ else:
+ v *= QQ.random_element()
+
+ return self.from_vector(V.coordinate_vector(v))
+
+ def random_elements(self, count, thorough=False):
+ """
+ Return ``count`` random elements as a tuple.
- ALGORITHM:
+ INPUT:
- 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.
+ - ``thorough`` -- (boolean; default False) whether or not we
+ should generate irrational coefficients for the random
+ elements when our base ring is irrational; this slows the
+ algebra operations to a crawl, but any truly random method
+ should include them
SETUP::
- sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
+ sage: from mjo.eja.eja_algebra import JordanSpinEJA
+
+ EXAMPLES::
+
+ sage: J = JordanSpinEJA(3)
+ sage: x,y,z = J.random_elements(3)
+ sage: all( [ x in J, y in J, z in J ])
+ True
+ sage: len( J.random_elements(10) ) == 10
+ True
+
+ """
+ return tuple( self.random_element(thorough)
+ for idx in range(count) )
+
+ @classmethod
+ def random_instance(cls, field=AA, **kwargs):
+ """
+ Return a random instance of this type of algebra.
+
+ Beware, this will crash for "most instances" because the
+ constructor below looks wrong.
+ """
+ n = ZZ.random_element(cls._max_random_instance_size() + 1)
+ return cls(n, field, **kwargs)
+
+ @cached_method
+ def _charpoly_coefficients(self):
+ r"""
+ The `r` polynomial coefficients of the "characteristic polynomial
+ of" function.
+ """
+ n = self.dimension()
+ var_names = [ "X" + str(z) for z in range(1,n+1) ]
+ R = PolynomialRing(self.base_ring(), var_names)
+ vars = R.gens()
+ 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.monomial(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.
+ return -A_rref.solve_right(E*b).change_ring(R)[:r]
+
+ @cached_method
+ def rank(self):
+ r"""
+ Return the rank of this EJA.
+
+ This is a cached method because we know the rank a priori for
+ all of the algebras we can construct. Thus we can avoid the
+ expensive ``_charpoly_coefficients()`` call unless we truly
+ need to compute the whole characteristic polynomial.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (HadamardEJA,
+ ....: JordanSpinEJA,
....: RealSymmetricEJA,
....: ComplexHermitianEJA,
....: QuaternionHermitianEJA,
The rank of the `n`-by-`n` Hermitian real, complex, or
quaternion matrices is `n`::
- sage: RealSymmetricEJA(2).rank()
- 2
- sage: ComplexHermitianEJA(2).rank()
- 2
+ sage: RealSymmetricEJA(4).rank()
+ 4
+ sage: ComplexHermitianEJA(3).rank()
+ 3
sage: QuaternionHermitianEJA(2).rank()
2
- sage: RealSymmetricEJA(5).rank()
- 5
- sage: ComplexHermitianEJA(5).rank()
- 5
- sage: QuaternionHermitianEJA(5).rank()
- 5
TESTS:
Ensure that every EJA that we know how to construct has a
- positive integer rank::
+ positive integer rank, unless the algebra is trivial in
+ which case its rank will be zero::
sage: set_random_seed()
- sage: r = random_eja().rank()
- sage: r in ZZ and r > 0
+ sage: J = random_eja()
+ sage: r = J.rank()
+ sage: r in ZZ
+ True
+ sage: r > 0 or (r == 0 and J.is_trivial())
True
+ Ensure that computing the rank actually works, since the ranks
+ of all simple algebras are known and will be cached by default::
+
+ sage: J = HadamardEJA(4)
+ sage: J.rank.clear_cache()
+ sage: J.rank()
+ 4
+
+ ::
+
+ sage: J = JordanSpinEJA(4)
+ sage: J.rank.clear_cache()
+ sage: J.rank()
+ 2
+
+ ::
+
+ sage: J = RealSymmetricEJA(3)
+ sage: J.rank.clear_cache()
+ sage: J.rank()
+ 3
+
+ ::
+
+ sage: J = ComplexHermitianEJA(2)
+ sage: J.rank.clear_cache()
+ sage: J.rank()
+ 2
+
+ ::
+
+ sage: J = QuaternionHermitianEJA(2)
+ sage: J.rank.clear_cache()
+ sage: J.rank()
+ 2
"""
- return self._rank
+ return len(self._charpoly_coefficients())
def vector_space(self):
Element = FiniteDimensionalEuclideanJordanAlgebraElement
-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.
+def random_eja(field=AA):
+ """
+ Return a "random" finite-dimensional Euclidean Jordan Algebra.
SETUP::
- sage: from mjo.eja.eja_algebra import RealCartesianProductEJA
-
- EXAMPLES:
+ sage: from mjo.eja.eja_algebra import random_eja
- This multiplication table can be verified by hand::
+ TESTS::
- 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
+ sage: random_eja()
+ Euclidean Jordan algebra of dimension...
- TESTS:
+ """
+ classname = choice([TrivialEJA,
+ HadamardEJA,
+ JordanSpinEJA,
+ RealSymmetricEJA,
+ ComplexHermitianEJA,
+ QuaternionHermitianEJA])
+ return classname.random_instance(field=field)
- We can change the generator prefix::
- sage: RealCartesianProductEJA(3, prefix='r').gens()
- (r0, r1, r2)
- Our inner product satisfies the Jordan axiom::
- sage: set_random_seed()
- sage: n = ZZ.random_element(1,5)
- sage: J = RealCartesianProductEJA(n)
- sage: x = J.random_element()
- sage: y = J.random_element()
- sage: z = J.random_element()
- sage: (x*y).inner_product(z) == y.inner_product(x*z)
- True
+class RationalBasisEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
+ r"""
+ Algebras whose basis consists of vectors with rational
+ entries. Equivalently, algebras whose multiplication tables
+ contain only rational coefficients.
+ When an EJA has a basis that can be made rational, we can speed up
+ the computation of its characteristic polynomial by doing it over
+ ``QQ``. All of the named EJA constructors that we provide fall
+ into this category.
"""
- def __init__(self, n, field=QQ, **kwargs):
- V = VectorSpace(field, n)
- mult_table = [ [ V.gen(i)*(i == j) for j in range(n) ]
- for i in range(n) ]
-
- fdeja = super(RealCartesianProductEJA, self)
- return fdeja.__init__(field, mult_table, rank=n, **kwargs)
-
- def inner_product(self, x, y):
- """
- Faster to reimplement than to use natural representations.
+ @cached_method
+ def _charpoly_coefficients(self):
+ r"""
+ Override the parent method with something that tries to compute
+ over a faster (non-extension) field.
SETUP::
- sage: from mjo.eja.eja_algebra import RealCartesianProductEJA
+ sage: from mjo.eja.eja_algebra import JordanSpinEJA
- TESTS:
+ EXAMPLES:
- Ensure that this is the usual inner product for the algebras
- over `R^n`::
+ The base ring of the resulting polynomial coefficients is what
+ it should be, and not the rationals (unless the algebra was
+ already over the rationals)::
- sage: set_random_seed()
- sage: n = ZZ.random_element(1,5)
- sage: J = RealCartesianProductEJA(n)
- sage: x = J.random_element()
- sage: y = J.random_element()
- sage: X = x.natural_representation()
- sage: Y = y.natural_representation()
- sage: x.inner_product(y) == J.natural_inner_product(X,Y)
- True
+ 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
"""
- return x.to_vector().inner_product(y.to_vector())
-
+ if self.base_ring() is QQ:
+ # There's no need to construct *another* algebra over the
+ # rationals if this one is already over the rationals.
+ superclass = super(RationalBasisEuclideanJordanAlgebra, self)
+ return superclass._charpoly_coefficients()
+
+ mult_table = tuple(
+ map(lambda x: x.to_vector(), ls)
+ for ls in self._multiplication_table
+ )
+
+ # Do the computation over the rationals. The answer will be
+ # the same, because our basis coordinates are (essentially)
+ # rational.
+ J = FiniteDimensionalEuclideanJordanAlgebra(QQ,
+ mult_table,
+ check_field=False,
+ check_axioms=False)
+ a = J._charpoly_coefficients()
+ return tuple(map(lambda x: x.change_ring(self.base_ring()), a))
+
+
+class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
+ @staticmethod
+ def _max_random_instance_size():
+ # Play it safe, since this will be squared and the underlying
+ # field can have dimension 4 (quaternions) too.
+ return 2
-def random_eja():
- """
- Return a "random" finite-dimensional Euclidean Jordan Algebra.
+ def __init__(self, field, basis, normalize_basis=True, **kwargs):
+ """
+ Compared to the superclass constructor, we take a basis instead of
+ a multiplication table because the latter can be computed in terms
+ of the former when the product is known (like it is here).
+ """
+ # Used in this class's fast _charpoly_coefficients() override.
+ self._basis_normalizers = None
- ALGORITHM:
+ # We're going to loop through this a few times, so now's a good
+ # time to ensure that it isn't a generator expression.
+ basis = tuple(basis)
- For now, we choose a random natural number ``n`` (greater than zero)
- and then give you back one of the following:
+ if len(basis) > 1 and normalize_basis:
+ # We'll need sqrt(2) to normalize the basis, and this
+ # winds up in the multiplication table, so the whole
+ # algebra needs to be over the field extension.
+ R = PolynomialRing(field, 'z')
+ z = R.gen()
+ p = z**2 - 2
+ if p.is_irreducible():
+ field = field.extension(p, 'sqrt2', embedding=RLF(2).sqrt())
+ basis = tuple( s.change_ring(field) for s in basis )
+ self._basis_normalizers = tuple(
+ ~(self.natural_inner_product(s,s).sqrt()) for s in basis )
+ basis = tuple(s*c for (s,c) in zip(basis,self._basis_normalizers))
- * The cartesian product of the rational numbers ``n`` times; this is
- ``QQ^n`` with the Hadamard product.
+ Qs = self.multiplication_table_from_matrix_basis(basis)
- * The Jordan spin algebra on ``QQ^n``.
+ super(MatrixEuclideanJordanAlgebra, self).__init__(field,
+ Qs,
+ natural_basis=basis,
+ **kwargs)
- * The ``n``-by-``n`` rational symmetric matrices with the symmetric
- product.
- * The ``n``-by-``n`` complex-rational Hermitian matrices embedded
- in the space of ``2n``-by-``2n`` real symmetric matrices.
+ @cached_method
+ def _charpoly_coefficients(self):
+ r"""
+ Override the parent method with something that tries to compute
+ over a faster (non-extension) field.
+ """
+ if self._basis_normalizers is None or self.base_ring() is QQ:
+ # We didn't normalize, or the basis we started with had
+ # entries in a nice field already. Just compute the thing.
+ return super(MatrixEuclideanJordanAlgebra, self)._charpoly_coefficients()
+
+ basis = ( (b/n) for (b,n) in zip(self.natural_basis(),
+ self._basis_normalizers) )
+
+ # Do this over the rationals and convert back at the end.
+ # Only works because we know the entries of the basis are
+ # integers. The argument ``check_axioms=False`` is required
+ # because the trace inner-product method for this
+ # class is a stub and can't actually be checked.
+ J = MatrixEuclideanJordanAlgebra(QQ,
+ basis,
+ normalize_basis=False,
+ check_field=False,
+ check_axioms=False)
+ a = J._charpoly_coefficients()
+
+ # Unfortunately, changing the basis does change the
+ # coefficients of the characteristic polynomial, but since
+ # these are really the coefficients of the "characteristic
+ # polynomial of" function, everything is still nice and
+ # unevaluated. It's therefore "obvious" how scaling the
+ # basis affects the coordinate variables X1, X2, et
+ # cetera. Scaling the first basis vector up by "n" adds a
+ # factor of 1/n into every "X1" term, for example. So here
+ # we simply undo the basis_normalizer scaling that we
+ # performed earlier.
+ #
+ # The a[0] access here is safe because trivial algebras
+ # won't have any basis normalizers and therefore won't
+ # make it to this "else" branch.
+ XS = a[0].parent().gens()
+ subs_dict = { XS[i]: self._basis_normalizers[i]*XS[i]
+ for i in range(len(XS)) }
+ return tuple( a_i.subs(subs_dict) for a_i in a )
- * The ``n``-by-``n`` quaternion-rational Hermitian matrices embedded
- in the space of ``4n``-by-``4n`` real symmetric matrices.
- Later this might be extended to return Cartesian products of the
- EJAs above.
-
- SETUP::
-
- sage: from mjo.eja.eja_algebra import random_eja
-
- TESTS::
-
- sage: random_eja()
- Euclidean Jordan algebra of dimension...
-
- """
- constructor = choice([RealCartesianProductEJA,
- JordanSpinEJA,
- RealSymmetricEJA,
- ComplexHermitianEJA,
- QuaternionHermitianEJA])
- n = ZZ.random_element(1, constructor._max_test_case_size())
- return constructor(n, field=QQ)
-
-
-
-def _real_symmetric_basis(n, field):
- """
- Return a basis for the space of real symmetric n-by-n matrices.
-
- SETUP::
-
- sage: from mjo.eja.eja_algebra import _real_symmetric_basis
-
- TESTS::
-
- sage: set_random_seed()
- sage: n = ZZ.random_element(1,5)
- sage: B = _real_symmetric_basis(n, QQ)
- 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 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:
- Sij = Eij + Eij.transpose()
- S.append(Sij)
- return tuple(S)
-
-
-def _complex_hermitian_basis(n, field):
- """
- Returns a basis for the space of complex Hermitian n-by-n matrices.
-
- 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.
-
- SETUP::
-
- sage: from mjo.eja.eja_algebra import _complex_hermitian_basis
-
- TESTS::
-
- sage: set_random_seed()
- sage: n = ZZ.random_element(1,5)
- sage: field = QuadraticField(2, 'sqrt2')
- sage: B = _complex_hermitian_basis(n, field)
- sage: all( M.is_symmetric() for M in B)
- True
-
- """
- R = PolynomialRing(field, 'z')
- z = R.gen()
- F = NumberField(z**2 + 1, 'I', embedding=CLF(-1).sqrt())
- 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(F, n, lambda k,l: k==i and l==j)
- if i == j:
- Sij = _embed_complex_matrix(Eij)
- S.append(Sij)
- else:
- # 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)
-
- # 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 _quaternion_hermitian_basis(n, field):
- """
- Returns a basis for the space of quaternion Hermitian n-by-n matrices.
-
- 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.
-
- SETUP::
-
- sage: from mjo.eja.eja_algebra import _quaternion_hermitian_basis
-
- TESTS::
-
- sage: set_random_seed()
- sage: n = ZZ.random_element(1,5)
- sage: B = _quaternion_hermitian_basis(n, QQ)
- sage: all( M.is_symmetric() for M in B )
- True
-
- """
- Q = QuaternionAlgebra(QQ,-1,-1)
- I,J,K = Q.gens()
-
- # This is like the symmetric case, but we need to be careful:
- #
- # * We want conjugate-symmetry, not just symmetry.
- # * The diagonal will (as a result) be real.
- #
- S = []
- for i in xrange(n):
- for j in xrange(i+1):
- Eij = matrix(Q, n, lambda k,l: k==i and l==j)
- if i == j:
- Sij = _embed_quaternion_matrix(Eij)
- S.append(Sij)
- else:
- # Beware, orthogonal but not normalized! The second,
- # third, and fourth ones have a minus because they're
- # conjugated.
- Sij_real = _embed_quaternion_matrix(Eij + Eij.transpose())
- S.append(Sij_real)
- Sij_I = _embed_quaternion_matrix(I*Eij - I*Eij.transpose())
- S.append(Sij_I)
- Sij_J = _embed_quaternion_matrix(J*Eij - J*Eij.transpose())
- S.append(Sij_J)
- Sij_K = _embed_quaternion_matrix(K*Eij - K*Eij.transpose())
- S.append(Sij_K)
- return tuple(S)
-
-
-
-def _multiplication_table_from_matrix_basis(basis):
- """
- At least three of the five simple Euclidean Jordan algebras have the
- symmetric multiplication (A,B) |-> (AB + BA)/2, where the
- multiplication on the right is matrix multiplication. Given a basis
- for the underlying matrix space, this function returns a
- multiplication table (obtained by looping through the basis
- elements) for an algebra of those matrices.
- """
- # In S^2, for example, we nominally have four coordinates even
- # though the space is of dimension three only. The vector space V
- # is supposed to hold the entire long vector, and the subspace W
- # of V will be spanned by the vectors that arise from symmetric
- # matrices. Thus for S^2, dim(V) == 4 and dim(W) == 3.
- field = basis[0].base_ring()
- dimension = basis[0].nrows()
-
- V = VectorSpace(field, dimension**2)
- W = V.span_of_basis( _mat2vec(s) for s in basis )
- n = len(basis)
- mult_table = [[W.zero() for j in range(n)] for i in range(n)]
- for i in range(n):
- for j in range(n):
- mat_entry = (basis[i]*basis[j] + basis[j]*basis[i])/2
- mult_table[i][j] = W.coordinate_vector(_mat2vec(mat_entry))
-
- return mult_table
-
-
-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::
-
- sage: from mjo.eja.eja_algebra import _embed_complex_matrix
-
- EXAMPLES::
-
- sage: F = QuadraticField(-1, 'i')
- sage: x1 = F(4 - 2*i)
- sage: x2 = F(1 + 2*i)
- sage: x3 = F(-i)
- sage: x4 = F(6)
- sage: M = matrix(F,2,[[x1,x2],[x3,x4]])
- sage: _embed_complex_matrix(M)
- [ 4 -2| 1 2]
- [ 2 4|-2 1]
- [-----+-----]
- [ 0 -1| 6 0]
- [ 1 0| 0 6]
-
- TESTS:
-
- Embedding is a homomorphism (isomorphism, in fact)::
-
- sage: set_random_seed()
- sage: n = ZZ.random_element(5)
- sage: F = QuadraticField(-1, 'i')
- sage: X = random_matrix(F, n)
- sage: Y = random_matrix(F, n)
- sage: actual = _embed_complex_matrix(X) * _embed_complex_matrix(Y)
- sage: expected = _embed_complex_matrix(X*Y)
- sage: actual == expected
- True
-
- """
- n = M.nrows()
- if M.ncols() != n:
- raise ValueError("the matrix 'M' must be square")
- field = M.base_ring()
- blocks = []
- for z in M.list():
- a = z.vector()[0] # real part, I guess
- b = z.vector()[1] # imag part, I guess
- blocks.append(matrix(field, 2, [[a,b],[-b,a]]))
-
- # We can drop the imaginaries here.
- return matrix.block(field.base_ring(), n, blocks)
-
-
-def _unembed_complex_matrix(M):
- """
- The inverse of _embed_complex_matrix().
-
- SETUP::
-
- sage: from mjo.eja.eja_algebra import (_embed_complex_matrix,
- ....: _unembed_complex_matrix)
-
- EXAMPLES::
-
- sage: A = matrix(QQ,[ [ 1, 2, 3, 4],
- ....: [-2, 1, -4, 3],
- ....: [ 9, 10, 11, 12],
- ....: [-10, 9, -12, 11] ])
- sage: _unembed_complex_matrix(A)
- [ 2*i + 1 4*i + 3]
- [ 10*i + 9 12*i + 11]
-
- TESTS:
-
- Unembedding is the inverse of embedding::
-
- sage: set_random_seed()
- sage: F = QuadraticField(-1, 'i')
- sage: M = random_matrix(F, 3)
- sage: _unembed_complex_matrix(_embed_complex_matrix(M)) == M
- True
+ @staticmethod
+ def multiplication_table_from_matrix_basis(basis):
+ """
+ At least three of the five simple Euclidean Jordan algebras have the
+ symmetric multiplication (A,B) |-> (AB + BA)/2, where the
+ multiplication on the right is matrix multiplication. Given a basis
+ for the underlying matrix space, this function returns a
+ multiplication table (obtained by looping through the basis
+ elements) for an algebra of those matrices.
+ """
+ # In S^2, for example, we nominally have four coordinates even
+ # though the space is of dimension three only. The vector space V
+ # is supposed to hold the entire long vector, and the subspace W
+ # of V will be spanned by the vectors that arise from symmetric
+ # matrices. Thus for S^2, dim(V) == 4 and dim(W) == 3.
+ if len(basis) == 0:
+ return []
+
+ field = basis[0].base_ring()
+ dimension = basis[0].nrows()
+
+ V = VectorSpace(field, dimension**2)
+ W = V.span_of_basis( _mat2vec(s) for s in basis )
+ n = len(basis)
+ mult_table = [[W.zero() for j in range(n)] for i in range(n)]
+ for i in range(n):
+ for j in range(n):
+ mat_entry = (basis[i]*basis[j] + basis[j]*basis[i])/2
+ mult_table[i][j] = W.coordinate_vector(_mat2vec(mat_entry))
- """
- 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")
-
- field = M.base_ring() # This should already have sqrt2
- R = PolynomialRing(field, 'z')
- z = R.gen()
- F = NumberField(z**2 + 1,'i', embedding=CLF(-1).sqrt())
- 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.
+ return mult_table
- SETUP::
- sage: from mjo.eja.eja_algebra import _embed_quaternion_matrix
+ @staticmethod
+ def real_embed(M):
+ """
+ Embed the matrix ``M`` into a space of real matrices.
- EXAMPLES::
+ 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.
- 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]
+ real_embed(M*N) = real_embed(M)*real_embed(N)
- Embedding is a homomorphism (isomorphism, in fact)::
+ """
+ raise NotImplementedError
- 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().
+ @staticmethod
+ def real_unembed(M):
+ """
+ The inverse of :meth:`real_embed`.
+ """
+ raise NotImplementedError
- SETUP::
- sage: from mjo.eja.eja_algebra import (_embed_quaternion_matrix,
- ....: _unembed_quaternion_matrix)
+ @classmethod
+ def natural_inner_product(cls,X,Y):
+ Xu = cls.real_unembed(X)
+ Yu = cls.real_unembed(Y)
+ tr = (Xu*Yu).trace()
- EXAMPLES::
+ 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]
- sage: M = matrix(QQ, [[ 1, 2, 3, 4],
- ....: [-2, 1, -4, 3],
- ....: [-3, 4, 1, -2],
- ....: [-4, -3, 2, 1]])
- sage: _unembed_quaternion_matrix(M)
- [1 + 2*i + 3*j + 4*k]
- TESTS:
+class RealMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
+ @staticmethod
+ def real_embed(M):
+ """
+ The identity function, for embedding real matrices into real
+ matrices.
+ """
+ return M
- Unembedding is the inverse of embedding::
+ @staticmethod
+ def real_unembed(M):
+ """
+ The identity function, for unembedding real matrices from real
+ matrices.
+ """
+ return M
- sage: set_random_seed()
- sage: Q = QuaternionAlgebra(QQ, -1, -1)
- sage: M = random_matrix(Q, 3)
- sage: _unembed_quaternion_matrix(_embed_quaternion_matrix(M)) == M
- True
- """
- n = ZZ(M.nrows())
- if M.ncols() != n:
- raise ValueError("the matrix 'M' must be square")
- if not n.mod(4).is_zero():
- raise ValueError("the matrix 'M' must be a complex embedding")
-
- # Use the base ring of the matrix to ensure that its entries can be
- # multiplied by elements of the quaternion algebra.
- field = M.base_ring()
- Q = QuaternionAlgebra(field,-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].vector()[0] # real part
- z += submat[0,0].vector()[1]*i # imag part
- z += submat[0,1].vector()[0]*j # real part
- z += submat[0,1].vector()[1]*k # imag part
- elements.append(z)
-
- return matrix(Q, n/4, elements)
-
-
-# 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):
+class RealSymmetricEJA(RealMatrixEuclideanJordanAlgebra):
"""
The rank-n simple EJA consisting of real symmetric n-by-n
matrices, the usual symmetric Jordan product, and the trace inner
sage: e2*e2
e2
+ In theory, our "field" can be any subfield of the reals::
+
+ sage: RealSymmetricEJA(2, RDF)
+ Euclidean Jordan algebra of dimension 3 over Real Double Field
+ sage: RealSymmetricEJA(2, RR)
+ Euclidean Jordan algebra of dimension 3 over Real Field with
+ 53 bits of precision
+
TESTS:
The dimension of this algebra is `(n^2 + n) / 2`::
sage: set_random_seed()
- sage: n = ZZ.random_element(1,5)
+ 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
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: J = RealSymmetricEJA.random_instance()
+ sage: x,y = J.random_elements(2)
sage: actual = (x*y).natural_representation()
sage: X = x.natural_representation()
sage: Y = y.natural_representation()
sage: RealSymmetricEJA(3, prefix='q').gens()
(q0, q1, q2, q3, q4, q5)
- Our inner product satisfies the Jordan axiom::
-
- 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: z = J.random_element()
- sage: (x*y).inner_product(z) == y.inner_product(x*z)
- True
-
Our natural basis is normalized with respect to the natural inner
product unless we specify otherwise::
sage: set_random_seed()
- sage: n = ZZ.random_element(1,5)
- sage: J = RealSymmetricEJA(n)
+ sage: J = RealSymmetricEJA.random_instance()
sage: all( b.norm() == 1 for b in J.gens() )
True
the operator is self-adjoint by the Jordan axiom::
sage: set_random_seed()
- sage: n = ZZ.random_element(1,5)
- sage: x = RealSymmetricEJA(n).random_element()
+ sage: x = RealSymmetricEJA.random_instance().random_element()
sage: x.operator().matrix().is_symmetric()
True
+ We can construct the (trivial) algebra of rank zero::
+
+ sage: RealSymmetricEJA(0)
+ Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
+
"""
- def __init__(self, n, field=QQ, normalize_basis=True, **kwargs):
- S = _real_symmetric_basis(n, field)
+ @classmethod
+ def _denormalized_basis(cls, n, field):
+ """
+ Return a basis for the space of real symmetric n-by-n matrices.
- if n > 1 and normalize_basis:
- # We'll need sqrt(2) to normalize the basis, and this
- # winds up in the multiplication table, so the whole
- # algebra needs to be over the field extension.
- R = PolynomialRing(field, 'z')
- z = R.gen()
- p = z**2 - 2
- if p.is_irreducible():
- field = NumberField(p, 'sqrt2', embedding=RLF(2).sqrt())
- S = [ s.change_ring(field) for s in S ]
- self._basis_normalizers = tuple(
- ~(self.natural_inner_product(s,s).sqrt()) for s in S )
- S = tuple( s*c for (s,c) in zip(S,self._basis_normalizers) )
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import RealSymmetricEJA
+
+ TESTS::
+
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,5)
+ sage: B = RealSymmetricEJA._denormalized_basis(n,QQ)
+ 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(field, n, lambda k,l: k==i and l==j)
+ if i == j:
+ Sij = Eij
+ else:
+ Sij = Eij + Eij.transpose()
+ S.append(Sij)
+ return S
+
+
+ @staticmethod
+ def _max_random_instance_size():
+ return 4 # Dimension 10
- Qs = _multiplication_table_from_matrix_basis(S)
- fdeja = super(RealSymmetricEJA, self)
- return fdeja.__init__(field,
- Qs,
- rank=n,
- natural_basis=S,
- **kwargs)
+ def __init__(self, n, field=AA, **kwargs):
+ basis = self._denormalized_basis(n, field)
+ super(RealSymmetricEJA, self).__init__(field,
+ basis,
+ check_axioms=False,
+ **kwargs)
+ self.rank.set_cache(n)
+
+class ComplexMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
@staticmethod
- def _max_test_case_size():
- return 5
+ def real_embed(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::
+
+ sage: from mjo.eja.eja_algebra import \
+ ....: ComplexMatrixEuclideanJordanAlgebra
+
+ 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: ComplexMatrixEuclideanJordanAlgebra.real_embed(M)
+ [ 4 -2| 1 2]
+ [ 2 4|-2 1]
+ [-----+-----]
+ [ 0 -1| 6 0]
+ [ 1 0| 0 6]
+
+ TESTS:
+
+ Embedding is a homomorphism (isomorphism, in fact)::
+
+ sage: set_random_seed()
+ sage: n_max = ComplexMatrixEuclideanJordanAlgebra._max_random_instance_size()
+ sage: n = ZZ.random_element(n_max)
+ sage: F = QuadraticField(-1, 'I')
+ sage: X = random_matrix(F, n)
+ sage: Y = random_matrix(F, n)
+ sage: Xe = ComplexMatrixEuclideanJordanAlgebra.real_embed(X)
+ sage: Ye = ComplexMatrixEuclideanJordanAlgebra.real_embed(Y)
+ sage: XYe = ComplexMatrixEuclideanJordanAlgebra.real_embed(X*Y)
+ sage: Xe*Ye == XYe
+ True
+
+ """
+ n = M.nrows()
+ if M.ncols() != n:
+ raise ValueError("the matrix 'M' must be square")
+
+ # We don't need any adjoined elements...
+ field = M.base_ring().base_ring()
+
+ blocks = []
+ for z in M.list():
+ a = z.list()[0] # real part, I guess
+ b = z.list()[1] # imag part, I guess
+ blocks.append(matrix(field, 2, [[a,b],[-b,a]]))
+
+ return matrix.block(field, n, blocks)
+
+
+ @staticmethod
+ def real_unembed(M):
+ """
+ The inverse of _embed_complex_matrix().
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import \
+ ....: ComplexMatrixEuclideanJordanAlgebra
+
+ EXAMPLES::
+
+ sage: A = matrix(QQ,[ [ 1, 2, 3, 4],
+ ....: [-2, 1, -4, 3],
+ ....: [ 9, 10, 11, 12],
+ ....: [-10, 9, -12, 11] ])
+ sage: ComplexMatrixEuclideanJordanAlgebra.real_unembed(A)
+ [ 2*I + 1 4*I + 3]
+ [ 10*I + 9 12*I + 11]
+
+ TESTS:
+
+ Unembedding is the inverse of embedding::
+
+ sage: set_random_seed()
+ sage: F = QuadraticField(-1, 'I')
+ sage: M = random_matrix(F, 3)
+ sage: Me = ComplexMatrixEuclideanJordanAlgebra.real_embed(M)
+ sage: ComplexMatrixEuclideanJordanAlgebra.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(2).is_zero():
+ raise ValueError("the matrix 'M' must be a complex embedding")
+
+ # If "M" was normalized, its base ring might have roots
+ # adjoined and they can stick around after unembedding.
+ field = M.base_ring()
+ R = PolynomialRing(field, 'z')
+ z = R.gen()
+ 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
+ else:
+ F = field.extension(z**2 + 1, 'I', embedding=CLF(-1).sqrt())
+ 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/2):
+ for j in range(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)
+
+
+ @classmethod
+ def natural_inner_product(cls,X,Y):
+ """
+ Compute a natural inner product in this algebra directly from
+ its real embedding.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
+ TESTS:
+
+ This gives the same answer as the slow, default method implemented
+ in :class:`MatrixEuclideanJordanAlgebra`::
+
+ sage: set_random_seed()
+ sage: J = ComplexHermitianEJA.random_instance()
+ sage: x,y = J.random_elements(2)
+ sage: Xe = x.natural_representation()
+ sage: Ye = y.natural_representation()
+ sage: X = ComplexHermitianEJA.real_unembed(Xe)
+ sage: Y = ComplexHermitianEJA.real_unembed(Ye)
+ sage: expected = (X*Y).trace().real()
+ sage: actual = ComplexHermitianEJA.natural_inner_product(Xe,Ye)
+ sage: actual == expected
+ True
+
+ """
+ return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/2
-class ComplexHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra):
+
+class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra):
"""
The rank-n simple EJA consisting of complex Hermitian n-by-n
matrices over the real numbers, the usual symmetric Jordan product,
sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
+ EXAMPLES:
+
+ In theory, our "field" can be any subfield of the reals::
+
+ sage: ComplexHermitianEJA(2, RDF)
+ Euclidean Jordan algebra of dimension 4 over Real Double Field
+ sage: ComplexHermitianEJA(2, RR)
+ Euclidean Jordan algebra of dimension 4 over Real Field with
+ 53 bits of precision
+
TESTS:
The dimension of this algebra is `n^2`::
sage: set_random_seed()
- sage: n = ZZ.random_element(1,5)
+ 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
The Jordan multiplication is what we think it is::
sage: set_random_seed()
- sage: n = ZZ.random_element(1,5)
- sage: J = ComplexHermitianEJA(n)
- sage: x = J.random_element()
- sage: y = J.random_element()
+ sage: J = ComplexHermitianEJA.random_instance()
+ sage: x,y = J.random_elements(2)
sage: actual = (x*y).natural_representation()
sage: X = x.natural_representation()
sage: Y = y.natural_representation()
sage: ComplexHermitianEJA(2, prefix='z').gens()
(z0, z1, z2, z3)
- Our inner product satisfies the Jordan axiom::
-
- 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: z = J.random_element()
- sage: (x*y).inner_product(z) == y.inner_product(x*z)
- True
-
Our natural basis is normalized with respect to the natural inner
product unless we specify otherwise::
sage: set_random_seed()
- sage: n = ZZ.random_element(1,4)
- sage: J = ComplexHermitianEJA(n)
+ sage: J = ComplexHermitianEJA.random_instance()
sage: all( b.norm() == 1 for b in J.gens() )
True
the operator is self-adjoint by the Jordan axiom::
sage: set_random_seed()
- sage: n = ZZ.random_element(1,5)
- sage: x = ComplexHermitianEJA(n).random_element()
+ sage: x = ComplexHermitianEJA.random_instance().random_element()
sage: x.operator().matrix().is_symmetric()
True
+ We can construct the (trivial) algebra of rank zero::
+
+ sage: ComplexHermitianEJA(0)
+ Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
+
"""
- def __init__(self, n, field=QQ, normalize_basis=True, **kwargs):
- S = _complex_hermitian_basis(n, field)
- if n > 1 and normalize_basis:
- # We'll need sqrt(2) to normalize the basis, and this
- # winds up in the multiplication table, so the whole
- # algebra needs to be over the field extension.
- R = PolynomialRing(field, 'z')
- z = R.gen()
- p = z**2 - 2
- if p.is_irreducible():
- field = NumberField(p, 'sqrt2', embedding=RLF(2).sqrt())
- S = [ s.change_ring(field) for s in S ]
- self._basis_normalizers = tuple(
- ~(self.natural_inner_product(s,s).sqrt()) for s in S )
- S = tuple( s*c for (s,c) in zip(S,self._basis_normalizers) )
+ @classmethod
+ def _denormalized_basis(cls, n, field):
+ """
+ Returns a basis for the space of complex Hermitian n-by-n matrices.
+
+ 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.
- Qs = _multiplication_table_from_matrix_basis(S)
+ SETUP::
- fdeja = super(ComplexHermitianEJA, self)
- return fdeja.__init__(field,
- Qs,
- rank=n,
- natural_basis=S,
- **kwargs)
+ sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
+ TESTS::
- @staticmethod
- def _max_test_case_size():
- return 4
+ sage: set_random_seed()
+ sage: n = ZZ.random_element(1,5)
+ sage: field = QuadraticField(2, 'sqrt2')
+ sage: B = ComplexHermitianEJA._denormalized_basis(n, field)
+ sage: all( M.is_symmetric() for M in B)
+ True
+
+ """
+ R = PolynomialRing(field, 'z')
+ z = R.gen()
+ F = field.extension(z**2 + 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 range(n):
+ for j in range(i+1):
+ Eij = matrix(F, n, lambda k,l: k==i and l==j)
+ if i == j:
+ Sij = cls.real_embed(Eij)
+ S.append(Sij)
+ else:
+ # The second one has a minus because it's conjugated.
+ Sij_real = cls.real_embed(Eij + Eij.transpose())
+ S.append(Sij_real)
+ Sij_imag = cls.real_embed(I*Eij - I*Eij.transpose())
+ S.append(Sij_imag)
+
+ # Since we embedded these, we can drop back to the "field" that we
+ # started with instead of the complex extension "F".
+ return ( s.change_ring(field) for s in S )
+
+
+ def __init__(self, n, field=AA, **kwargs):
+ basis = self._denormalized_basis(n,field)
+ super(ComplexHermitianEJA,self).__init__(field,
+ basis,
+ check_axioms=False,
+ **kwargs)
+ self.rank.set_cache(n)
+
+
+class QuaternionMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
@staticmethod
- def natural_inner_product(X,Y):
- Xu = _unembed_complex_matrix(X)
- Yu = _unembed_complex_matrix(Y)
- # The trace need not be real; consider Xu = (i*I) and Yu = I.
- return ((Xu*Yu).trace()).vector()[0] # real part, I guess
+ def real_embed(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.
+ SETUP::
+ sage: from mjo.eja.eja_algebra import \
+ ....: QuaternionMatrixEuclideanJordanAlgebra
-class QuaternionHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra):
- """
- The rank-n simple EJA consisting of self-adjoint n-by-n quaternion
+ EXAMPLES::
+
+ sage: Q = QuaternionAlgebra(QQ,-1,-1)
+ sage: i,j,k = Q.gens()
+ sage: x = 1 + 2*i + 3*j + 4*k
+ sage: M = matrix(Q, 1, [[x]])
+ sage: QuaternionMatrixEuclideanJordanAlgebra.real_embed(M)
+ [ 1 2 3 4]
+ [-2 1 -4 3]
+ [-3 4 1 -2]
+ [-4 -3 2 1]
+
+ Embedding is a homomorphism (isomorphism, in fact)::
+
+ sage: set_random_seed()
+ sage: n_max = QuaternionMatrixEuclideanJordanAlgebra._max_random_instance_size()
+ sage: n = ZZ.random_element(n_max)
+ sage: Q = QuaternionAlgebra(QQ,-1,-1)
+ sage: X = random_matrix(Q, n)
+ sage: Y = random_matrix(Q, n)
+ sage: Xe = QuaternionMatrixEuclideanJordanAlgebra.real_embed(X)
+ sage: Ye = QuaternionMatrixEuclideanJordanAlgebra.real_embed(Y)
+ sage: XYe = QuaternionMatrixEuclideanJordanAlgebra.real_embed(X*Y)
+ sage: Xe*Ye == XYe
+ 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]
+ cplxM = matrix(F, 2, [[ a + b*i, c + d*i],
+ [-c + d*i, a - b*i]])
+ realM = ComplexMatrixEuclideanJordanAlgebra.real_embed(cplxM)
+ blocks.append(realM)
+
+ # 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)
+
+
+
+ @staticmethod
+ def real_unembed(M):
+ """
+ The inverse of _embed_quaternion_matrix().
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import \
+ ....: QuaternionMatrixEuclideanJordanAlgebra
+
+ EXAMPLES::
+
+ sage: M = matrix(QQ, [[ 1, 2, 3, 4],
+ ....: [-2, 1, -4, 3],
+ ....: [-3, 4, 1, -2],
+ ....: [-4, -3, 2, 1]])
+ sage: QuaternionMatrixEuclideanJordanAlgebra.real_unembed(M)
+ [1 + 2*i + 3*j + 4*k]
+
+ TESTS:
+
+ Unembedding is the inverse of embedding::
+
+ sage: set_random_seed()
+ sage: Q = QuaternionAlgebra(QQ, -1, -1)
+ sage: M = random_matrix(Q, 3)
+ sage: Me = QuaternionMatrixEuclideanJordanAlgebra.real_embed(M)
+ sage: QuaternionMatrixEuclideanJordanAlgebra.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 quaternion embedding")
+
+ # Use the base ring of the matrix to ensure that its entries can be
+ # multiplied by elements of the quaternion algebra.
+ field = M.base_ring()
+ Q = QuaternionAlgebra(field,-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 range(n/4):
+ for m in range(n/4):
+ submat = ComplexMatrixEuclideanJordanAlgebra.real_unembed(
+ 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()
+ 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/4, elements)
+
+
+ @classmethod
+ def natural_inner_product(cls,X,Y):
+ """
+ Compute a natural inner product in this algebra directly from
+ its real embedding.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
+
+ TESTS:
+
+ This gives the same answer as the slow, default method implemented
+ in :class:`MatrixEuclideanJordanAlgebra`::
+
+ sage: set_random_seed()
+ sage: J = QuaternionHermitianEJA.random_instance()
+ sage: x,y = J.random_elements(2)
+ sage: Xe = x.natural_representation()
+ sage: Ye = y.natural_representation()
+ sage: X = QuaternionHermitianEJA.real_unembed(Xe)
+ sage: Y = QuaternionHermitianEJA.real_unembed(Ye)
+ sage: expected = (X*Y).trace().coefficient_tuple()[0]
+ sage: actual = QuaternionHermitianEJA.natural_inner_product(Xe,Ye)
+ sage: actual == expected
+ True
+
+ """
+ return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/4
+
+
+class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra):
+ """
+ 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.
sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
+ EXAMPLES:
+
+ In theory, our "field" can be any subfield of the reals::
+
+ sage: QuaternionHermitianEJA(2, RDF)
+ Euclidean Jordan algebra of dimension 6 over Real Double Field
+ sage: QuaternionHermitianEJA(2, RR)
+ Euclidean Jordan algebra of dimension 6 over Real Field with
+ 53 bits of precision
+
TESTS:
The dimension of this algebra is `2*n^2 - n`::
sage: set_random_seed()
- sage: n = ZZ.random_element(1,4)
+ 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,4)
- sage: J = QuaternionHermitianEJA(n)
- sage: x = J.random_element()
- sage: y = J.random_element()
+ sage: J = QuaternionHermitianEJA.random_instance()
+ sage: x,y = J.random_elements(2)
sage: actual = (x*y).natural_representation()
sage: X = x.natural_representation()
sage: Y = y.natural_representation()
sage: QuaternionHermitianEJA(2, prefix='a').gens()
(a0, a1, a2, a3, a4, a5)
- Our inner product satisfies the Jordan axiom::
-
- sage: set_random_seed()
- sage: n = ZZ.random_element(1,4)
- sage: J = QuaternionHermitianEJA(n)
- sage: x = J.random_element()
- sage: y = J.random_element()
- sage: z = J.random_element()
- sage: (x*y).inner_product(z) == y.inner_product(x*z)
- True
-
Our natural basis is normalized with respect to the natural inner
product unless we specify otherwise::
sage: set_random_seed()
- sage: n = ZZ.random_element(1,4)
- sage: J = QuaternionHermitianEJA(n)
+ sage: J = QuaternionHermitianEJA.random_instance()
sage: all( b.norm() == 1 for b in J.gens() )
True
the operator is self-adjoint by the Jordan axiom::
sage: set_random_seed()
- sage: n = ZZ.random_element(1,5)
- sage: x = QuaternionHermitianEJA(n).random_element()
+ sage: x = QuaternionHermitianEJA.random_instance().random_element()
sage: x.operator().matrix().is_symmetric()
True
- """
- def __init__(self, n, field=QQ, normalize_basis=True, **kwargs):
- S = _quaternion_hermitian_basis(n, field)
+ We can construct the (trivial) algebra of rank zero::
- if n > 1 and normalize_basis:
- # We'll need sqrt(2) to normalize the basis, and this
- # winds up in the multiplication table, so the whole
- # algebra needs to be over the field extension.
- R = PolynomialRing(field, 'z')
- z = R.gen()
- p = z**2 - 2
- if p.is_irreducible():
- field = NumberField(p, 'sqrt2', embedding=RLF(2).sqrt())
- S = [ s.change_ring(field) for s in S ]
- self._basis_normalizers = tuple(
- ~(self.natural_inner_product(s,s).sqrt()) for s in S )
- S = tuple( s*c for (s,c) in zip(S,self._basis_normalizers) )
+ sage: QuaternionHermitianEJA(0)
+ Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
- Qs = _multiplication_table_from_matrix_basis(S)
+ """
+ @classmethod
+ def _denormalized_basis(cls, n, field):
+ """
+ Returns a basis for the space of quaternion Hermitian n-by-n matrices.
- fdeja = super(QuaternionHermitianEJA, self)
- return fdeja.__init__(field,
- Qs,
- rank=n,
- natural_basis=S,
- **kwargs)
+ 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.
- @staticmethod
- def _max_test_case_size():
- return 3
+ SETUP::
- @staticmethod
- def natural_inner_product(X,Y):
- Xu = _unembed_quaternion_matrix(X)
- Yu = _unembed_quaternion_matrix(Y)
- # The trace need not be real; consider Xu = (i*I) and Yu = I.
- # The result will be a quaternion algebra element, which doesn't
- # have a "vector" method, but does have coefficient_tuple() method
- # that returns the coefficients of 1, i, j, and k -- in that order.
- return ((Xu*Yu).trace()).coefficient_tuple()[0]
+ 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,QQ)
+ sage: all( M.is_symmetric() for M in B )
+ True
-class JordanSpinEJA(FiniteDimensionalEuclideanJordanAlgebra):
+ """
+ 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 range(n):
+ for j in range(i+1):
+ Eij = matrix(Q, n, lambda k,l: k==i and l==j)
+ 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.
+ Sij_real = cls.real_embed(Eij + Eij.transpose())
+ S.append(Sij_real)
+ Sij_I = cls.real_embed(I*Eij - I*Eij.transpose())
+ S.append(Sij_I)
+ Sij_J = cls.real_embed(J*Eij - J*Eij.transpose())
+ S.append(Sij_J)
+ Sij_K = cls.real_embed(K*Eij - K*Eij.transpose())
+ S.append(Sij_K)
+
+ # Since we embedded these, we can drop back to the "field" that we
+ # started with instead of the quaternion algebra "Q".
+ return ( s.change_ring(field) for s in S )
+
+
+ def __init__(self, n, field=AA, **kwargs):
+ basis = self._denormalized_basis(n,field)
+ super(QuaternionHermitianEJA,self).__init__(field,
+ basis,
+ check_axioms=False,
+ **kwargs)
+ self.rank.set_cache(n)
+
+
+class HadamardEJA(RationalBasisEuclideanJordanAlgebra):
"""
- The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
- with the usual inner product and jordan product ``x*y =
- (<x_bar,y_bar>, x0*y_bar + y0*x_bar)``. It has dimension `n` over
- the reals.
+ 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 JordanSpinEJA
+ sage: from mjo.eja.eja_algebra import HadamardEJA
EXAMPLES:
This multiplication table can be verified by hand::
- sage: J = JordanSpinEJA(4)
- sage: e0,e1,e2,e3 = J.gens()
+ sage: J = HadamardEJA(3)
+ sage: e0,e1,e2 = J.gens()
sage: e0*e0
e0
sage: e0*e1
- e1
- sage: e0*e2
- e2
- sage: e0*e3
- e3
- sage: e1*e2
0
- sage: e1*e3
+ sage: e0*e2
0
- sage: e2*e3
+ sage: e1*e1
+ e1
+ sage: e1*e2
0
+ sage: e2*e2
+ e2
+
+ TESTS:
We can change the generator prefix::
- sage: JordanSpinEJA(2, prefix='B').gens()
- (B0, B1)
+ sage: HadamardEJA(3, prefix='r').gens()
+ (r0, r1, r2)
- Our inner product satisfies the Jordan axiom::
+ """
+ def __init__(self, n, field=AA, **kwargs):
+ V = VectorSpace(field, n)
+ mult_table = [ [ V.gen(i)*(i == j) for j in range(n) ]
+ for i in range(n) ]
- sage: set_random_seed()
- sage: n = ZZ.random_element(1,5)
- sage: J = JordanSpinEJA(n)
- 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)
+ super(HadamardEJA, self).__init__(field,
+ mult_table,
+ check_axioms=False,
+ **kwargs)
+ self.rank.set_cache(n)
+
+ @staticmethod
+ def _max_random_instance_size():
+ return 5
+
+ def inner_product(self, x, y):
+ """
+ Faster to reimplement than to use natural representations.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import HadamardEJA
+
+ 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: X = x.natural_representation()
+ sage: Y = y.natural_representation()
+ sage: x.inner_product(y) == J.natural_inner_product(X,Y)
+ True
+
+ """
+ return x.to_vector().inner_product(y.to_vector())
+
+
+class BilinearFormEJA(RationalBasisEuclideanJordanAlgebra):
+ 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``.
+
+ 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 (BilinearFormEJA,
+ ....: JordanSpinEJA)
+
+ EXAMPLES:
+
+ When no bilinear form is specified, the identity matrix is used,
+ and the resulting algebra is the Jordan spin algebra::
+
+ sage: B = matrix.identity(AA,3)
+ sage: J0 = BilinearFormEJA(B)
+ sage: J1 = JordanSpinEJA(3)
+ sage: J0.multiplication_table() == J0.multiplication_table()
True
+ 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::
+
+ sage: set_random_seed()
+ 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)
+ sage: eis = VectorSpace(M.base_ring(), M.ncols()).basis()
+ sage: V = J.vector_space()
+ sage: sis = [ J.from_vector(V([0] + (M.inverse()*ei).list()))
+ ....: 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
"""
- def __init__(self, n, field=QQ, **kwargs):
+ def __init__(self, B, field=AA, **kwargs):
+ self._B = B
+ n = B.nrows()
+
+ if not B.is_positive_definite():
+ raise TypeError("matrix B is not positive-definite")
+
V = VectorSpace(field, n)
mult_table = [[V.zero() for j in range(n)] for i in range(n)]
for i in range(n):
xbar = x[1:]
y0 = y[0]
ybar = y[1:]
- # z = x*y
- z0 = x.inner_product(y)
+ z0 = (B*x).inner_product(y)
zbar = y0*xbar + x0*ybar
z = V([z0] + zbar.list())
mult_table[i][j] = z
- # 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, self)
- return fdeja.__init__(field, mult_table, rank=min(n,2), **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).
+ super(BilinearFormEJA, self).__init__(field,
+ mult_table,
+ check_axioms=False,
+ **kwargs)
+ self.rank.set_cache(min(n,2))
- def inner_product(self, x, y):
+ @staticmethod
+ def _max_random_instance_size():
+ return 5
+
+ @classmethod
+ def random_instance(cls, field=AA, **kwargs):
"""
- Faster to reimplement than to use natural representations.
+ Return a random instance of this algebra.
+ """
+ n = ZZ.random_element(cls._max_random_instance_size() + 1)
+ if n == 0:
+ # Special case needed since we use (n-1) below.
+ B = matrix.identity(field, 0)
+ return cls(B, field, **kwargs)
+
+ B11 = matrix.identity(field,1)
+ M = matrix.random(field, n-1)
+ I = matrix.identity(field, n-1)
+ alpha = field.zero()
+ while alpha.is_zero():
+ alpha = field.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, field, **kwargs)
+
+ def inner_product(self, x, y):
+ r"""
+ Half of the trace inner product.
+
+ This is defined so that the special case of the Jordan spin
+ algebra gets the usual inner product.
SETUP::
- sage: from mjo.eja.eja_algebra import JordanSpinEJA
+ sage: from mjo.eja.eja_algebra import BilinearFormEJA
TESTS:
- Ensure that this is the usual inner product for the algebras
- over `R^n`::
+ Ensure that this is one-half of the trace inner-product when
+ the algebra 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: n = ZZ.random_element(1,5)
- sage: J = JordanSpinEJA(n)
+ sage: J = BilinearFormEJA.random_instance()
+ sage: n = J.dimension()
sage: x = J.random_element()
sage: y = J.random_element()
+ sage: (n == 1) or (x.inner_product(y) == (x*y).trace()/2)
+ True
+
+ """
+ return (self._B*x.to_vector()).inner_product(y.to_vector())
+
+
+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,y>, x0*y_bar + y0*x_bar)``. It has dimension `n` over
+ the reals.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import JordanSpinEJA
+
+ EXAMPLES:
+
+ This multiplication table can be verified by hand::
+
+ sage: J = JordanSpinEJA(4)
+ sage: e0,e1,e2,e3 = J.gens()
+ sage: e0*e0
+ e0
+ sage: e0*e1
+ e1
+ sage: e0*e2
+ e2
+ sage: e0*e3
+ e3
+ sage: e1*e2
+ 0
+ sage: e1*e3
+ 0
+ sage: e2*e3
+ 0
+
+ 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: X = x.natural_representation()
sage: Y = y.natural_representation()
sage: x.inner_product(y) == J.natural_inner_product(X,Y)
True
+ """
+ def __init__(self, n, field=AA, **kwargs):
+ # This is a special case of the BilinearFormEJA with the identity
+ # matrix as its bilinear form.
+ B = matrix.identity(field, n)
+ super(JordanSpinEJA, self).__init__(B, field, **kwargs)
+
+ @classmethod
+ def random_instance(cls, field=AA, **kwargs):
"""
- return x.to_vector().inner_product(y.to_vector())
+ 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, field, **kwargs)
+
+
+class TrivialEJA(FiniteDimensionalEuclideanJordanAlgebra):
+ """
+ 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, field=AA, **kwargs):
+ mult_table = []
+ super(TrivialEJA, self).__init__(field,
+ mult_table,
+ check_axioms=False,
+ **kwargs)
+ # The rank is zero using my definition, namely the dimension of the
+ # largest subalgebra generated by any element.
+ self.rank.set_cache(0)
+
+ @classmethod
+ def random_instance(cls, field=AA, **kwargs):
+ # We don't take a "size" argument so the superclass method is
+ # inappropriate for us.
+ return cls(field, **kwargs)
+
+class DirectSumEJA(FiniteDimensionalEuclideanJordanAlgebra):
+ r"""
+ The external (orthogonal) direct sum of two other Euclidean Jordan
+ algebras. Essentially the Cartesian product of its two factors.
+ Every Euclidean Jordan algebra decomposes into an orthogonal
+ direct sum of simple Euclidean Jordan algebras, so no generality
+ is lost by providing only this construction.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (HadamardEJA,
+ ....: RealSymmetricEJA,
+ ....: DirectSumEJA)
+
+ EXAMPLES::
+
+ sage: J1 = HadamardEJA(2)
+ sage: J2 = RealSymmetricEJA(3)
+ sage: J = DirectSumEJA(J1,J2)
+ sage: J.dimension()
+ 8
+ sage: J.rank()
+ 5
+
+ """
+ def __init__(self, J1, J2, field=AA, **kwargs):
+ self._factors = (J1, J2)
+ n1 = J1.dimension()
+ n2 = J2.dimension()
+ n = n1+n2
+ V = VectorSpace(field, n)
+ mult_table = [ [ V.zero() for j in range(n) ]
+ for i in range(n) ]
+ for i in range(n1):
+ for j in range(n1):
+ p = (J1.monomial(i)*J1.monomial(j)).to_vector()
+ mult_table[i][j] = V(p.list() + [field.zero()]*n2)
+
+ for i in range(n2):
+ for j in range(n2):
+ p = (J2.monomial(i)*J2.monomial(j)).to_vector()
+ mult_table[n1+i][n1+j] = V([field.zero()]*n1 + p.list())
+
+ super(DirectSumEJA, self).__init__(field,
+ mult_table,
+ check_axioms=False,
+ **kwargs)
+ self.rank.set_cache(J1.rank() + J2.rank())
+
+
+ def factors(self):
+ r"""
+ Return the pair of this algebra's factors.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (HadamardEJA,
+ ....: JordanSpinEJA,
+ ....: DirectSumEJA)
+
+ EXAMPLES::
+
+ sage: J1 = HadamardEJA(2,QQ)
+ sage: J2 = JordanSpinEJA(3,QQ)
+ sage: J = DirectSumEJA(J1,J2)
+ sage: J.factors()
+ (Euclidean Jordan algebra of dimension 2 over Rational Field,
+ Euclidean Jordan algebra of dimension 3 over Rational Field)
+
+ """
+ return self._factors
+
+ def projections(self):
+ r"""
+ Return a pair of projections onto this algebra's factors.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
+ ....: ComplexHermitianEJA,
+ ....: DirectSumEJA)
+
+ EXAMPLES::
+
+ sage: J1 = JordanSpinEJA(2)
+ sage: J2 = ComplexHermitianEJA(2)
+ sage: J = DirectSumEJA(J1,J2)
+ sage: (pi_left, pi_right) = J.projections()
+ sage: J.one().to_vector()
+ (1, 0, 1, 0, 0, 1)
+ sage: pi_left(J.one()).to_vector()
+ (1, 0)
+ sage: pi_right(J.one()).to_vector()
+ (1, 0, 0, 1)
+
+ """
+ (J1,J2) = self.factors()
+ n = J1.dimension()
+ pi_left = lambda x: J1.from_vector(x.to_vector()[:n])
+ pi_right = lambda x: J2.from_vector(x.to_vector()[n:])
+ return (pi_left, pi_right)
+
+ def inclusions(self):
+ r"""
+ Return the pair of inclusion maps from our factors into us.
+
+ SETUP::
+
+ sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
+ ....: RealSymmetricEJA,
+ ....: DirectSumEJA)
+
+ EXAMPLES::
+
+ sage: J1 = JordanSpinEJA(3)
+ sage: J2 = RealSymmetricEJA(2)
+ sage: J = DirectSumEJA(J1,J2)
+ sage: (iota_left, iota_right) = J.inclusions()
+ 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)
+
+ """
+ (J1,J2) = self.factors()
+ n = J1.dimension()
+ V_basis = self.vector_space().basis()
+ I1 = matrix.column(self.base_ring(), V_basis[:n])
+ I2 = matrix.column(self.base_ring(), V_basis[n:])
+ iota_left = lambda x: self.from_vector(I1*x.to_vector())
+ iota_right = lambda x: self.from_vector(I2*+x.to_vector())
+ return (iota_left, iota_right)
+
+ def inner_product(self, x, y):
+ r"""
+ The standard Cartesian inner-product.
+
+ We project ``x`` and ``y`` onto our factors, and add up the
+ inner-products from the subalgebras.
+
+ SETUP::
+
+
+ sage: from mjo.eja.eja_algebra import (HadamardEJA,
+ ....: QuaternionHermitianEJA,
+ ....: DirectSumEJA)
+
+ EXAMPLE::
+
+ sage: J1 = HadamardEJA(3)
+ sage: J2 = QuaternionHermitianEJA(2,QQ,normalize_basis=False)
+ sage: J = DirectSumEJA(J1,J2)
+ sage: x1 = J1.one()
+ sage: x2 = x1
+ sage: y1 = J2.one()
+ sage: y2 = y1
+ sage: x1.inner_product(x2)
+ 3
+ sage: y1.inner_product(y2)
+ 2
+ sage: J.one().inner_product(J.one())
+ 5
+
+ """
+ (pi_left, pi_right) = self.projections()
+ x1 = pi_left(x)
+ x2 = pi_right(x)
+ y1 = pi_left(y)
+ y2 = pi_right(y)
+
+ return (x1.inner_product(y1) + x2.inner_product(y2))