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.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.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_operator import FiniteDimensionalEuclideanJordanAlgebraOperator
from mjo.eja.eja_utils import _mat2vec
class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
sage: J(1)
Traceback (most recent call last):
...
- ValueError: not a naturally-represented algebra element
+ ValueError: not an element of this algebra
"""
return None
prefix='e',
category=None,
natural_basis=None,
- check=True):
+ check_field=True,
+ check_axioms=True):
"""
SETUP::
TESTS:
- The ``field`` we're given must be real with ``check=True``::
+ The ``field`` we're given must be real with ``check_field=True``::
sage: JordanSpinEJA(2,QQbar)
Traceback (most recent call last):
...
- ValueError: field is not real
+ ValueError: scalar field is not real
- The multiplication table must be square with ``check=True``::
+ The multiplication table must be square with ``check_axioms=True``::
sage: FiniteDimensionalEuclideanJordanAlgebra(QQ,((),()))
Traceback (most recent call last):
ValueError: multiplication table is not square
"""
- if check:
+ if check_field:
if not field.is_subring(RR):
# Note: this does return true for the real algebraic
- # field, and any quadratic field where we've specified
- # a real embedding.
- raise ValueError('field is not real')
+ # 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")
self._natural_basis = natural_basis
category = MagmaticAlgebras(field).FiniteDimensional()
category = category.WithBasis().Unital()
- # The multiplication table had better be square
- n = len(mult_table)
- if check:
- if not all( len(l) == n for l in mult_table ):
- raise ValueError("multiplication table is not square")
-
fda = super(FiniteDimensionalEuclideanJordanAlgebra, self)
fda.__init__(field,
range(n),
for ls in mult_table
]
- if check:
+ if check_axioms:
if not self._is_commutative():
raise ValueError("algebra is not commutative")
if not self._is_jordanian():
sage: J(A)
Traceback (most recent call last):
...
- ArithmeticError: vector is not in free module
+ ValueError: not an element of this algebra
TESTS:
True
"""
- msg = "not a naturally-represented algebra element"
+ 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.
# could be QQ instead of QQbar.
V = VectorSpace(basis_space.base_ring(), elt.nrows()*elt.ncols())
W = V.span_of_basis( _mat2vec(s) for s in natural_basis )
- coords = W.coordinate_vector(_mat2vec(elt))
- return self.from_vector(coords)
- @staticmethod
- def _max_test_case_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 quite vague -- 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.
+ try:
+ coords = W.coordinate_vector(_mat2vec(elt))
+ except ArithmeticError: # vector is not in free module
+ raise ValueError(msg)
- We default to five in this class, which is safe in `R^n`. The
- matrix algebra subclasses (or any class where the "size" is
- interpreted to be far less than the dimension) should override
- with a smaller number.
- """
- return 5
+ return self.from_vector(coords)
def _repr_(self):
"""
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=False`` and passed
- an invalid multiplication table.
+ this algebra was constructed with ``check_axioms=False`` and
+ passed an invalid multiplication table.
"""
return all( self.product_on_basis(i,j) == self.product_on_basis(i,j)
for i in range(self.dimension())
``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=False`` and passed an invalid multiplication table.
+ ``check_axioms=False`` and passed an invalid multiplication table.
"""
return all( (self.monomial(i)**2)*(self.monomial(i)*self.monomial(j))
==
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=False`` and passed
- an invalid multiplication table.
+ this algebra was constructed with ``check_axioms=False`` and
+ passed an invalid multiplication table.
"""
# 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=True.
+ # QuaternionHermitianEJA(2, RDF) with check_axioms=True.
epsilon = 1e-16
for i in range(self.dimension()):
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.
+
+ 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,QQ)
+ 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:
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: (n == 1) or (x.inner_product(y) == (x*y).trace()/2)
+ True
"""
- X = x.natural_representation()
- Y = y.natural_representation()
- return self.natural_inner_product(X,Y)
+ B = self._inner_product_matrix
+ return (B*x.to_vector()).inner_product(y.to_vector())
def is_trivial(self):
return self._natural_basis[0].matrix_space()
- @staticmethod
- def natural_inner_product(X,Y):
- """
- Compute the inner product of two naturally-represented elements.
-
- For example in the real symmetric matrix EJA, this will compute
- the trace inner-product of two n-by-n symmetric matrices. The
- default should work for the real cartesian product EJA, the
- Jordan spin EJA, and the real symmetric matrices. The others
- will have to be overridden.
- """
- return (X.conjugate_transpose()*Y).trace()
-
-
@cached_method
def one(self):
"""
sage: actual == expected
True
+ Ensure that the cached unit element (often precomputed by
+ hand) agrees with the computed one::
+
+ sage: set_random_seed()
+ sage: J = random_eja()
+ sage: cached = J.one()
+ sage: J.one.clear_cache()
+ sage: J.one() == cached
+ True
+
"""
# We can brute-force compute the matrices of the operators
# that correspond to the basis elements of this algebra.
# 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 peirce_decomposition(self, c):
if not c.is_idempotent():
raise ValueError("element is not idempotent: %s" % c)
+ from mjo.eja.eja_subalgebra import FiniteDimensionalEuclideanJordanSubalgebra
+
# 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
gens = tuple( self.from_vector(b) for b in eigspace.basis() )
subalg = FiniteDimensionalEuclideanJordanSubalgebra(self,
gens,
- check=False)
+ check_axioms=False)
if eigval == 0:
J0 = subalg
elif eigval == 1:
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.
- """
- if cls is TrivialEJA:
- # The TrivialEJA class doesn't take an "n" argument because
- # there's only one.
- return cls(field)
-
- n = ZZ.random_element(cls._max_test_case_size() + 1)
- return cls(n, field, **kwargs)
@cached_method
def _charpoly_coefficients(self):
of" function.
"""
n = self.dimension()
- var_names = [ "X" + str(z) for z in range(1,n+1) ]
- R = PolynomialRing(self.base_ring(), var_names)
+ R = self.coordinate_polynomial_ring()
vars = R.gens()
F = R.fraction_field()
Element = FiniteDimensionalEuclideanJordanAlgebraElement
-class HadamardEJA(FiniteDimensionalEuclideanJordanAlgebra):
+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.
"""
- 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 HadamardEJA
-
- EXAMPLES:
-
- This multiplication table can be verified by hand::
-
- 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
+ @cached_method
+ def _charpoly_coefficients(self):
+ r"""
+ Override the parent method with something that tries to compute
+ over a faster (non-extension) field.
- TESTS:
+ SETUP::
- We can change the generator prefix::
+ sage: from mjo.eja.eja_algebra import JordanSpinEJA
- sage: HadamardEJA(3, prefix='r').gens()
- (r0, r1, r2)
+ EXAMPLES:
- """
- 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) ]
+ 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)::
- super(HadamardEJA, self).__init__(field,
- mult_table,
- check=False,
- **kwargs)
- self.rank.set_cache(n)
+ 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
+
+ """
+ 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 ConcreteEuclideanJordanAlgebra:
+ r"""
+ A class for the Euclidean Jordan algebras that we know by name.
- def inner_product(self, x, y):
- """
- Faster to reimplement than to use natural representations.
+ 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."
- SETUP::
+ SETUP::
- sage: from mjo.eja.eja_algebra import HadamardEJA
+ sage: from mjo.eja.eja_algebra import ConcreteEuclideanJordanAlgebra
- TESTS:
+ TESTS:
- Ensure that this is the usual inner product for the algebras
- over `R^n`::
+ Our natural basis is normalized with respect to the natural inner
+ product unless we specify otherwise::
- 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
+ sage: set_random_seed()
+ sage: J = ConcreteEuclideanJordanAlgebra.random_instance()
+ sage: all( b.norm() == 1 for b in J.gens() )
+ True
- """
- return x.to_vector().inner_product(y.to_vector())
+ Since our natural basis is normalized with respect to the natural
+ 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::
+ sage: set_random_seed()
+ sage: J = ConcreteEuclideanJordanAlgebra.random_instance()
+ sage: x = J.random_element()
+ sage: x.operator().is_self_adjoint()
+ True
-def random_eja(field=AA):
"""
- Return a "random" finite-dimensional Euclidean Jordan Algebra.
-
- SETUP::
-
- sage: from mjo.eja.eja_algebra import random_eja
- TESTS::
-
- sage: random_eja()
- Euclidean Jordan algebra of dimension...
+ @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.
- """
- classname = choice([TrivialEJA,
- HadamardEJA,
- JordanSpinEJA,
- RealSymmetricEJA,
- ComplexHermitianEJA,
- QuaternionHermitianEJA])
- return classname.random_instance(field=field)
+ This method must be implemented in each subclass.
+ """
+ raise NotImplementedError
+ @classmethod
+ def random_instance(cls, field=AA, **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__())
+ return eja_class.random_instance(field)
class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
- @staticmethod
- def _max_test_case_size():
- # Play it safe, since this will be squared and the underlying
- # field can have dimension 4 (quaternions) too.
- return 2
def __init__(self, field, basis, normalize_basis=True, **kwargs):
"""
# time to ensure that it isn't a generator expression.
basis = tuple(basis)
- if len(basis) > 1 and normalize_basis:
+ algebra_dim = len(basis)
+ degree = 0 # size of the matrices
+ if algebra_dim > 0:
+ degree = basis[0].nrows()
+
+ if algebra_dim > 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.
~(self.natural_inner_product(s,s).sqrt()) for s in basis )
basis = tuple(s*c for (s,c) in zip(basis,self._basis_normalizers))
- Qs = self.multiplication_table_from_matrix_basis(basis)
+ # Now compute the multiplication and inner product tables.
+ # We have to do this *after* normalizing the basis, because
+ # scaling affects the answers.
+ V = VectorSpace(field, degree**2)
+ W = V.span_of_basis( _mat2vec(s) for s in basis )
+ mult_table = [[W.zero() for j in range(algebra_dim)]
+ for i in range(algebra_dim)]
+ ip_table = [[W.zero() for j in range(algebra_dim)]
+ for i in range(algebra_dim)]
+ for i in range(algebra_dim):
+ for j in range(algebra_dim):
+ mat_entry = (basis[i]*basis[j] + basis[j]*basis[i])/2
+ mult_table[i][j] = W.coordinate_vector(_mat2vec(mat_entry))
+
+ try:
+ # HACK: ignore the error here if we don't need the
+ # inner product (as is the case when we construct
+ # a dummy QQ-algebra for fast charpoly coefficients.
+ ip_table[i][j] = self.natural_inner_product(basis[i],
+ basis[j])
+ except:
+ pass
+
+ try:
+ # HACK PART DEUX
+ self._inner_product_matrix = matrix(field,ip_table)
+ except:
+ pass
super(MatrixEuclideanJordanAlgebra, self).__init__(field,
- Qs,
+ mult_table,
natural_basis=basis,
**kwargs)
+ if algebra_dim == 0:
+ self.one.set_cache(self.zero())
+ else:
+ n = basis[0].nrows()
+ # The identity wrt (A,B) -> (AB + BA)/2 is independent of the
+ # details of this algebra.
+ self.one.set_cache(self(matrix.identity(field,n)))
+
@cached_method
def _charpoly_coefficients(self):
Override the parent method with something that tries to compute
over a faster (non-extension) field.
"""
- if self._basis_normalizers is None:
- # We didn't normalize, so assume that the basis we started
- # with had entries in a nice 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()
- else:
- 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=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=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 )
-
- @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))
-
- return mult_table
+ 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 )
@staticmethod
"""
raise NotImplementedError
-
@classmethod
def natural_inner_product(cls,X,Y):
Xu = cls.real_unembed(X)
return M
-class RealSymmetricEJA(RealMatrixEuclideanJordanAlgebra):
+class RealSymmetricEJA(RealMatrixEuclideanJordanAlgebra,
+ ConcreteEuclideanJordanAlgebra):
"""
The rank-n simple EJA consisting of real symmetric n-by-n
matrices, the usual symmetric Jordan product, and the trace inner
The dimension of this algebra is `(n^2 + n) / 2`::
sage: set_random_seed()
- sage: n_max = RealSymmetricEJA._max_test_case_size()
+ 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
sage: RealSymmetricEJA(3, prefix='q').gens()
(q0, q1, q2, q3, q4, q5)
- Our natural basis is normalized with respect to the natural inner
- product unless we specify otherwise::
-
- sage: set_random_seed()
- sage: J = RealSymmetricEJA.random_instance()
- sage: all( b.norm() == 1 for b in J.gens() )
- True
-
- Since our natural basis is normalized with respect to the natural
- 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::
-
- sage: set_random_seed()
- 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)
@staticmethod
- def _max_test_case_size():
+ def _max_random_instance_size():
return 4 # Dimension 10
+ @classmethod
+ def random_instance(cls, field=AA, **kwargs):
+ """
+ Return a random instance of this type of algebra.
+ """
+ n = ZZ.random_element(cls._max_random_instance_size() + 1)
+ return cls(n, field, **kwargs)
def __init__(self, n, field=AA, **kwargs):
basis = self._denormalized_basis(n, field)
super(RealSymmetricEJA, self).__init__(field,
basis,
- check=False,
+ check_axioms=False,
**kwargs)
self.rank.set_cache(n)
Embedding is a homomorphism (isomorphism, in fact)::
sage: set_random_seed()
- sage: n_max = ComplexMatrixEuclideanJordanAlgebra._max_test_case_size()
- sage: n = ZZ.random_element(n_max)
+ sage: n = ZZ.random_element(3)
sage: F = QuadraticField(-1, 'I')
sage: X = random_matrix(F, n)
sage: Y = random_matrix(F, n)
return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/2
-class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra):
+class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra,
+ ConcreteEuclideanJordanAlgebra):
"""
The rank-n simple EJA consisting of complex Hermitian n-by-n
matrices over the real numbers, the usual symmetric Jordan product,
The dimension of this algebra is `n^2`::
sage: set_random_seed()
- sage: n_max = ComplexHermitianEJA._max_test_case_size()
+ 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
sage: ComplexHermitianEJA(2, prefix='z').gens()
(z0, z1, z2, z3)
- Our natural basis is normalized with respect to the natural inner
- product unless we specify otherwise::
-
- sage: set_random_seed()
- sage: J = ComplexHermitianEJA.random_instance()
- sage: all( b.norm() == 1 for b in J.gens() )
- True
-
- Since our natural basis is normalized with respect to the natural
- 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::
-
- sage: set_random_seed()
- 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)
basis = self._denormalized_basis(n,field)
super(ComplexHermitianEJA,self).__init__(field,
basis,
- check=False,
+ check_axioms=False,
**kwargs)
self.rank.set_cache(n)
+ @staticmethod
+ def _max_random_instance_size():
+ return 3 # Dimension 9
+
+ @classmethod
+ def random_instance(cls, field=AA, **kwargs):
+ """
+ Return a random instance of this type of algebra.
+ """
+ n = ZZ.random_element(cls._max_random_instance_size() + 1)
+ return cls(n, field, **kwargs)
class QuaternionMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
@staticmethod
Embedding is a homomorphism (isomorphism, in fact)::
sage: set_random_seed()
- sage: n_max = QuaternionMatrixEuclideanJordanAlgebra._max_test_case_size()
- sage: n = ZZ.random_element(n_max)
+ sage: n = ZZ.random_element(2)
sage: Q = QuaternionAlgebra(QQ,-1,-1)
sage: X = random_matrix(Q, n)
sage: Y = random_matrix(Q, n)
return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/4
-class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra):
- """
+class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra,
+ ConcreteEuclideanJordanAlgebra):
+ 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 dimension of this algebra is `2*n^2 - n`::
sage: set_random_seed()
- sage: n_max = QuaternionHermitianEJA._max_test_case_size()
+ 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
sage: QuaternionHermitianEJA(2, prefix='a').gens()
(a0, a1, a2, a3, a4, a5)
- Our natural basis is normalized with respect to the natural inner
- product unless we specify otherwise::
-
- sage: set_random_seed()
- sage: J = QuaternionHermitianEJA.random_instance()
- sage: all( b.norm() == 1 for b in J.gens() )
- True
-
- Since our natural basis is normalized with respect to the natural
- 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::
-
- sage: set_random_seed()
- sage: x = QuaternionHermitianEJA.random_instance().random_element()
- sage: x.operator().matrix().is_symmetric()
- True
-
We can construct the (trivial) algebra of rank zero::
sage: QuaternionHermitianEJA(0)
basis = self._denormalized_basis(n,field)
super(QuaternionHermitianEJA,self).__init__(field,
basis,
- check=False,
+ check_axioms=False,
**kwargs)
self.rank.set_cache(n)
+ @staticmethod
+ def _max_random_instance_size():
+ r"""
+ The maximum rank of a random QuaternionHermitianEJA.
+ """
+ return 2 # Dimension 6
+
+ @classmethod
+ def random_instance(cls, field=AA, **kwargs):
+ """
+ Return a random instance of this type of algebra.
+ """
+ n = ZZ.random_element(cls._max_random_instance_size() + 1)
+ return cls(n, field, **kwargs)
-class BilinearFormEJA(FiniteDimensionalEuclideanJordanAlgebra):
+
+class HadamardEJA(RationalBasisEuclideanJordanAlgebra,
+ ConcreteEuclideanJordanAlgebra):
+ """
+ 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 HadamardEJA
+
+ EXAMPLES:
+
+ This multiplication table can be verified by hand::
+
+ 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
+
+ TESTS:
+
+ We can change the generator prefix::
+
+ sage: HadamardEJA(3, prefix='r').gens()
+ (r0, r1, r2)
+
+ """
+ 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) ]
+
+ # Inner products are real numbers and not algebra
+ # elements, so once we turn the algebra element
+ # into a vector in inner_product(), we never go
+ # back. As a result -- contrary to what we do with
+ # self._multiplication_table -- we store the inner
+ # product table as a plain old matrix and not as
+ # an algebra operator.
+ ip_table = matrix.identity(field,n)
+ self._inner_product_matrix = ip_table
+
+ super(HadamardEJA, self).__init__(field,
+ mult_table,
+ check_axioms=False,
+ **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 _max_random_instance_size():
+ r"""
+ The maximum dimension of a random HadamardEJA.
+ """
+ return 5
+
+ @classmethod
+ def random_instance(cls, field=AA, **kwargs):
+ """
+ Return a random instance of this type of algebra.
+ """
+ n = ZZ.random_element(cls._max_random_instance_size() + 1)
+ return cls(n, field, **kwargs)
+
+
+class BilinearFormEJA(RationalBasisEuclideanJordanAlgebra,
+ ConcreteEuclideanJordanAlgebra):
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 =
- (x0*y0 + <B*x_bar,y_bar>, x0*y_bar + y0*x_bar)`` where ``B`` is a
- symmetric positive-definite "bilinear form" matrix. It has
- dimension `n` over the reals, and reduces to the ``JordanSpinEJA``
- when ``B`` is the identity matrix of order ``n-1``.
+ (<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::
When no bilinear form is specified, the identity matrix is used,
and the resulting algebra is the Jordan spin algebra::
- sage: J0 = BilinearFormEJA(3)
+ sage: B = matrix.identity(AA,3)
+ sage: J0 = BilinearFormEJA(B)
sage: J1 = JordanSpinEJA(3)
sage: J0.multiplication_table() == J0.multiplication_table()
True
+ 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: J = BilinearFormEJA(0)
+ sage: B = matrix.identity(AA,0)
+ sage: J = BilinearFormEJA(B)
sage: J.basis()
Finite family {}
sage: set_random_seed()
sage: n = ZZ.random_element(5)
sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular')
- sage: B = M.transpose()*M
- sage: J = BilinearFormEJA(n, B=B)
+ 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()))
sage: actual == expected
True
"""
- def __init__(self, n, field=AA, B=None, **kwargs):
- if B is None:
- self._B = matrix.identity(field, max(0,n-1))
- else:
- self._B = B
+ def __init__(self, B, field=AA, **kwargs):
+ n = B.nrows()
+
+ if not B.is_positive_definite():
+ raise ValueError("bilinear form is not positive-definite")
V = VectorSpace(field, n)
mult_table = [[V.zero() for j in range(n)] for i in range(n)]
xbar = x[1:]
y0 = y[0]
ybar = y[1:]
- z0 = x0*y0 + (self._B*xbar).inner_product(ybar)
+ z0 = (B*x).inner_product(y)
zbar = y0*xbar + x0*ybar
z = V([z0] + zbar.list())
mult_table[i][j] = z
- # 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).
+ # Inner products are real numbers and not algebra
+ # elements, so once we turn the algebra element
+ # into a vector in inner_product(), we never go
+ # back. As a result -- contrary to what we do with
+ # self._multiplication_table -- we store the inner
+ # product table as a plain old matrix and not as
+ # an algebra operator.
+ ip_table = B
+ self._inner_product_matrix = ip_table
+
super(BilinearFormEJA, self).__init__(field,
mult_table,
- check=False,
+ check_axioms=False,
**kwargs)
- self.rank.set_cache(min(n,2))
-
- 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.
+ # 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))
- SETUP::
+ if n == 0:
+ self.one.set_cache( self.zero() )
+ else:
+ self.one.set_cache( self.monomial(0) )
- sage: from mjo.eja.eja_algebra import BilinearFormEJA
+ @staticmethod
+ def _max_random_instance_size():
+ r"""
+ The maximum dimension of a random BilinearFormEJA.
+ """
+ return 5
- TESTS:
+ @classmethod
+ def random_instance(cls, field=AA, **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(field, n)
+ return cls(B, field, **kwargs)
- 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::
+ 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
- sage: set_random_seed()
- sage: n = ZZ.random_element(2,5)
- sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular')
- sage: B = M.transpose()*M
- sage: J = BilinearFormEJA(n, B=B)
- sage: x = J.random_element()
- sage: y = J.random_element()
- sage: x.inner_product(y) == (x*y).trace()/2
- True
+ from sage.matrix.special import block_matrix
+ B = block_matrix(2,2, [ [B11, ZZ(0) ],
+ [ZZ(0), B22 ] ])
- """
- xvec = x.to_vector()
- xbar = xvec[1:]
- yvec = y.to_vector()
- ybar = yvec[1:]
- return x[0]*y[0] + (self._B*xbar).inner_product(ybar)
+ return cls(B, field, **kwargs)
class JordanSpinEJA(BilinearFormEJA):
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)
+ sage: actual = x.inner_product(y)
+ sage: expected = x.to_vector().inner_product(y.to_vector())
+ sage: actual == expected
True
"""
def __init__(self, n, field=AA, **kwargs):
# This is a special case of the BilinearFormEJA with the identity
# matrix as its bilinear form.
- super(JordanSpinEJA, self).__init__(n, field, **kwargs)
+ B = matrix.identity(field, n)
+ super(JordanSpinEJA, self).__init__(B, field, **kwargs)
+ @staticmethod
+ def _max_random_instance_size():
+ r"""
+ The maximum dimension of a random JordanSpinEJA.
+ """
+ return 5
+
+ @classmethod
+ def random_instance(cls, field=AA, **kwargs):
+ """
+ Return a random instance of this type of algebra.
-class TrivialEJA(FiniteDimensionalEuclideanJordanAlgebra):
+ 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,
+ ConcreteEuclideanJordanAlgebra):
"""
The trivial Euclidean Jordan algebra consisting of only a zero element.
"""
def __init__(self, field=AA, **kwargs):
mult_table = []
+ self._inner_product_matrix = matrix(field,0)
super(TrivialEJA, self).__init__(field,
mult_table,
- check=False,
+ 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)
+ self.one.set_cache( self.zero() )
+ @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"""
SETUP::
- sage: from mjo.eja.eja_algebra import (HadamardEJA,
+ sage: from mjo.eja.eja_algebra import (random_eja,
+ ....: HadamardEJA,
....: RealSymmetricEJA,
....: DirectSumEJA)
sage: J.rank()
5
+ TESTS:
+
+ The external direct sum construction is only valid when the two factors
+ have the same base ring; an error is raised otherwise::
+
+ sage: set_random_seed()
+ sage: J1 = random_eja(AA)
+ sage: J2 = random_eja(QQ)
+ sage: J = DirectSumEJA(J1,J2)
+ Traceback (most recent call last):
+ ...
+ ValueError: algebras must share the same base field
+
"""
- def __init__(self, J1, J2, field=AA, **kwargs):
+ def __init__(self, J1, J2, **kwargs):
+ if J1.base_ring() != J2.base_ring():
+ raise ValueError("algebras must share the same base field")
+ field = J1.base_ring()
+
+ self._factors = (J1, J2)
n1 = J1.dimension()
n2 = J2.dimension()
n = n1+n2
super(DirectSumEJA, self).__init__(field,
mult_table,
- check=False,
+ 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()
+ m = J1.dimension()
+ n = J2.dimension()
+ V_basis = self.vector_space().basis()
+ # Need to specify the dimensions explicitly so that we don't
+ # wind up with a zero-by-zero matrix when we want e.g. a
+ # zero-by-two matrix (important for composing things).
+ P1 = matrix(self.base_ring(), m, m+n, V_basis[:m])
+ P2 = matrix(self.base_ring(), n, m+n, V_basis[m:])
+ pi_left = FiniteDimensionalEuclideanJordanAlgebraOperator(self,J1,P1)
+ pi_right = FiniteDimensionalEuclideanJordanAlgebraOperator(self,J2,P2)
+ 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 (random_eja,
+ ....: 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)
+
+ TESTS:
+
+ 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 = DirectSumEJA(J1,J2)
+ sage: (iota_left, iota_right) = J.inclusions()
+ sage: (pi_left, pi_right) = J.projections()
+ 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
+
+ """
+ (J1,J2) = self.factors()
+ m = J1.dimension()
+ n = J2.dimension()
+ V_basis = self.vector_space().basis()
+ # Need to specify the dimensions explicitly so that we don't
+ # wind up with a zero-by-zero matrix when we want e.g. a
+ # two-by-zero matrix (important for composing things).
+ I1 = matrix.column(self.base_ring(), m, m+n, V_basis[:m])
+ I2 = matrix.column(self.base_ring(), n, m+n, V_basis[m:])
+ iota_left = FiniteDimensionalEuclideanJordanAlgebraOperator(J1,self,I1)
+ iota_right = FiniteDimensionalEuclideanJordanAlgebraOperator(J2,self,I2)
+ 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,QQ)
+ 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))
+
+
+
+random_eja = ConcreteEuclideanJordanAlgebra.random_instance