2 Representations and constructions for Euclidean Jordan algebras.
4 A Euclidean Jordan algebra is a Jordan algebra that has some
7 1. It is finite-dimensional.
8 2. Its scalar field is the real numbers.
9 3a. An inner product is defined on it, and...
10 3b. That inner product is compatible with the Jordan product
11 in the sense that `<x*y,z> = <y,x*z>` for all elements
12 `x,y,z` in the algebra.
14 Every Euclidean Jordan algebra is formally-real: for any two elements
15 `x` and `y` in the algebra, `x^{2} + y^{2} = 0` implies that `x = y =
16 0`. Conversely, every finite-dimensional formally-real Jordan algebra
17 can be made into a Euclidean Jordan algebra with an appropriate choice
20 Formally-real Jordan algebras were originally studied as a framework
21 for quantum mechanics. Today, Euclidean Jordan algebras are crucial in
22 symmetric cone optimization, since every symmetric cone arises as the
23 cone of squares in some Euclidean Jordan algebra.
25 It is known that every Euclidean Jordan algebra decomposes into an
26 orthogonal direct sum (essentially, a Cartesian product) of simple
27 algebras, and that moreover, up to Jordan-algebra isomorphism, there
28 are only five families of simple algebras. We provide constructions
29 for these simple algebras:
31 * :class:`BilinearFormEJA`
32 * :class:`RealSymmetricEJA`
33 * :class:`ComplexHermitianEJA`
34 * :class:`QuaternionHermitianEJA`
35 * :class:`OctonionHermitianEJA`
37 In addition to these, we provide two other example constructions,
39 * :class:`JordanSpinEJA`
40 * :class:`HadamardEJA`
44 The Jordan spin algebra is a bilinear form algebra where the bilinear
45 form is the identity. The Hadamard EJA is simply a Cartesian product
46 of one-dimensional spin algebras. The Albert EJA is simply a special
47 case of the :class:`OctonionHermitianEJA` where the matrices are
48 three-by-three and the resulting space has dimension 27. And
49 last/least, the trivial EJA is exactly what you think it is; it could
50 also be obtained by constructing a dimension-zero instance of any of
51 the other algebras. Cartesian products of these are also supported
52 using the usual ``cartesian_product()`` function; as a result, we
53 support (up to isomorphism) all Euclidean Jordan algebras.
57 sage: from mjo.eja.eja_algebra import random_eja
62 Euclidean Jordan algebra of dimension...
65 from sage
.algebras
.quatalg
.quaternion_algebra
import QuaternionAlgebra
66 from sage
.categories
.magmatic_algebras
import MagmaticAlgebras
67 from sage
.categories
.sets_cat
import cartesian_product
68 from sage
.combinat
.free_module
import CombinatorialFreeModule
69 from sage
.matrix
.constructor
import matrix
70 from sage
.matrix
.matrix_space
import MatrixSpace
71 from sage
.misc
.cachefunc
import cached_method
72 from sage
.misc
.table
import table
73 from sage
.modules
.free_module
import FreeModule
, VectorSpace
74 from sage
.rings
.all
import (ZZ
, QQ
, AA
, QQbar
, RR
, RLF
, CLF
,
77 from mjo
.eja
.eja_element
import FiniteDimensionalEJAElement
78 from mjo
.eja
.eja_operator
import FiniteDimensionalEJAOperator
79 from mjo
.eja
.eja_utils
import _all2list
, _mat2vec
81 class FiniteDimensionalEJA(CombinatorialFreeModule
):
83 A finite-dimensional Euclidean Jordan algebra.
87 - ``basis`` -- a tuple; a tuple of basis elements in "matrix
88 form," which must be the same form as the arguments to
89 ``jordan_product`` and ``inner_product``. In reality, "matrix
90 form" can be either vectors, matrices, or a Cartesian product
91 (ordered tuple) of vectors or matrices. All of these would
92 ideally be vector spaces in sage with no special-casing
93 needed; but in reality we turn vectors into column-matrices
94 and Cartesian products `(a,b)` into column matrices
95 `(a,b)^{T}` after converting `a` and `b` themselves.
97 - ``jordan_product`` -- a function; afunction of two ``basis``
98 elements (in matrix form) that returns their jordan product,
99 also in matrix form; this will be applied to ``basis`` to
100 compute a multiplication table for the algebra.
102 - ``inner_product`` -- a function; a function of two ``basis``
103 elements (in matrix form) that returns their inner
104 product. This will be applied to ``basis`` to compute an
105 inner-product table (basically a matrix) for this algebra.
107 - ``field`` -- a subfield of the reals (default: ``AA``); the scalar
108 field for the algebra.
110 - ``orthonormalize`` -- boolean (default: ``True``); whether or
111 not to orthonormalize the basis. Doing so is expensive and
112 generally rules out using the rationals as your ``field``, but
113 is required for spectral decompositions.
117 sage: from mjo.eja.eja_algebra import random_eja
121 We should compute that an element subalgebra is associative even
122 if we circumvent the element method::
124 sage: set_random_seed()
125 sage: J = random_eja(field=QQ,orthonormalize=False)
126 sage: x = J.random_element()
127 sage: A = x.subalgebra_generated_by(orthonormalize=False)
128 sage: basis = tuple(b.superalgebra_element() for b in A.basis())
129 sage: J.subalgebra(basis, orthonormalize=False).is_associative()
133 Element
= FiniteDimensionalEJAElement
142 cartesian_product
=False,
150 if not field
.is_subring(RR
):
151 # Note: this does return true for the real algebraic
152 # field, the rationals, and any quadratic field where
153 # we've specified a real embedding.
154 raise ValueError("scalar field is not real")
157 # Check commutativity of the Jordan and inner-products.
158 # This has to be done before we build the multiplication
159 # and inner-product tables/matrices, because we take
160 # advantage of symmetry in the process.
161 if not all( jordan_product(bi
,bj
) == jordan_product(bj
,bi
)
164 raise ValueError("Jordan product is not commutative")
166 if not all( inner_product(bi
,bj
) == inner_product(bj
,bi
)
169 raise ValueError("inner-product is not commutative")
172 category
= MagmaticAlgebras(field
).FiniteDimensional()
173 category
= category
.WithBasis().Unital().Commutative()
175 if associative
is None:
176 # We should figure it out. As with check_axioms, we have to do
177 # this without the help of the _jordan_product_is_associative()
178 # method because we need to know the category before we
179 # initialize the algebra.
180 associative
= all( jordan_product(jordan_product(bi
,bj
),bk
)
182 jordan_product(bi
,jordan_product(bj
,bk
))
188 # Element subalgebras can take advantage of this.
189 category
= category
.Associative()
190 if cartesian_product
:
191 # Use join() here because otherwise we only get the
192 # "Cartesian product of..." and not the things themselves.
193 category
= category
.join([category
,
194 category
.CartesianProducts()])
196 # Call the superclass constructor so that we can use its from_vector()
197 # method to build our multiplication table.
198 CombinatorialFreeModule
.__init
__(self
,
205 # Now comes all of the hard work. We'll be constructing an
206 # ambient vector space V that our (vectorized) basis lives in,
207 # as well as a subspace W of V spanned by those (vectorized)
208 # basis elements. The W-coordinates are the coefficients that
209 # we see in things like x = 1*b1 + 2*b2.
214 degree
= len(_all2list(basis
[0]))
216 # Build an ambient space that fits our matrix basis when
217 # written out as "long vectors."
218 V
= VectorSpace(field
, degree
)
220 # The matrix that will hole the orthonormal -> unorthonormal
221 # coordinate transformation.
222 self
._deortho
_matrix
= None
225 # Save a copy of the un-orthonormalized basis for later.
226 # Convert it to ambient V (vector) coordinates while we're
227 # at it, because we'd have to do it later anyway.
228 deortho_vector_basis
= tuple( V(_all2list(b
)) for b
in basis
)
230 from mjo
.eja
.eja_utils
import gram_schmidt
231 basis
= tuple(gram_schmidt(basis
, inner_product
))
233 # Save the (possibly orthonormalized) matrix basis for
235 self
._matrix
_basis
= basis
237 # Now create the vector space for the algebra, which will have
238 # its own set of non-ambient coordinates (in terms of the
240 vector_basis
= tuple( V(_all2list(b
)) for b
in basis
)
241 W
= V
.span_of_basis( vector_basis
, check
=check_axioms
)
244 # Now "W" is the vector space of our algebra coordinates. The
245 # variables "X1", "X2",... refer to the entries of vectors in
246 # W. Thus to convert back and forth between the orthonormal
247 # coordinates and the given ones, we need to stick the original
249 U
= V
.span_of_basis( deortho_vector_basis
, check
=check_axioms
)
250 self
._deortho
_matrix
= matrix( U
.coordinate_vector(q
)
251 for q
in vector_basis
)
254 # Now we actually compute the multiplication and inner-product
255 # tables/matrices using the possibly-orthonormalized basis.
256 self
._inner
_product
_matrix
= matrix
.identity(field
, n
)
257 self
._multiplication
_table
= [ [0 for j
in range(i
+1)]
260 # Note: the Jordan and inner-products are defined in terms
261 # of the ambient basis. It's important that their arguments
262 # are in ambient coordinates as well.
265 # ortho basis w.r.t. ambient coords
269 # The jordan product returns a matrixy answer, so we
270 # have to convert it to the algebra coordinates.
271 elt
= jordan_product(q_i
, q_j
)
272 elt
= W
.coordinate_vector(V(_all2list(elt
)))
273 self
._multiplication
_table
[i
][j
] = self
.from_vector(elt
)
275 if not orthonormalize
:
276 # If we're orthonormalizing the basis with respect
277 # to an inner-product, then the inner-product
278 # matrix with respect to the resulting basis is
279 # just going to be the identity.
280 ip
= inner_product(q_i
, q_j
)
281 self
._inner
_product
_matrix
[i
,j
] = ip
282 self
._inner
_product
_matrix
[j
,i
] = ip
284 self
._inner
_product
_matrix
._cache
= {'hermitian': True}
285 self
._inner
_product
_matrix
.set_immutable()
288 if not self
._is
_jordanian
():
289 raise ValueError("Jordan identity does not hold")
290 if not self
._inner
_product
_is
_associative
():
291 raise ValueError("inner product is not associative")
294 def _coerce_map_from_base_ring(self
):
296 Disable the map from the base ring into the algebra.
298 Performing a nonsense conversion like this automatically
299 is counterpedagogical. The fallback is to try the usual
300 element constructor, which should also fail.
304 sage: from mjo.eja.eja_algebra import random_eja
308 sage: set_random_seed()
309 sage: J = random_eja()
311 Traceback (most recent call last):
313 ValueError: not an element of this algebra
319 def product_on_basis(self
, i
, j
):
321 Returns the Jordan product of the `i` and `j`th basis elements.
323 This completely defines the Jordan product on the algebra, and
324 is used direclty by our superclass machinery to implement
329 sage: from mjo.eja.eja_algebra import random_eja
333 sage: set_random_seed()
334 sage: J = random_eja()
335 sage: n = J.dimension()
338 sage: bi_bj = J.zero()*J.zero()
340 ....: i = ZZ.random_element(n)
341 ....: j = ZZ.random_element(n)
342 ....: bi = J.monomial(i)
343 ....: bj = J.monomial(j)
344 ....: bi_bj = J.product_on_basis(i,j)
349 # We only stored the lower-triangular portion of the
350 # multiplication table.
352 return self
._multiplication
_table
[i
][j
]
354 return self
._multiplication
_table
[j
][i
]
356 def inner_product(self
, x
, y
):
358 The inner product associated with this Euclidean Jordan algebra.
360 Defaults to the trace inner product, but can be overridden by
361 subclasses if they are sure that the necessary properties are
366 sage: from mjo.eja.eja_algebra import (random_eja,
368 ....: BilinearFormEJA)
372 Our inner product is "associative," which means the following for
373 a symmetric bilinear form::
375 sage: set_random_seed()
376 sage: J = random_eja()
377 sage: x,y,z = J.random_elements(3)
378 sage: (x*y).inner_product(z) == y.inner_product(x*z)
383 Ensure that this is the usual inner product for the algebras
386 sage: set_random_seed()
387 sage: J = HadamardEJA.random_instance()
388 sage: x,y = J.random_elements(2)
389 sage: actual = x.inner_product(y)
390 sage: expected = x.to_vector().inner_product(y.to_vector())
391 sage: actual == expected
394 Ensure that this is one-half of the trace inner-product in a
395 BilinearFormEJA that isn't just the reals (when ``n`` isn't
396 one). This is in Faraut and Koranyi, and also my "On the
399 sage: set_random_seed()
400 sage: J = BilinearFormEJA.random_instance()
401 sage: n = J.dimension()
402 sage: x = J.random_element()
403 sage: y = J.random_element()
404 sage: (n == 1) or (x.inner_product(y) == (x*y).trace()/2)
408 B
= self
._inner
_product
_matrix
409 return (B
*x
.to_vector()).inner_product(y
.to_vector())
412 def is_associative(self
):
414 Return whether or not this algebra's Jordan product is associative.
418 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
422 sage: J = ComplexHermitianEJA(3, field=QQ, orthonormalize=False)
423 sage: J.is_associative()
425 sage: x = sum(J.gens())
426 sage: A = x.subalgebra_generated_by(orthonormalize=False)
427 sage: A.is_associative()
431 return "Associative" in self
.category().axioms()
433 def _is_commutative(self
):
435 Whether or not this algebra's multiplication table is commutative.
437 This method should of course always return ``True``, unless
438 this algebra was constructed with ``check_axioms=False`` and
439 passed an invalid multiplication table.
441 return all( x
*y
== y
*x
for x
in self
.gens() for y
in self
.gens() )
443 def _is_jordanian(self
):
445 Whether or not this algebra's multiplication table respects the
446 Jordan identity `(x^{2})(xy) = x(x^{2}y)`.
448 We only check one arrangement of `x` and `y`, so for a
449 ``True`` result to be truly true, you should also check
450 :meth:`_is_commutative`. This method should of course always
451 return ``True``, unless this algebra was constructed with
452 ``check_axioms=False`` and passed an invalid multiplication table.
454 return all( (self
.monomial(i
)**2)*(self
.monomial(i
)*self
.monomial(j
))
456 (self
.monomial(i
))*((self
.monomial(i
)**2)*self
.monomial(j
))
457 for i
in range(self
.dimension())
458 for j
in range(self
.dimension()) )
460 def _jordan_product_is_associative(self
):
462 Return whether or not this algebra's Jordan product is
463 associative; that is, whether or not `x*(y*z) = (x*y)*z`
466 This method should agree with :meth:`is_associative` unless
467 you lied about the value of the ``associative`` parameter
468 when you constructed the algebra.
472 sage: from mjo.eja.eja_algebra import (random_eja,
473 ....: RealSymmetricEJA,
474 ....: ComplexHermitianEJA,
475 ....: QuaternionHermitianEJA)
479 sage: J = RealSymmetricEJA(4, orthonormalize=False)
480 sage: J._jordan_product_is_associative()
482 sage: x = sum(J.gens())
483 sage: A = x.subalgebra_generated_by()
484 sage: A._jordan_product_is_associative()
489 sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
490 sage: J._jordan_product_is_associative()
492 sage: x = sum(J.gens())
493 sage: A = x.subalgebra_generated_by(orthonormalize=False)
494 sage: A._jordan_product_is_associative()
499 sage: J = QuaternionHermitianEJA(2)
500 sage: J._jordan_product_is_associative()
502 sage: x = sum(J.gens())
503 sage: A = x.subalgebra_generated_by()
504 sage: A._jordan_product_is_associative()
509 The values we've presupplied to the constructors agree with
512 sage: set_random_seed()
513 sage: J = random_eja()
514 sage: J.is_associative() == J._jordan_product_is_associative()
520 # Used to check whether or not something is zero.
523 # I don't know of any examples that make this magnitude
524 # necessary because I don't know how to make an
525 # associative algebra when the element subalgebra
526 # construction is unreliable (as it is over RDF; we can't
527 # find the degree of an element because we can't compute
528 # the rank of a matrix). But even multiplication of floats
529 # is non-associative, so *some* epsilon is needed... let's
530 # just take the one from _inner_product_is_associative?
533 for i
in range(self
.dimension()):
534 for j
in range(self
.dimension()):
535 for k
in range(self
.dimension()):
539 diff
= (x
*y
)*z
- x
*(y
*z
)
541 if diff
.norm() > epsilon
:
546 def _inner_product_is_associative(self
):
548 Return whether or not this algebra's inner product `B` is
549 associative; that is, whether or not `B(xy,z) = B(x,yz)`.
551 This method should of course always return ``True``, unless
552 this algebra was constructed with ``check_axioms=False`` and
553 passed an invalid Jordan or inner-product.
557 # Used to check whether or not something is zero.
560 # This choice is sufficient to allow the construction of
561 # QuaternionHermitianEJA(2, field=RDF) with check_axioms=True.
564 for i
in range(self
.dimension()):
565 for j
in range(self
.dimension()):
566 for k
in range(self
.dimension()):
570 diff
= (x
*y
).inner_product(z
) - x
.inner_product(y
*z
)
572 if diff
.abs() > epsilon
:
577 def _element_constructor_(self
, elt
):
579 Construct an element of this algebra from its vector or matrix
582 This gets called only after the parent element _call_ method
583 fails to find a coercion for the argument.
587 sage: from mjo.eja.eja_algebra import (random_eja,
590 ....: RealSymmetricEJA)
594 The identity in `S^n` is converted to the identity in the EJA::
596 sage: J = RealSymmetricEJA(3)
597 sage: I = matrix.identity(QQ,3)
598 sage: J(I) == J.one()
601 This skew-symmetric matrix can't be represented in the EJA::
603 sage: J = RealSymmetricEJA(3)
604 sage: A = matrix(QQ,3, lambda i,j: i-j)
606 Traceback (most recent call last):
608 ValueError: not an element of this algebra
610 Tuples work as well, provided that the matrix basis for the
611 algebra consists of them::
613 sage: J1 = HadamardEJA(3)
614 sage: J2 = RealSymmetricEJA(2)
615 sage: J = cartesian_product([J1,J2])
616 sage: J( (J1.matrix_basis()[1], J2.matrix_basis()[2]) )
621 Ensure that we can convert any element back and forth
622 faithfully between its matrix and algebra representations::
624 sage: set_random_seed()
625 sage: J = random_eja()
626 sage: x = J.random_element()
627 sage: J(x.to_matrix()) == x
630 We cannot coerce elements between algebras just because their
631 matrix representations are compatible::
633 sage: J1 = HadamardEJA(3)
634 sage: J2 = JordanSpinEJA(3)
636 Traceback (most recent call last):
638 ValueError: not an element of this algebra
640 Traceback (most recent call last):
642 ValueError: not an element of this algebra
644 msg
= "not an element of this algebra"
645 if elt
in self
.base_ring():
646 # Ensure that no base ring -> algebra coercion is performed
647 # by this method. There's some stupidity in sage that would
648 # otherwise propagate to this method; for example, sage thinks
649 # that the integer 3 belongs to the space of 2-by-2 matrices.
650 raise ValueError(msg
)
653 # Try to convert a vector into a column-matrix...
655 except (AttributeError, TypeError):
656 # and ignore failure, because we weren't really expecting
657 # a vector as an argument anyway.
660 if elt
not in self
.matrix_space():
661 raise ValueError(msg
)
663 # Thanks for nothing! Matrix spaces aren't vector spaces in
664 # Sage, so we have to figure out its matrix-basis coordinates
665 # ourselves. We use the basis space's ring instead of the
666 # element's ring because the basis space might be an algebraic
667 # closure whereas the base ring of the 3-by-3 identity matrix
668 # could be QQ instead of QQbar.
670 # And, we also have to handle Cartesian product bases (when
671 # the matrix basis consists of tuples) here. The "good news"
672 # is that we're already converting everything to long vectors,
673 # and that strategy works for tuples as well.
675 # We pass check=False because the matrix basis is "guaranteed"
676 # to be linearly independent... right? Ha ha.
678 V
= VectorSpace(self
.base_ring(), len(elt
))
679 W
= V
.span_of_basis( (V(_all2list(s
)) for s
in self
.matrix_basis()),
683 coords
= W
.coordinate_vector(V(elt
))
684 except ArithmeticError: # vector is not in free module
685 raise ValueError(msg
)
687 return self
.from_vector(coords
)
691 Return a string representation of ``self``.
695 sage: from mjo.eja.eja_algebra import JordanSpinEJA
699 Ensure that it says what we think it says::
701 sage: JordanSpinEJA(2, field=AA)
702 Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
703 sage: JordanSpinEJA(3, field=RDF)
704 Euclidean Jordan algebra of dimension 3 over Real Double Field
707 fmt
= "Euclidean Jordan algebra of dimension {} over {}"
708 return fmt
.format(self
.dimension(), self
.base_ring())
712 def characteristic_polynomial_of(self
):
714 Return the algebra's "characteristic polynomial of" function,
715 which is itself a multivariate polynomial that, when evaluated
716 at the coordinates of some algebra element, returns that
717 element's characteristic polynomial.
719 The resulting polynomial has `n+1` variables, where `n` is the
720 dimension of this algebra. The first `n` variables correspond to
721 the coordinates of an algebra element: when evaluated at the
722 coordinates of an algebra element with respect to a certain
723 basis, the result is a univariate polynomial (in the one
724 remaining variable ``t``), namely the characteristic polynomial
729 sage: from mjo.eja.eja_algebra import JordanSpinEJA, TrivialEJA
733 The characteristic polynomial in the spin algebra is given in
734 Alizadeh, Example 11.11::
736 sage: J = JordanSpinEJA(3)
737 sage: p = J.characteristic_polynomial_of(); p
738 X1^2 - X2^2 - X3^2 + (-2*t)*X1 + t^2
739 sage: xvec = J.one().to_vector()
743 By definition, the characteristic polynomial is a monic
744 degree-zero polynomial in a rank-zero algebra. Note that
745 Cayley-Hamilton is indeed satisfied since the polynomial
746 ``1`` evaluates to the identity element of the algebra on
749 sage: J = TrivialEJA()
750 sage: J.characteristic_polynomial_of()
757 # The list of coefficient polynomials a_0, a_1, a_2, ..., a_(r-1).
758 a
= self
._charpoly
_coefficients
()
760 # We go to a bit of trouble here to reorder the
761 # indeterminates, so that it's easier to evaluate the
762 # characteristic polynomial at x's coordinates and get back
763 # something in terms of t, which is what we want.
764 S
= PolynomialRing(self
.base_ring(),'t')
768 S
= PolynomialRing(S
, R
.variable_names())
771 return (t
**r
+ sum( a
[k
]*(t
**k
) for k
in range(r
) ))
773 def coordinate_polynomial_ring(self
):
775 The multivariate polynomial ring in which this algebra's
776 :meth:`characteristic_polynomial_of` lives.
780 sage: from mjo.eja.eja_algebra import (HadamardEJA,
781 ....: RealSymmetricEJA)
785 sage: J = HadamardEJA(2)
786 sage: J.coordinate_polynomial_ring()
787 Multivariate Polynomial Ring in X1, X2...
788 sage: J = RealSymmetricEJA(3,field=QQ,orthonormalize=False)
789 sage: J.coordinate_polynomial_ring()
790 Multivariate Polynomial Ring in X1, X2, X3, X4, X5, X6...
793 var_names
= tuple( "X%d" % z
for z
in range(1, self
.dimension()+1) )
794 return PolynomialRing(self
.base_ring(), var_names
)
796 def inner_product(self
, x
, y
):
798 The inner product associated with this Euclidean Jordan algebra.
800 Defaults to the trace inner product, but can be overridden by
801 subclasses if they are sure that the necessary properties are
806 sage: from mjo.eja.eja_algebra import (random_eja,
808 ....: BilinearFormEJA)
812 Our inner product is "associative," which means the following for
813 a symmetric bilinear form::
815 sage: set_random_seed()
816 sage: J = random_eja()
817 sage: x,y,z = J.random_elements(3)
818 sage: (x*y).inner_product(z) == y.inner_product(x*z)
823 Ensure that this is the usual inner product for the algebras
826 sage: set_random_seed()
827 sage: J = HadamardEJA.random_instance()
828 sage: x,y = J.random_elements(2)
829 sage: actual = x.inner_product(y)
830 sage: expected = x.to_vector().inner_product(y.to_vector())
831 sage: actual == expected
834 Ensure that this is one-half of the trace inner-product in a
835 BilinearFormEJA that isn't just the reals (when ``n`` isn't
836 one). This is in Faraut and Koranyi, and also my "On the
839 sage: set_random_seed()
840 sage: J = BilinearFormEJA.random_instance()
841 sage: n = J.dimension()
842 sage: x = J.random_element()
843 sage: y = J.random_element()
844 sage: (n == 1) or (x.inner_product(y) == (x*y).trace()/2)
847 B
= self
._inner
_product
_matrix
848 return (B
*x
.to_vector()).inner_product(y
.to_vector())
851 def is_trivial(self
):
853 Return whether or not this algebra is trivial.
855 A trivial algebra contains only the zero element.
859 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
864 sage: J = ComplexHermitianEJA(3)
870 sage: J = TrivialEJA()
875 return self
.dimension() == 0
878 def multiplication_table(self
):
880 Return a visual representation of this algebra's multiplication
881 table (on basis elements).
885 sage: from mjo.eja.eja_algebra import JordanSpinEJA
889 sage: J = JordanSpinEJA(4)
890 sage: J.multiplication_table()
891 +----++----+----+----+----+
892 | * || b0 | b1 | b2 | b3 |
893 +====++====+====+====+====+
894 | b0 || b0 | b1 | b2 | b3 |
895 +----++----+----+----+----+
896 | b1 || b1 | b0 | 0 | 0 |
897 +----++----+----+----+----+
898 | b2 || b2 | 0 | b0 | 0 |
899 +----++----+----+----+----+
900 | b3 || b3 | 0 | 0 | b0 |
901 +----++----+----+----+----+
905 # Prepend the header row.
906 M
= [["*"] + list(self
.gens())]
908 # And to each subsequent row, prepend an entry that belongs to
909 # the left-side "header column."
910 M
+= [ [self
.monomial(i
)] + [ self
.monomial(i
)*self
.monomial(j
)
914 return table(M
, header_row
=True, header_column
=True, frame
=True)
917 def matrix_basis(self
):
919 Return an (often more natural) representation of this algebras
920 basis as an ordered tuple of matrices.
922 Every finite-dimensional Euclidean Jordan Algebra is a, up to
923 Jordan isomorphism, a direct sum of five simple
924 algebras---four of which comprise Hermitian matrices. And the
925 last type of algebra can of course be thought of as `n`-by-`1`
926 column matrices (ambiguusly called column vectors) to avoid
927 special cases. As a result, matrices (and column vectors) are
928 a natural representation format for Euclidean Jordan algebra
931 But, when we construct an algebra from a basis of matrices,
932 those matrix representations are lost in favor of coordinate
933 vectors *with respect to* that basis. We could eventually
934 convert back if we tried hard enough, but having the original
935 representations handy is valuable enough that we simply store
936 them and return them from this method.
938 Why implement this for non-matrix algebras? Avoiding special
939 cases for the :class:`BilinearFormEJA` pays with simplicity in
940 its own right. But mainly, we would like to be able to assume
941 that elements of a :class:`CartesianProductEJA` can be displayed
942 nicely, without having to have special classes for direct sums
943 one of whose components was a matrix algebra.
947 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
948 ....: RealSymmetricEJA)
952 sage: J = RealSymmetricEJA(2)
954 Finite family {0: b0, 1: b1, 2: b2}
955 sage: J.matrix_basis()
957 [1 0] [ 0 0.7071067811865475?] [0 0]
958 [0 0], [0.7071067811865475? 0], [0 1]
963 sage: J = JordanSpinEJA(2)
965 Finite family {0: b0, 1: b1}
966 sage: J.matrix_basis()
972 return self
._matrix
_basis
975 def matrix_space(self
):
977 Return the matrix space in which this algebra's elements live, if
978 we think of them as matrices (including column vectors of the
981 "By default" this will be an `n`-by-`1` column-matrix space,
982 except when the algebra is trivial. There it's `n`-by-`n`
983 (where `n` is zero), to ensure that two elements of the matrix
984 space (empty matrices) can be multiplied. For algebras of
985 matrices, this returns the space in which their
986 real embeddings live.
990 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
992 ....: QuaternionHermitianEJA,
997 By default, the matrix representation is just a column-matrix
998 equivalent to the vector representation::
1000 sage: J = JordanSpinEJA(3)
1001 sage: J.matrix_space()
1002 Full MatrixSpace of 3 by 1 dense matrices over Algebraic
1005 The matrix representation in the trivial algebra is
1006 zero-by-zero instead of the usual `n`-by-one::
1008 sage: J = TrivialEJA()
1009 sage: J.matrix_space()
1010 Full MatrixSpace of 0 by 0 dense matrices over Algebraic
1013 The matrix space for complex/quaternion Hermitian matrix EJA
1014 is the space in which their real-embeddings live, not the
1015 original complex/quaternion matrix space::
1017 sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
1018 sage: J.matrix_space()
1019 Full MatrixSpace of 4 by 4 dense matrices over Rational Field
1020 sage: J = QuaternionHermitianEJA(1,field=QQ,orthonormalize=False)
1021 sage: J.matrix_space()
1022 Module of 1 by 1 matrices with entries in Quaternion
1023 Algebra (-1, -1) with base ring Rational Field over
1024 the scalar ring Rational Field
1027 if self
.is_trivial():
1028 return MatrixSpace(self
.base_ring(), 0)
1030 return self
.matrix_basis()[0].parent()
1036 Return the unit element of this algebra.
1040 sage: from mjo.eja.eja_algebra import (HadamardEJA,
1045 We can compute unit element in the Hadamard EJA::
1047 sage: J = HadamardEJA(5)
1049 b0 + b1 + b2 + b3 + b4
1051 The unit element in the Hadamard EJA is inherited in the
1052 subalgebras generated by its elements::
1054 sage: J = HadamardEJA(5)
1056 b0 + b1 + b2 + b3 + b4
1057 sage: x = sum(J.gens())
1058 sage: A = x.subalgebra_generated_by(orthonormalize=False)
1061 sage: A.one().superalgebra_element()
1062 b0 + b1 + b2 + b3 + b4
1066 The identity element acts like the identity, regardless of
1067 whether or not we orthonormalize::
1069 sage: set_random_seed()
1070 sage: J = random_eja()
1071 sage: x = J.random_element()
1072 sage: J.one()*x == x and x*J.one() == x
1074 sage: A = x.subalgebra_generated_by()
1075 sage: y = A.random_element()
1076 sage: A.one()*y == y and y*A.one() == y
1081 sage: set_random_seed()
1082 sage: J = random_eja(field=QQ, orthonormalize=False)
1083 sage: x = J.random_element()
1084 sage: J.one()*x == x and x*J.one() == x
1086 sage: A = x.subalgebra_generated_by(orthonormalize=False)
1087 sage: y = A.random_element()
1088 sage: A.one()*y == y and y*A.one() == y
1091 The matrix of the unit element's operator is the identity,
1092 regardless of the base field and whether or not we
1095 sage: set_random_seed()
1096 sage: J = random_eja()
1097 sage: actual = J.one().operator().matrix()
1098 sage: expected = matrix.identity(J.base_ring(), J.dimension())
1099 sage: actual == expected
1101 sage: x = J.random_element()
1102 sage: A = x.subalgebra_generated_by()
1103 sage: actual = A.one().operator().matrix()
1104 sage: expected = matrix.identity(A.base_ring(), A.dimension())
1105 sage: actual == expected
1110 sage: set_random_seed()
1111 sage: J = random_eja(field=QQ, orthonormalize=False)
1112 sage: actual = J.one().operator().matrix()
1113 sage: expected = matrix.identity(J.base_ring(), J.dimension())
1114 sage: actual == expected
1116 sage: x = J.random_element()
1117 sage: A = x.subalgebra_generated_by(orthonormalize=False)
1118 sage: actual = A.one().operator().matrix()
1119 sage: expected = matrix.identity(A.base_ring(), A.dimension())
1120 sage: actual == expected
1123 Ensure that the cached unit element (often precomputed by
1124 hand) agrees with the computed one::
1126 sage: set_random_seed()
1127 sage: J = random_eja()
1128 sage: cached = J.one()
1129 sage: J.one.clear_cache()
1130 sage: J.one() == cached
1135 sage: set_random_seed()
1136 sage: J = random_eja(field=QQ, orthonormalize=False)
1137 sage: cached = J.one()
1138 sage: J.one.clear_cache()
1139 sage: J.one() == cached
1143 # We can brute-force compute the matrices of the operators
1144 # that correspond to the basis elements of this algebra.
1145 # If some linear combination of those basis elements is the
1146 # algebra identity, then the same linear combination of
1147 # their matrices has to be the identity matrix.
1149 # Of course, matrices aren't vectors in sage, so we have to
1150 # appeal to the "long vectors" isometry.
1151 oper_vecs
= [ _mat2vec(g
.operator().matrix()) for g
in self
.gens() ]
1153 # Now we use basic linear algebra to find the coefficients,
1154 # of the matrices-as-vectors-linear-combination, which should
1155 # work for the original algebra basis too.
1156 A
= matrix(self
.base_ring(), oper_vecs
)
1158 # We used the isometry on the left-hand side already, but we
1159 # still need to do it for the right-hand side. Recall that we
1160 # wanted something that summed to the identity matrix.
1161 b
= _mat2vec( matrix
.identity(self
.base_ring(), self
.dimension()) )
1163 # Now if there's an identity element in the algebra, this
1164 # should work. We solve on the left to avoid having to
1165 # transpose the matrix "A".
1166 return self
.from_vector(A
.solve_left(b
))
1169 def peirce_decomposition(self
, c
):
1171 The Peirce decomposition of this algebra relative to the
1174 In the future, this can be extended to a complete system of
1175 orthogonal idempotents.
1179 - ``c`` -- an idempotent of this algebra.
1183 A triple (J0, J5, J1) containing two subalgebras and one subspace
1186 - ``J0`` -- the algebra on the eigenspace of ``c.operator()``
1187 corresponding to the eigenvalue zero.
1189 - ``J5`` -- the eigenspace (NOT a subalgebra) of ``c.operator()``
1190 corresponding to the eigenvalue one-half.
1192 - ``J1`` -- the algebra on the eigenspace of ``c.operator()``
1193 corresponding to the eigenvalue one.
1195 These are the only possible eigenspaces for that operator, and this
1196 algebra is a direct sum of them. The spaces ``J0`` and ``J1`` are
1197 orthogonal, and are subalgebras of this algebra with the appropriate
1202 sage: from mjo.eja.eja_algebra import random_eja, RealSymmetricEJA
1206 The canonical example comes from the symmetric matrices, which
1207 decompose into diagonal and off-diagonal parts::
1209 sage: J = RealSymmetricEJA(3)
1210 sage: C = matrix(QQ, [ [1,0,0],
1214 sage: J0,J5,J1 = J.peirce_decomposition(c)
1216 Euclidean Jordan algebra of dimension 1...
1218 Vector space of degree 6 and dimension 2...
1220 Euclidean Jordan algebra of dimension 3...
1221 sage: J0.one().to_matrix()
1225 sage: orig_df = AA.options.display_format
1226 sage: AA.options.display_format = 'radical'
1227 sage: J.from_vector(J5.basis()[0]).to_matrix()
1231 sage: J.from_vector(J5.basis()[1]).to_matrix()
1235 sage: AA.options.display_format = orig_df
1236 sage: J1.one().to_matrix()
1243 Every algebra decomposes trivially with respect to its identity
1246 sage: set_random_seed()
1247 sage: J = random_eja()
1248 sage: J0,J5,J1 = J.peirce_decomposition(J.one())
1249 sage: J0.dimension() == 0 and J5.dimension() == 0
1251 sage: J1.superalgebra() == J and J1.dimension() == J.dimension()
1254 The decomposition is into eigenspaces, and its components are
1255 therefore necessarily orthogonal. Moreover, the identity
1256 elements in the two subalgebras are the projections onto their
1257 respective subspaces of the superalgebra's identity element::
1259 sage: set_random_seed()
1260 sage: J = random_eja()
1261 sage: x = J.random_element()
1262 sage: if not J.is_trivial():
1263 ....: while x.is_nilpotent():
1264 ....: x = J.random_element()
1265 sage: c = x.subalgebra_idempotent()
1266 sage: J0,J5,J1 = J.peirce_decomposition(c)
1268 sage: for (w,y,z) in zip(J0.basis(), J5.basis(), J1.basis()):
1269 ....: w = w.superalgebra_element()
1270 ....: y = J.from_vector(y)
1271 ....: z = z.superalgebra_element()
1272 ....: ipsum += w.inner_product(y).abs()
1273 ....: ipsum += w.inner_product(z).abs()
1274 ....: ipsum += y.inner_product(z).abs()
1277 sage: J1(c) == J1.one()
1279 sage: J0(J.one() - c) == J0.one()
1283 if not c
.is_idempotent():
1284 raise ValueError("element is not idempotent: %s" % c
)
1286 # Default these to what they should be if they turn out to be
1287 # trivial, because eigenspaces_left() won't return eigenvalues
1288 # corresponding to trivial spaces (e.g. it returns only the
1289 # eigenspace corresponding to lambda=1 if you take the
1290 # decomposition relative to the identity element).
1291 trivial
= self
.subalgebra(())
1292 J0
= trivial
# eigenvalue zero
1293 J5
= VectorSpace(self
.base_ring(), 0) # eigenvalue one-half
1294 J1
= trivial
# eigenvalue one
1296 for (eigval
, eigspace
) in c
.operator().matrix().right_eigenspaces():
1297 if eigval
== ~
(self
.base_ring()(2)):
1300 gens
= tuple( self
.from_vector(b
) for b
in eigspace
.basis() )
1301 subalg
= self
.subalgebra(gens
, check_axioms
=False)
1307 raise ValueError("unexpected eigenvalue: %s" % eigval
)
1312 def random_element(self
, thorough
=False):
1314 Return a random element of this algebra.
1316 Our algebra superclass method only returns a linear
1317 combination of at most two basis elements. We instead
1318 want the vector space "random element" method that
1319 returns a more diverse selection.
1323 - ``thorough`` -- (boolean; default False) whether or not we
1324 should generate irrational coefficients for the random
1325 element when our base ring is irrational; this slows the
1326 algebra operations to a crawl, but any truly random method
1330 # For a general base ring... maybe we can trust this to do the
1331 # right thing? Unlikely, but.
1332 V
= self
.vector_space()
1333 v
= V
.random_element()
1335 if self
.base_ring() is AA
:
1336 # The "random element" method of the algebraic reals is
1337 # stupid at the moment, and only returns integers between
1338 # -2 and 2, inclusive:
1340 # https://trac.sagemath.org/ticket/30875
1342 # Instead, we implement our own "random vector" method,
1343 # and then coerce that into the algebra. We use the vector
1344 # space degree here instead of the dimension because a
1345 # subalgebra could (for example) be spanned by only two
1346 # vectors, each with five coordinates. We need to
1347 # generate all five coordinates.
1349 v
*= QQbar
.random_element().real()
1351 v
*= QQ
.random_element()
1353 return self
.from_vector(V
.coordinate_vector(v
))
1355 def random_elements(self
, count
, thorough
=False):
1357 Return ``count`` random elements as a tuple.
1361 - ``thorough`` -- (boolean; default False) whether or not we
1362 should generate irrational coefficients for the random
1363 elements when our base ring is irrational; this slows the
1364 algebra operations to a crawl, but any truly random method
1369 sage: from mjo.eja.eja_algebra import JordanSpinEJA
1373 sage: J = JordanSpinEJA(3)
1374 sage: x,y,z = J.random_elements(3)
1375 sage: all( [ x in J, y in J, z in J ])
1377 sage: len( J.random_elements(10) ) == 10
1381 return tuple( self
.random_element(thorough
)
1382 for idx
in range(count
) )
1386 def _charpoly_coefficients(self
):
1388 The `r` polynomial coefficients of the "characteristic polynomial
1393 sage: from mjo.eja.eja_algebra import random_eja
1397 The theory shows that these are all homogeneous polynomials of
1400 sage: set_random_seed()
1401 sage: J = random_eja()
1402 sage: all(p.is_homogeneous() for p in J._charpoly_coefficients())
1406 n
= self
.dimension()
1407 R
= self
.coordinate_polynomial_ring()
1409 F
= R
.fraction_field()
1412 # From a result in my book, these are the entries of the
1413 # basis representation of L_x.
1414 return sum( vars[k
]*self
.monomial(k
).operator().matrix()[i
,j
]
1417 L_x
= matrix(F
, n
, n
, L_x_i_j
)
1420 if self
.rank
.is_in_cache():
1422 # There's no need to pad the system with redundant
1423 # columns if we *know* they'll be redundant.
1426 # Compute an extra power in case the rank is equal to
1427 # the dimension (otherwise, we would stop at x^(r-1)).
1428 x_powers
= [ (L_x
**k
)*self
.one().to_vector()
1429 for k
in range(n
+1) ]
1430 A
= matrix
.column(F
, x_powers
[:n
])
1431 AE
= A
.extended_echelon_form()
1438 # The theory says that only the first "r" coefficients are
1439 # nonzero, and they actually live in the original polynomial
1440 # ring and not the fraction field. We negate them because in
1441 # the actual characteristic polynomial, they get moved to the
1442 # other side where x^r lives. We don't bother to trim A_rref
1443 # down to a square matrix and solve the resulting system,
1444 # because the upper-left r-by-r portion of A_rref is
1445 # guaranteed to be the identity matrix, so e.g.
1447 # A_rref.solve_right(Y)
1449 # would just be returning Y.
1450 return (-E
*b
)[:r
].change_ring(R
)
1455 Return the rank of this EJA.
1457 This is a cached method because we know the rank a priori for
1458 all of the algebras we can construct. Thus we can avoid the
1459 expensive ``_charpoly_coefficients()`` call unless we truly
1460 need to compute the whole characteristic polynomial.
1464 sage: from mjo.eja.eja_algebra import (HadamardEJA,
1465 ....: JordanSpinEJA,
1466 ....: RealSymmetricEJA,
1467 ....: ComplexHermitianEJA,
1468 ....: QuaternionHermitianEJA,
1473 The rank of the Jordan spin algebra is always two::
1475 sage: JordanSpinEJA(2).rank()
1477 sage: JordanSpinEJA(3).rank()
1479 sage: JordanSpinEJA(4).rank()
1482 The rank of the `n`-by-`n` Hermitian real, complex, or
1483 quaternion matrices is `n`::
1485 sage: RealSymmetricEJA(4).rank()
1487 sage: ComplexHermitianEJA(3).rank()
1489 sage: QuaternionHermitianEJA(2).rank()
1494 Ensure that every EJA that we know how to construct has a
1495 positive integer rank, unless the algebra is trivial in
1496 which case its rank will be zero::
1498 sage: set_random_seed()
1499 sage: J = random_eja()
1503 sage: r > 0 or (r == 0 and J.is_trivial())
1506 Ensure that computing the rank actually works, since the ranks
1507 of all simple algebras are known and will be cached by default::
1509 sage: set_random_seed() # long time
1510 sage: J = random_eja() # long time
1511 sage: cached = J.rank() # long time
1512 sage: J.rank.clear_cache() # long time
1513 sage: J.rank() == cached # long time
1517 return len(self
._charpoly
_coefficients
())
1520 def subalgebra(self
, basis
, **kwargs
):
1522 Create a subalgebra of this algebra from the given basis.
1524 from mjo
.eja
.eja_subalgebra
import FiniteDimensionalEJASubalgebra
1525 return FiniteDimensionalEJASubalgebra(self
, basis
, **kwargs
)
1528 def vector_space(self
):
1530 Return the vector space that underlies this algebra.
1534 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1538 sage: J = RealSymmetricEJA(2)
1539 sage: J.vector_space()
1540 Vector space of dimension 3 over...
1543 return self
.zero().to_vector().parent().ambient_vector_space()
1547 class RationalBasisEJA(FiniteDimensionalEJA
):
1549 Algebras whose supplied basis elements have all rational entries.
1553 sage: from mjo.eja.eja_algebra import BilinearFormEJA
1557 The supplied basis is orthonormalized by default::
1559 sage: B = matrix(QQ, [[1, 0, 0], [0, 25, -32], [0, -32, 41]])
1560 sage: J = BilinearFormEJA(B)
1561 sage: J.matrix_basis()
1578 # Abuse the check_field parameter to check that the entries of
1579 # out basis (in ambient coordinates) are in the field QQ.
1580 # Use _all2list to get the vector coordinates of octonion
1581 # entries and not the octonions themselves (which are not
1583 if not all( all(b_i
in QQ
for b_i
in _all2list(b
))
1585 raise TypeError("basis not rational")
1587 super().__init
__(basis
,
1591 check_field
=check_field
,
1594 self
._rational
_algebra
= None
1596 # There's no point in constructing the extra algebra if this
1597 # one is already rational.
1599 # Note: the same Jordan and inner-products work here,
1600 # because they are necessarily defined with respect to
1601 # ambient coordinates and not any particular basis.
1602 self
._rational
_algebra
= FiniteDimensionalEJA(
1607 associative
=self
.is_associative(),
1608 orthonormalize
=False,
1613 def _charpoly_coefficients(self
):
1617 sage: from mjo.eja.eja_algebra import (BilinearFormEJA,
1618 ....: JordanSpinEJA)
1622 The base ring of the resulting polynomial coefficients is what
1623 it should be, and not the rationals (unless the algebra was
1624 already over the rationals)::
1626 sage: J = JordanSpinEJA(3)
1627 sage: J._charpoly_coefficients()
1628 (X1^2 - X2^2 - X3^2, -2*X1)
1629 sage: a0 = J._charpoly_coefficients()[0]
1631 Algebraic Real Field
1632 sage: a0.base_ring()
1633 Algebraic Real Field
1636 if self
._rational
_algebra
is None:
1637 # There's no need to construct *another* algebra over the
1638 # rationals if this one is already over the
1639 # rationals. Likewise, if we never orthonormalized our
1640 # basis, we might as well just use the given one.
1641 return super()._charpoly
_coefficients
()
1643 # Do the computation over the rationals. The answer will be
1644 # the same, because all we've done is a change of basis.
1645 # Then, change back from QQ to our real base ring
1646 a
= ( a_i
.change_ring(self
.base_ring())
1647 for a_i
in self
._rational
_algebra
._charpoly
_coefficients
() )
1649 if self
._deortho
_matrix
is None:
1650 # This can happen if our base ring was, say, AA and we
1651 # chose not to (or didn't need to) orthonormalize. It's
1652 # still faster to do the computations over QQ even if
1653 # the numbers in the boxes stay the same.
1656 # Otherwise, convert the coordinate variables back to the
1657 # deorthonormalized ones.
1658 R
= self
.coordinate_polynomial_ring()
1659 from sage
.modules
.free_module_element
import vector
1660 X
= vector(R
, R
.gens())
1661 BX
= self
._deortho
_matrix
*X
1663 subs_dict
= { X[i]: BX[i] for i in range(len(X)) }
1664 return tuple( a_i
.subs(subs_dict
) for a_i
in a
)
1666 class ConcreteEJA(FiniteDimensionalEJA
):
1668 A class for the Euclidean Jordan algebras that we know by name.
1670 These are the Jordan algebras whose basis, multiplication table,
1671 rank, and so on are known a priori. More to the point, they are
1672 the Euclidean Jordan algebras for which we are able to conjure up
1673 a "random instance."
1677 sage: from mjo.eja.eja_algebra import ConcreteEJA
1681 Our basis is normalized with respect to the algebra's inner
1682 product, unless we specify otherwise::
1684 sage: set_random_seed()
1685 sage: J = ConcreteEJA.random_instance()
1686 sage: all( b.norm() == 1 for b in J.gens() )
1689 Since our basis is orthonormal with respect to the algebra's inner
1690 product, and since we know that this algebra is an EJA, any
1691 left-multiplication operator's matrix will be symmetric because
1692 natural->EJA basis representation is an isometry and within the
1693 EJA the operator is self-adjoint by the Jordan axiom::
1695 sage: set_random_seed()
1696 sage: J = ConcreteEJA.random_instance()
1697 sage: x = J.random_element()
1698 sage: x.operator().is_self_adjoint()
1703 def _max_random_instance_size():
1705 Return an integer "size" that is an upper bound on the size of
1706 this algebra when it is used in a random test
1707 case. Unfortunately, the term "size" is ambiguous -- when
1708 dealing with `R^n` under either the Hadamard or Jordan spin
1709 product, the "size" refers to the dimension `n`. When dealing
1710 with a matrix algebra (real symmetric or complex/quaternion
1711 Hermitian), it refers to the size of the matrix, which is far
1712 less than the dimension of the underlying vector space.
1714 This method must be implemented in each subclass.
1716 raise NotImplementedError
1719 def random_instance(cls
, *args
, **kwargs
):
1721 Return a random instance of this type of algebra.
1723 This method should be implemented in each subclass.
1725 from sage
.misc
.prandom
import choice
1726 eja_class
= choice(cls
.__subclasses
__())
1728 # These all bubble up to the RationalBasisEJA superclass
1729 # constructor, so any (kw)args valid there are also valid
1731 return eja_class
.random_instance(*args
, **kwargs
)
1736 def jordan_product(X
,Y
):
1737 return (X
*Y
+ Y
*X
)/2
1740 def trace_inner_product(X
,Y
):
1742 A trace inner-product for matrices that aren't embedded in the
1743 reals. It takes MATRICES as arguments, not EJA elements.
1745 return (X
*Y
).trace().real()
1747 class RealEmbeddedMatrixEJA(MatrixEJA
):
1749 def dimension_over_reals():
1751 The dimension of this matrix's base ring over the reals.
1753 The reals are dimension one over themselves, obviously; that's
1754 just `\mathbb{R}^{1}`. Likewise, the complex numbers `a + bi`
1755 have dimension two. Finally, the quaternions have dimension
1756 four over the reals.
1758 This is used to determine the size of the matrix returned from
1759 :meth:`real_embed`, among other things.
1761 raise NotImplementedError
1764 def real_embed(cls
,M
):
1766 Embed the matrix ``M`` into a space of real matrices.
1768 The matrix ``M`` can have entries in any field at the moment:
1769 the real numbers, complex numbers, or quaternions. And although
1770 they are not a field, we can probably support octonions at some
1771 point, too. This function returns a real matrix that "acts like"
1772 the original with respect to matrix multiplication; i.e.
1774 real_embed(M*N) = real_embed(M)*real_embed(N)
1777 if M
.ncols() != M
.nrows():
1778 raise ValueError("the matrix 'M' must be square")
1783 def real_unembed(cls
,M
):
1785 The inverse of :meth:`real_embed`.
1787 if M
.ncols() != M
.nrows():
1788 raise ValueError("the matrix 'M' must be square")
1789 if not ZZ(M
.nrows()).mod(cls
.dimension_over_reals()).is_zero():
1790 raise ValueError("the matrix 'M' must be a real embedding")
1795 def trace_inner_product(cls
,X
,Y
):
1797 Compute the trace inner-product of two real-embeddings.
1801 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1805 sage: set_random_seed()
1806 sage: J = ComplexHermitianEJA.random_instance()
1807 sage: x,y = J.random_elements(2)
1808 sage: Xe = x.to_matrix()
1809 sage: Ye = y.to_matrix()
1810 sage: X = J.real_unembed(Xe)
1811 sage: Y = J.real_unembed(Ye)
1812 sage: expected = (X*Y).trace().real()
1813 sage: actual = J.trace_inner_product(Xe,Ye)
1814 sage: actual == expected
1818 # This does in fact compute the real part of the trace.
1819 # If we compute the trace of e.g. a complex matrix M,
1820 # then we do so by adding up its diagonal entries --
1821 # call them z_1 through z_n. The real embedding of z_1
1822 # will be a 2-by-2 REAL matrix [a, b; -b, a] whose trace
1823 # as a REAL matrix will be 2*a = 2*Re(z_1). And so forth.
1824 return (X
*Y
).trace()/cls
.dimension_over_reals()
1826 class RealSymmetricEJA(RationalBasisEJA
, ConcreteEJA
, MatrixEJA
):
1828 The rank-n simple EJA consisting of real symmetric n-by-n
1829 matrices, the usual symmetric Jordan product, and the trace inner
1830 product. It has dimension `(n^2 + n)/2` over the reals.
1834 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1838 sage: J = RealSymmetricEJA(2)
1839 sage: b0, b1, b2 = J.gens()
1847 In theory, our "field" can be any subfield of the reals::
1849 sage: RealSymmetricEJA(2, field=RDF, check_axioms=True)
1850 Euclidean Jordan algebra of dimension 3 over Real Double Field
1851 sage: RealSymmetricEJA(2, field=RR, check_axioms=True)
1852 Euclidean Jordan algebra of dimension 3 over Real Field with
1853 53 bits of precision
1857 The dimension of this algebra is `(n^2 + n) / 2`::
1859 sage: set_random_seed()
1860 sage: n_max = RealSymmetricEJA._max_random_instance_size()
1861 sage: n = ZZ.random_element(1, n_max)
1862 sage: J = RealSymmetricEJA(n)
1863 sage: J.dimension() == (n^2 + n)/2
1866 The Jordan multiplication is what we think it is::
1868 sage: set_random_seed()
1869 sage: J = RealSymmetricEJA.random_instance()
1870 sage: x,y = J.random_elements(2)
1871 sage: actual = (x*y).to_matrix()
1872 sage: X = x.to_matrix()
1873 sage: Y = y.to_matrix()
1874 sage: expected = (X*Y + Y*X)/2
1875 sage: actual == expected
1877 sage: J(expected) == x*y
1880 We can change the generator prefix::
1882 sage: RealSymmetricEJA(3, prefix='q').gens()
1883 (q0, q1, q2, q3, q4, q5)
1885 We can construct the (trivial) algebra of rank zero::
1887 sage: RealSymmetricEJA(0)
1888 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
1892 def _denormalized_basis(cls
, n
, field
):
1894 Return a basis for the space of real symmetric n-by-n matrices.
1898 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1902 sage: set_random_seed()
1903 sage: n = ZZ.random_element(1,5)
1904 sage: B = RealSymmetricEJA._denormalized_basis(n,ZZ)
1905 sage: all( M.is_symmetric() for M in B)
1909 # The basis of symmetric matrices, as matrices, in their R^(n-by-n)
1913 for j
in range(i
+1):
1914 Eij
= matrix(field
, n
, lambda k
,l
: k
==i
and l
==j
)
1918 Sij
= Eij
+ Eij
.transpose()
1924 def _max_random_instance_size():
1925 return 4 # Dimension 10
1928 def random_instance(cls
, **kwargs
):
1930 Return a random instance of this type of algebra.
1932 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
() + 1)
1933 return cls(n
, **kwargs
)
1935 def __init__(self
, n
, field
=AA
, **kwargs
):
1936 # We know this is a valid EJA, but will double-check
1937 # if the user passes check_axioms=True.
1938 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
1944 super().__init
__(self
._denormalized
_basis
(n
,field
),
1945 self
.jordan_product
,
1946 self
.trace_inner_product
,
1948 associative
=associative
,
1951 # TODO: this could be factored out somehow, but is left here
1952 # because the MatrixEJA is not presently a subclass of the
1953 # FDEJA class that defines rank() and one().
1954 self
.rank
.set_cache(n
)
1955 idV
= self
.matrix_space().one()
1956 self
.one
.set_cache(self(idV
))
1960 class ComplexMatrixEJA(RealEmbeddedMatrixEJA
):
1961 # A manual dictionary-cache for the complex_extension() method,
1962 # since apparently @classmethods can't also be @cached_methods.
1963 _complex_extension
= {}
1966 def complex_extension(cls
,field
):
1968 The complex field that we embed/unembed, as an extension
1969 of the given ``field``.
1971 if field
in cls
._complex
_extension
:
1972 return cls
._complex
_extension
[field
]
1974 # Sage doesn't know how to adjoin the complex "i" (the root of
1975 # x^2 + 1) to a field in a general way. Here, we just enumerate
1976 # all of the cases that I have cared to support so far.
1978 # Sage doesn't know how to embed AA into QQbar, i.e. how
1979 # to adjoin sqrt(-1) to AA.
1981 elif not field
.is_exact():
1983 F
= field
.complex_field()
1985 # Works for QQ and... maybe some other fields.
1986 R
= PolynomialRing(field
, 'z')
1988 F
= field
.extension(z
**2 + 1, 'I', embedding
=CLF(-1).sqrt())
1990 cls
._complex
_extension
[field
] = F
1994 def dimension_over_reals():
1998 def real_embed(cls
,M
):
2000 Embed the n-by-n complex matrix ``M`` into the space of real
2001 matrices of size 2n-by-2n via the map the sends each entry `z = a +
2002 bi` to the block matrix ``[[a,b],[-b,a]]``.
2006 sage: from mjo.eja.eja_algebra import ComplexMatrixEJA
2010 sage: F = QuadraticField(-1, 'I')
2011 sage: x1 = F(4 - 2*i)
2012 sage: x2 = F(1 + 2*i)
2015 sage: M = matrix(F,2,[[x1,x2],[x3,x4]])
2016 sage: ComplexMatrixEJA.real_embed(M)
2025 Embedding is a homomorphism (isomorphism, in fact)::
2027 sage: set_random_seed()
2028 sage: n = ZZ.random_element(3)
2029 sage: F = QuadraticField(-1, 'I')
2030 sage: X = random_matrix(F, n)
2031 sage: Y = random_matrix(F, n)
2032 sage: Xe = ComplexMatrixEJA.real_embed(X)
2033 sage: Ye = ComplexMatrixEJA.real_embed(Y)
2034 sage: XYe = ComplexMatrixEJA.real_embed(X*Y)
2039 super().real_embed(M
)
2042 # We don't need any adjoined elements...
2043 field
= M
.base_ring().base_ring()
2049 blocks
.append(matrix(field
, 2, [ [ a
, b
],
2052 return matrix
.block(field
, n
, blocks
)
2056 def real_unembed(cls
,M
):
2058 The inverse of _embed_complex_matrix().
2062 sage: from mjo.eja.eja_algebra import ComplexMatrixEJA
2066 sage: A = matrix(QQ,[ [ 1, 2, 3, 4],
2067 ....: [-2, 1, -4, 3],
2068 ....: [ 9, 10, 11, 12],
2069 ....: [-10, 9, -12, 11] ])
2070 sage: ComplexMatrixEJA.real_unembed(A)
2072 [ 10*I + 9 12*I + 11]
2076 Unembedding is the inverse of embedding::
2078 sage: set_random_seed()
2079 sage: F = QuadraticField(-1, 'I')
2080 sage: M = random_matrix(F, 3)
2081 sage: Me = ComplexMatrixEJA.real_embed(M)
2082 sage: ComplexMatrixEJA.real_unembed(Me) == M
2086 super().real_unembed(M
)
2088 d
= cls
.dimension_over_reals()
2089 F
= cls
.complex_extension(M
.base_ring())
2092 # Go top-left to bottom-right (reading order), converting every
2093 # 2-by-2 block we see to a single complex element.
2095 for k
in range(n
/d
):
2096 for j
in range(n
/d
):
2097 submat
= M
[d
*k
:d
*k
+d
,d
*j
:d
*j
+d
]
2098 if submat
[0,0] != submat
[1,1]:
2099 raise ValueError('bad on-diagonal submatrix')
2100 if submat
[0,1] != -submat
[1,0]:
2101 raise ValueError('bad off-diagonal submatrix')
2102 z
= submat
[0,0] + submat
[0,1]*i
2105 return matrix(F
, n
/d
, elements
)
2108 class ComplexHermitianEJA(RationalBasisEJA
, ConcreteEJA
, ComplexMatrixEJA
):
2110 The rank-n simple EJA consisting of complex Hermitian n-by-n
2111 matrices over the real numbers, the usual symmetric Jordan product,
2112 and the real-part-of-trace inner product. It has dimension `n^2` over
2117 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
2121 In theory, our "field" can be any subfield of the reals::
2123 sage: ComplexHermitianEJA(2, field=RDF, check_axioms=True)
2124 Euclidean Jordan algebra of dimension 4 over Real Double Field
2125 sage: ComplexHermitianEJA(2, field=RR, check_axioms=True)
2126 Euclidean Jordan algebra of dimension 4 over Real Field with
2127 53 bits of precision
2131 The dimension of this algebra is `n^2`::
2133 sage: set_random_seed()
2134 sage: n_max = ComplexHermitianEJA._max_random_instance_size()
2135 sage: n = ZZ.random_element(1, n_max)
2136 sage: J = ComplexHermitianEJA(n)
2137 sage: J.dimension() == n^2
2140 The Jordan multiplication is what we think it is::
2142 sage: set_random_seed()
2143 sage: J = ComplexHermitianEJA.random_instance()
2144 sage: x,y = J.random_elements(2)
2145 sage: actual = (x*y).to_matrix()
2146 sage: X = x.to_matrix()
2147 sage: Y = y.to_matrix()
2148 sage: expected = (X*Y + Y*X)/2
2149 sage: actual == expected
2151 sage: J(expected) == x*y
2154 We can change the generator prefix::
2156 sage: ComplexHermitianEJA(2, prefix='z').gens()
2159 We can construct the (trivial) algebra of rank zero::
2161 sage: ComplexHermitianEJA(0)
2162 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2167 def _denormalized_basis(cls
, n
, field
):
2169 Returns a basis for the space of complex Hermitian n-by-n matrices.
2171 Why do we embed these? Basically, because all of numerical linear
2172 algebra assumes that you're working with vectors consisting of `n`
2173 entries from a field and scalars from the same field. There's no way
2174 to tell SageMath that (for example) the vectors contain complex
2175 numbers, while the scalar field is real.
2179 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
2183 sage: set_random_seed()
2184 sage: n = ZZ.random_element(1,5)
2185 sage: B = ComplexHermitianEJA._denormalized_basis(n,ZZ)
2186 sage: all( M.is_symmetric() for M in B)
2190 R
= PolynomialRing(ZZ
, 'z')
2192 F
= ZZ
.extension(z
**2 + 1, 'I')
2195 # This is like the symmetric case, but we need to be careful:
2197 # * We want conjugate-symmetry, not just symmetry.
2198 # * The diagonal will (as a result) be real.
2201 Eij
= matrix
.zero(F
,n
)
2203 for j
in range(i
+1):
2207 Sij
= cls
.real_embed(Eij
)
2210 # The second one has a minus because it's conjugated.
2211 Eij
[j
,i
] = 1 # Eij = Eij + Eij.transpose()
2212 Sij_real
= cls
.real_embed(Eij
)
2214 # Eij = I*Eij - I*Eij.transpose()
2217 Sij_imag
= cls
.real_embed(Eij
)
2223 # Since we embedded the entries, we can drop back to the
2224 # desired real "field" instead of the extension "F".
2225 return tuple( s
.change_ring(field
) for s
in S
)
2228 def __init__(self
, n
, field
=AA
, **kwargs
):
2229 # We know this is a valid EJA, but will double-check
2230 # if the user passes check_axioms=True.
2231 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2237 super().__init
__(self
._denormalized
_basis
(n
,field
),
2238 self
.jordan_product
,
2239 self
.trace_inner_product
,
2241 associative
=associative
,
2243 # TODO: this could be factored out somehow, but is left here
2244 # because the MatrixEJA is not presently a subclass of the
2245 # FDEJA class that defines rank() and one().
2246 self
.rank
.set_cache(n
)
2247 idV
= matrix
.identity(ZZ
, self
.dimension_over_reals()*n
)
2248 self
.one
.set_cache(self(idV
))
2251 def _max_random_instance_size():
2252 return 3 # Dimension 9
2255 def random_instance(cls
, **kwargs
):
2257 Return a random instance of this type of algebra.
2259 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
() + 1)
2260 return cls(n
, **kwargs
)
2263 class QuaternionHermitianEJA(RationalBasisEJA
, ConcreteEJA
, MatrixEJA
):
2265 The rank-n simple EJA consisting of self-adjoint n-by-n quaternion
2266 matrices, the usual symmetric Jordan product, and the
2267 real-part-of-trace inner product. It has dimension `2n^2 - n` over
2272 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
2276 In theory, our "field" can be any subfield of the reals::
2278 sage: QuaternionHermitianEJA(2, field=RDF, check_axioms=True)
2279 Euclidean Jordan algebra of dimension 6 over Real Double Field
2280 sage: QuaternionHermitianEJA(2, field=RR, check_axioms=True)
2281 Euclidean Jordan algebra of dimension 6 over Real Field with
2282 53 bits of precision
2286 The dimension of this algebra is `2*n^2 - n`::
2288 sage: set_random_seed()
2289 sage: n_max = QuaternionHermitianEJA._max_random_instance_size()
2290 sage: n = ZZ.random_element(1, n_max)
2291 sage: J = QuaternionHermitianEJA(n)
2292 sage: J.dimension() == 2*(n^2) - n
2295 The Jordan multiplication is what we think it is::
2297 sage: set_random_seed()
2298 sage: J = QuaternionHermitianEJA.random_instance()
2299 sage: x,y = J.random_elements(2)
2300 sage: actual = (x*y).to_matrix()
2301 sage: X = x.to_matrix()
2302 sage: Y = y.to_matrix()
2303 sage: expected = (X*Y + Y*X)/2
2304 sage: actual == expected
2306 sage: J(expected) == x*y
2309 We can change the generator prefix::
2311 sage: QuaternionHermitianEJA(2, prefix='a').gens()
2312 (a0, a1, a2, a3, a4, a5)
2314 We can construct the (trivial) algebra of rank zero::
2316 sage: QuaternionHermitianEJA(0)
2317 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2321 def _denormalized_basis(cls
, n
, field
):
2323 Returns a basis for the space of quaternion Hermitian n-by-n matrices.
2325 Why do we embed these? Basically, because all of numerical
2326 linear algebra assumes that you're working with vectors consisting
2327 of `n` entries from a field and scalars from the same field. There's
2328 no way to tell SageMath that (for example) the vectors contain
2329 complex numbers, while the scalar field is real.
2333 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
2337 sage: set_random_seed()
2338 sage: n = ZZ.random_element(1,5)
2339 sage: B = QuaternionHermitianEJA._denormalized_basis(n,QQ)
2340 sage: all( M.is_hermitian() for M in B )
2344 from mjo
.hurwitz
import QuaternionMatrixAlgebra
2345 A
= QuaternionMatrixAlgebra(n
, scalars
=field
)
2346 es
= A
.entry_algebra_gens()
2350 for j
in range(i
+1):
2352 E_ii
= A
.monomial( (i
,j
,es
[0]) )
2356 E_ij
= A
.monomial( (i
,j
,e
) )
2358 # If the conjugate has a negative sign in front
2359 # of it, (j,i,ec) won't be a monomial!
2360 if (j
,i
,ec
) in A
.indices():
2361 E_ij
+= A
.monomial( (j
,i
,ec
) )
2363 E_ij
-= A
.monomial( (j
,i
,-ec
) )
2366 return tuple( basis
)
2370 def trace_inner_product(X
,Y
):
2372 Overload the superclass method because the quaternions are weird
2373 and we need to use ``coefficient_tuple()`` to get the realpart.
2377 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
2381 sage: J = QuaternionHermitianEJA(2,field=QQ,orthonormalize=False)
2382 sage: I = J.one().to_matrix()
2383 sage: J.trace_inner_product(I, -I)
2387 return (X
*Y
).trace().coefficient_tuple()[0]
2389 def __init__(self
, n
, field
=AA
, **kwargs
):
2390 # We know this is a valid EJA, but will double-check
2391 # if the user passes check_axioms=True.
2392 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2398 super().__init
__(self
._denormalized
_basis
(n
,field
),
2399 self
.jordan_product
,
2400 self
.trace_inner_product
,
2402 associative
=associative
,
2405 # TODO: this could be factored out somehow, but is left here
2406 # because the MatrixEJA is not presently a subclass of the
2407 # FDEJA class that defines rank() and one().
2408 self
.rank
.set_cache(n
)
2409 idV
= self
.matrix_space().one()
2410 self
.one
.set_cache(self(idV
))
2414 def _max_random_instance_size():
2416 The maximum rank of a random QuaternionHermitianEJA.
2418 return 2 # Dimension 6
2421 def random_instance(cls
, **kwargs
):
2423 Return a random instance of this type of algebra.
2425 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
() + 1)
2426 return cls(n
, **kwargs
)
2428 class OctonionHermitianEJA(RationalBasisEJA
, ConcreteEJA
, MatrixEJA
):
2432 sage: from mjo.eja.eja_algebra import (FiniteDimensionalEJA,
2433 ....: OctonionHermitianEJA)
2437 The 3-by-3 algebra satisfies the axioms of an EJA::
2439 sage: OctonionHermitianEJA(3, # long time
2440 ....: field=QQ, # long time
2441 ....: orthonormalize=False, # long time
2442 ....: check_axioms=True) # long time
2443 Euclidean Jordan algebra of dimension 27 over Rational Field
2445 After a change-of-basis, the 2-by-2 algebra has the same
2446 multiplication table as the ten-dimensional Jordan spin algebra::
2448 sage: b = OctonionHermitianEJA._denormalized_basis(2,QQ)
2449 sage: basis = (b[0] + b[9],) + b[1:9] + (b[0] - b[9],)
2450 sage: jp = OctonionHermitianEJA.jordan_product
2451 sage: ip = OctonionHermitianEJA.trace_inner_product
2452 sage: J = FiniteDimensionalEJA(basis,
2456 ....: orthonormalize=False)
2457 sage: J.multiplication_table()
2458 +----++----+----+----+----+----+----+----+----+----+----+
2459 | * || b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7 | b8 | b9 |
2460 +====++====+====+====+====+====+====+====+====+====+====+
2461 | b0 || b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7 | b8 | b9 |
2462 +----++----+----+----+----+----+----+----+----+----+----+
2463 | b1 || b1 | b0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2464 +----++----+----+----+----+----+----+----+----+----+----+
2465 | b2 || b2 | 0 | b0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2466 +----++----+----+----+----+----+----+----+----+----+----+
2467 | b3 || b3 | 0 | 0 | b0 | 0 | 0 | 0 | 0 | 0 | 0 |
2468 +----++----+----+----+----+----+----+----+----+----+----+
2469 | b4 || b4 | 0 | 0 | 0 | b0 | 0 | 0 | 0 | 0 | 0 |
2470 +----++----+----+----+----+----+----+----+----+----+----+
2471 | b5 || b5 | 0 | 0 | 0 | 0 | b0 | 0 | 0 | 0 | 0 |
2472 +----++----+----+----+----+----+----+----+----+----+----+
2473 | b6 || b6 | 0 | 0 | 0 | 0 | 0 | b0 | 0 | 0 | 0 |
2474 +----++----+----+----+----+----+----+----+----+----+----+
2475 | b7 || b7 | 0 | 0 | 0 | 0 | 0 | 0 | b0 | 0 | 0 |
2476 +----++----+----+----+----+----+----+----+----+----+----+
2477 | b8 || b8 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | b0 | 0 |
2478 +----++----+----+----+----+----+----+----+----+----+----+
2479 | b9 || b9 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | b0 |
2480 +----++----+----+----+----+----+----+----+----+----+----+
2484 We can actually construct the 27-dimensional Albert algebra,
2485 and we get the right unit element if we recompute it::
2487 sage: J = OctonionHermitianEJA(3, # long time
2488 ....: field=QQ, # long time
2489 ....: orthonormalize=False) # long time
2490 sage: J.one.clear_cache() # long time
2491 sage: J.one() # long time
2493 sage: J.one().to_matrix() # long time
2502 The 2-by-2 algebra is isomorphic to the ten-dimensional Jordan
2503 spin algebra, but just to be sure, we recompute its rank::
2505 sage: J = OctonionHermitianEJA(2, # long time
2506 ....: field=QQ, # long time
2507 ....: orthonormalize=False) # long time
2508 sage: J.rank.clear_cache() # long time
2509 sage: J.rank() # long time
2514 def _max_random_instance_size():
2516 The maximum rank of a random QuaternionHermitianEJA.
2518 return 1 # Dimension 1
2521 def random_instance(cls
, **kwargs
):
2523 Return a random instance of this type of algebra.
2525 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
() + 1)
2526 return cls(n
, **kwargs
)
2528 def __init__(self
, n
, field
=AA
, **kwargs
):
2530 # Otherwise we don't get an EJA.
2531 raise ValueError("n cannot exceed 3")
2533 # We know this is a valid EJA, but will double-check
2534 # if the user passes check_axioms=True.
2535 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2537 super().__init
__(self
._denormalized
_basis
(n
,field
),
2538 self
.jordan_product
,
2539 self
.trace_inner_product
,
2543 # TODO: this could be factored out somehow, but is left here
2544 # because the MatrixEJA is not presently a subclass of the
2545 # FDEJA class that defines rank() and one().
2546 self
.rank
.set_cache(n
)
2547 idV
= self
.matrix_space().one()
2548 self
.one
.set_cache(self(idV
))
2552 def _denormalized_basis(cls
, n
, field
):
2554 Returns a basis for the space of octonion Hermitian n-by-n
2559 sage: from mjo.eja.eja_algebra import OctonionHermitianEJA
2563 sage: B = OctonionHermitianEJA._denormalized_basis(3,QQ)
2564 sage: all( M.is_hermitian() for M in B )
2570 from mjo
.hurwitz
import OctonionMatrixAlgebra
2571 A
= OctonionMatrixAlgebra(n
, scalars
=field
)
2572 es
= A
.entry_algebra_gens()
2576 for j
in range(i
+1):
2578 E_ii
= A
.monomial( (i
,j
,es
[0]) )
2582 E_ij
= A
.monomial( (i
,j
,e
) )
2584 # If the conjugate has a negative sign in front
2585 # of it, (j,i,ec) won't be a monomial!
2586 if (j
,i
,ec
) in A
.indices():
2587 E_ij
+= A
.monomial( (j
,i
,ec
) )
2589 E_ij
-= A
.monomial( (j
,i
,-ec
) )
2592 return tuple( basis
)
2595 def trace_inner_product(X
,Y
):
2597 The octonions don't know that the reals are embedded in them,
2598 so we have to take the e0 component ourselves.
2602 sage: from mjo.eja.eja_algebra import OctonionHermitianEJA
2606 sage: J = OctonionHermitianEJA(2,field=QQ,orthonormalize=False)
2607 sage: I = J.one().to_matrix()
2608 sage: J.trace_inner_product(I, -I)
2612 return (X
*Y
).trace().coefficient(0)
2615 class AlbertEJA(OctonionHermitianEJA
):
2617 The Albert algebra is the algebra of three-by-three Hermitian
2618 matrices whose entries are octonions.
2622 sage: from mjo.eja.eja_algebra import AlbertEJA
2626 sage: AlbertEJA(field=QQ, orthonormalize=False)
2627 Euclidean Jordan algebra of dimension 27 over Rational Field
2628 sage: AlbertEJA() # long time
2629 Euclidean Jordan algebra of dimension 27 over Algebraic Real Field
2632 def __init__(self
, *args
, **kwargs
):
2633 super().__init
__(3, *args
, **kwargs
)
2636 class HadamardEJA(RationalBasisEJA
, ConcreteEJA
):
2638 Return the Euclidean Jordan algebra on `R^n` with the Hadamard
2639 (pointwise real-number multiplication) Jordan product and the
2640 usual inner-product.
2642 This is nothing more than the Cartesian product of ``n`` copies of
2643 the one-dimensional Jordan spin algebra, and is the most common
2644 example of a non-simple Euclidean Jordan algebra.
2648 sage: from mjo.eja.eja_algebra import HadamardEJA
2652 This multiplication table can be verified by hand::
2654 sage: J = HadamardEJA(3)
2655 sage: b0,b1,b2 = J.gens()
2671 We can change the generator prefix::
2673 sage: HadamardEJA(3, prefix='r').gens()
2676 def __init__(self
, n
, field
=AA
, **kwargs
):
2678 jordan_product
= lambda x
,y
: x
2679 inner_product
= lambda x
,y
: x
2681 def jordan_product(x
,y
):
2683 return P( xi
*yi
for (xi
,yi
) in zip(x
,y
) )
2685 def inner_product(x
,y
):
2688 # New defaults for keyword arguments. Don't orthonormalize
2689 # because our basis is already orthonormal with respect to our
2690 # inner-product. Don't check the axioms, because we know this
2691 # is a valid EJA... but do double-check if the user passes
2692 # check_axioms=True. Note: we DON'T override the "check_field"
2693 # default here, because the user can pass in a field!
2694 if "orthonormalize" not in kwargs
: kwargs
["orthonormalize"] = False
2695 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2697 column_basis
= tuple( b
.column()
2698 for b
in FreeModule(field
, n
).basis() )
2699 super().__init
__(column_basis
,
2705 self
.rank
.set_cache(n
)
2708 self
.one
.set_cache( self
.zero() )
2710 self
.one
.set_cache( sum(self
.gens()) )
2713 def _max_random_instance_size():
2715 The maximum dimension of a random HadamardEJA.
2720 def random_instance(cls
, **kwargs
):
2722 Return a random instance of this type of algebra.
2724 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
() + 1)
2725 return cls(n
, **kwargs
)
2728 class BilinearFormEJA(RationalBasisEJA
, ConcreteEJA
):
2730 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
2731 with the half-trace inner product and jordan product ``x*y =
2732 (<Bx,y>,y_bar>, x0*y_bar + y0*x_bar)`` where `B = 1 \times B22` is
2733 a symmetric positive-definite "bilinear form" matrix. Its
2734 dimension is the size of `B`, and it has rank two in dimensions
2735 larger than two. It reduces to the ``JordanSpinEJA`` when `B` is
2736 the identity matrix of order ``n``.
2738 We insist that the one-by-one upper-left identity block of `B` be
2739 passed in as well so that we can be passed a matrix of size zero
2740 to construct a trivial algebra.
2744 sage: from mjo.eja.eja_algebra import (BilinearFormEJA,
2745 ....: JordanSpinEJA)
2749 When no bilinear form is specified, the identity matrix is used,
2750 and the resulting algebra is the Jordan spin algebra::
2752 sage: B = matrix.identity(AA,3)
2753 sage: J0 = BilinearFormEJA(B)
2754 sage: J1 = JordanSpinEJA(3)
2755 sage: J0.multiplication_table() == J0.multiplication_table()
2758 An error is raised if the matrix `B` does not correspond to a
2759 positive-definite bilinear form::
2761 sage: B = matrix.random(QQ,2,3)
2762 sage: J = BilinearFormEJA(B)
2763 Traceback (most recent call last):
2765 ValueError: bilinear form is not positive-definite
2766 sage: B = matrix.zero(QQ,3)
2767 sage: J = BilinearFormEJA(B)
2768 Traceback (most recent call last):
2770 ValueError: bilinear form is not positive-definite
2774 We can create a zero-dimensional algebra::
2776 sage: B = matrix.identity(AA,0)
2777 sage: J = BilinearFormEJA(B)
2781 We can check the multiplication condition given in the Jordan, von
2782 Neumann, and Wigner paper (and also discussed on my "On the
2783 symmetry..." paper). Note that this relies heavily on the standard
2784 choice of basis, as does anything utilizing the bilinear form
2785 matrix. We opt not to orthonormalize the basis, because if we
2786 did, we would have to normalize the `s_{i}` in a similar manner::
2788 sage: set_random_seed()
2789 sage: n = ZZ.random_element(5)
2790 sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular')
2791 sage: B11 = matrix.identity(QQ,1)
2792 sage: B22 = M.transpose()*M
2793 sage: B = block_matrix(2,2,[ [B11,0 ],
2795 sage: J = BilinearFormEJA(B, orthonormalize=False)
2796 sage: eis = VectorSpace(M.base_ring(), M.ncols()).basis()
2797 sage: V = J.vector_space()
2798 sage: sis = [ J( V([0] + (M.inverse()*ei).list()).column() )
2799 ....: for ei in eis ]
2800 sage: actual = [ sis[i]*sis[j]
2801 ....: for i in range(n-1)
2802 ....: for j in range(n-1) ]
2803 sage: expected = [ J.one() if i == j else J.zero()
2804 ....: for i in range(n-1)
2805 ....: for j in range(n-1) ]
2806 sage: actual == expected
2810 def __init__(self
, B
, field
=AA
, **kwargs
):
2811 # The matrix "B" is supplied by the user in most cases,
2812 # so it makes sense to check whether or not its positive-
2813 # definite unless we are specifically asked not to...
2814 if ("check_axioms" not in kwargs
) or kwargs
["check_axioms"]:
2815 if not B
.is_positive_definite():
2816 raise ValueError("bilinear form is not positive-definite")
2818 # However, all of the other data for this EJA is computed
2819 # by us in manner that guarantees the axioms are
2820 # satisfied. So, again, unless we are specifically asked to
2821 # verify things, we'll skip the rest of the checks.
2822 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2824 def inner_product(x
,y
):
2825 return (y
.T
*B
*x
)[0,0]
2827 def jordan_product(x
,y
):
2833 z0
= inner_product(y
,x
)
2834 zbar
= y0
*xbar
+ x0
*ybar
2835 return P([z0
] + zbar
.list())
2838 column_basis
= tuple( b
.column()
2839 for b
in FreeModule(field
, n
).basis() )
2841 # TODO: I haven't actually checked this, but it seems legit.
2846 super().__init
__(column_basis
,
2850 associative
=associative
,
2853 # The rank of this algebra is two, unless we're in a
2854 # one-dimensional ambient space (because the rank is bounded
2855 # by the ambient dimension).
2856 self
.rank
.set_cache(min(n
,2))
2859 self
.one
.set_cache( self
.zero() )
2861 self
.one
.set_cache( self
.monomial(0) )
2864 def _max_random_instance_size():
2866 The maximum dimension of a random BilinearFormEJA.
2871 def random_instance(cls
, **kwargs
):
2873 Return a random instance of this algebra.
2875 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
() + 1)
2877 B
= matrix
.identity(ZZ
, n
)
2878 return cls(B
, **kwargs
)
2880 B11
= matrix
.identity(ZZ
, 1)
2881 M
= matrix
.random(ZZ
, n
-1)
2882 I
= matrix
.identity(ZZ
, n
-1)
2884 while alpha
.is_zero():
2885 alpha
= ZZ
.random_element().abs()
2886 B22
= M
.transpose()*M
+ alpha
*I
2888 from sage
.matrix
.special
import block_matrix
2889 B
= block_matrix(2,2, [ [B11
, ZZ(0) ],
2892 return cls(B
, **kwargs
)
2895 class JordanSpinEJA(BilinearFormEJA
):
2897 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
2898 with the usual inner product and jordan product ``x*y =
2899 (<x,y>, x0*y_bar + y0*x_bar)``. It has dimension `n` over
2904 sage: from mjo.eja.eja_algebra import JordanSpinEJA
2908 This multiplication table can be verified by hand::
2910 sage: J = JordanSpinEJA(4)
2911 sage: b0,b1,b2,b3 = J.gens()
2927 We can change the generator prefix::
2929 sage: JordanSpinEJA(2, prefix='B').gens()
2934 Ensure that we have the usual inner product on `R^n`::
2936 sage: set_random_seed()
2937 sage: J = JordanSpinEJA.random_instance()
2938 sage: x,y = J.random_elements(2)
2939 sage: actual = x.inner_product(y)
2940 sage: expected = x.to_vector().inner_product(y.to_vector())
2941 sage: actual == expected
2945 def __init__(self
, n
, *args
, **kwargs
):
2946 # This is a special case of the BilinearFormEJA with the
2947 # identity matrix as its bilinear form.
2948 B
= matrix
.identity(ZZ
, n
)
2950 # Don't orthonormalize because our basis is already
2951 # orthonormal with respect to our inner-product.
2952 if "orthonormalize" not in kwargs
: kwargs
["orthonormalize"] = False
2954 # But also don't pass check_field=False here, because the user
2955 # can pass in a field!
2956 super().__init
__(B
, *args
, **kwargs
)
2959 def _max_random_instance_size():
2961 The maximum dimension of a random JordanSpinEJA.
2966 def random_instance(cls
, **kwargs
):
2968 Return a random instance of this type of algebra.
2970 Needed here to override the implementation for ``BilinearFormEJA``.
2972 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
() + 1)
2973 return cls(n
, **kwargs
)
2976 class TrivialEJA(RationalBasisEJA
, ConcreteEJA
):
2978 The trivial Euclidean Jordan algebra consisting of only a zero element.
2982 sage: from mjo.eja.eja_algebra import TrivialEJA
2986 sage: J = TrivialEJA()
2993 sage: 7*J.one()*12*J.one()
2995 sage: J.one().inner_product(J.one())
2997 sage: J.one().norm()
2999 sage: J.one().subalgebra_generated_by()
3000 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
3005 def __init__(self
, **kwargs
):
3006 jordan_product
= lambda x
,y
: x
3007 inner_product
= lambda x
,y
: 0
3010 # New defaults for keyword arguments
3011 if "orthonormalize" not in kwargs
: kwargs
["orthonormalize"] = False
3012 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
3014 super().__init
__(basis
,
3020 # The rank is zero using my definition, namely the dimension of the
3021 # largest subalgebra generated by any element.
3022 self
.rank
.set_cache(0)
3023 self
.one
.set_cache( self
.zero() )
3026 def random_instance(cls
, **kwargs
):
3027 # We don't take a "size" argument so the superclass method is
3028 # inappropriate for us.
3029 return cls(**kwargs
)
3032 class CartesianProductEJA(FiniteDimensionalEJA
):
3034 The external (orthogonal) direct sum of two or more Euclidean
3035 Jordan algebras. Every Euclidean Jordan algebra decomposes into an
3036 orthogonal direct sum of simple Euclidean Jordan algebras which is
3037 then isometric to a Cartesian product, so no generality is lost by
3038 providing only this construction.
3042 sage: from mjo.eja.eja_algebra import (random_eja,
3043 ....: CartesianProductEJA,
3045 ....: JordanSpinEJA,
3046 ....: RealSymmetricEJA)
3050 The Jordan product is inherited from our factors and implemented by
3051 our CombinatorialFreeModule Cartesian product superclass::
3053 sage: set_random_seed()
3054 sage: J1 = HadamardEJA(2)
3055 sage: J2 = RealSymmetricEJA(2)
3056 sage: J = cartesian_product([J1,J2])
3057 sage: x,y = J.random_elements(2)
3061 The ability to retrieve the original factors is implemented by our
3062 CombinatorialFreeModule Cartesian product superclass::
3064 sage: J1 = HadamardEJA(2, field=QQ)
3065 sage: J2 = JordanSpinEJA(3, field=QQ)
3066 sage: J = cartesian_product([J1,J2])
3067 sage: J.cartesian_factors()
3068 (Euclidean Jordan algebra of dimension 2 over Rational Field,
3069 Euclidean Jordan algebra of dimension 3 over Rational Field)
3071 You can provide more than two factors::
3073 sage: J1 = HadamardEJA(2)
3074 sage: J2 = JordanSpinEJA(3)
3075 sage: J3 = RealSymmetricEJA(3)
3076 sage: cartesian_product([J1,J2,J3])
3077 Euclidean Jordan algebra of dimension 2 over Algebraic Real
3078 Field (+) Euclidean Jordan algebra of dimension 3 over Algebraic
3079 Real Field (+) Euclidean Jordan algebra of dimension 6 over
3080 Algebraic Real Field
3082 Rank is additive on a Cartesian product::
3084 sage: J1 = HadamardEJA(1)
3085 sage: J2 = RealSymmetricEJA(2)
3086 sage: J = cartesian_product([J1,J2])
3087 sage: J1.rank.clear_cache()
3088 sage: J2.rank.clear_cache()
3089 sage: J.rank.clear_cache()
3092 sage: J.rank() == J1.rank() + J2.rank()
3095 The same rank computation works over the rationals, with whatever
3098 sage: J1 = HadamardEJA(1, field=QQ, orthonormalize=False)
3099 sage: J2 = RealSymmetricEJA(2, field=QQ, orthonormalize=False)
3100 sage: J = cartesian_product([J1,J2])
3101 sage: J1.rank.clear_cache()
3102 sage: J2.rank.clear_cache()
3103 sage: J.rank.clear_cache()
3106 sage: J.rank() == J1.rank() + J2.rank()
3109 The product algebra will be associative if and only if all of its
3110 components are associative::
3112 sage: J1 = HadamardEJA(2)
3113 sage: J1.is_associative()
3115 sage: J2 = HadamardEJA(3)
3116 sage: J2.is_associative()
3118 sage: J3 = RealSymmetricEJA(3)
3119 sage: J3.is_associative()
3121 sage: CP1 = cartesian_product([J1,J2])
3122 sage: CP1.is_associative()
3124 sage: CP2 = cartesian_product([J1,J3])
3125 sage: CP2.is_associative()
3128 Cartesian products of Cartesian products work::
3130 sage: J1 = JordanSpinEJA(1)
3131 sage: J2 = JordanSpinEJA(1)
3132 sage: J3 = JordanSpinEJA(1)
3133 sage: J = cartesian_product([J1,cartesian_product([J2,J3])])
3134 sage: J.multiplication_table()
3135 +----++----+----+----+
3136 | * || b0 | b1 | b2 |
3137 +====++====+====+====+
3138 | b0 || b0 | 0 | 0 |
3139 +----++----+----+----+
3140 | b1 || 0 | b1 | 0 |
3141 +----++----+----+----+
3142 | b2 || 0 | 0 | b2 |
3143 +----++----+----+----+
3144 sage: HadamardEJA(3).multiplication_table()
3145 +----++----+----+----+
3146 | * || b0 | b1 | b2 |
3147 +====++====+====+====+
3148 | b0 || b0 | 0 | 0 |
3149 +----++----+----+----+
3150 | b1 || 0 | b1 | 0 |
3151 +----++----+----+----+
3152 | b2 || 0 | 0 | b2 |
3153 +----++----+----+----+
3157 All factors must share the same base field::
3159 sage: J1 = HadamardEJA(2, field=QQ)
3160 sage: J2 = RealSymmetricEJA(2)
3161 sage: CartesianProductEJA((J1,J2))
3162 Traceback (most recent call last):
3164 ValueError: all factors must share the same base field
3166 The cached unit element is the same one that would be computed::
3168 sage: set_random_seed() # long time
3169 sage: J1 = random_eja() # long time
3170 sage: J2 = random_eja() # long time
3171 sage: J = cartesian_product([J1,J2]) # long time
3172 sage: actual = J.one() # long time
3173 sage: J.one.clear_cache() # long time
3174 sage: expected = J.one() # long time
3175 sage: actual == expected # long time
3179 Element
= FiniteDimensionalEJAElement
3182 def __init__(self
, factors
, **kwargs
):
3187 self
._sets
= factors
3189 field
= factors
[0].base_ring()
3190 if not all( J
.base_ring() == field
for J
in factors
):
3191 raise ValueError("all factors must share the same base field")
3193 associative
= all( f
.is_associative() for f
in factors
)
3195 MS
= self
.matrix_space()
3199 for b
in factors
[i
].matrix_basis():
3204 basis
= tuple( MS(b
) for b
in basis
)
3206 # Define jordan/inner products that operate on that matrix_basis.
3207 def jordan_product(x
,y
):
3209 (factors
[i
](x
[i
])*factors
[i
](y
[i
])).to_matrix()
3213 def inner_product(x
, y
):
3215 factors
[i
](x
[i
]).inner_product(factors
[i
](y
[i
]))
3219 # There's no need to check the field since it already came
3220 # from an EJA. Likewise the axioms are guaranteed to be
3221 # satisfied, unless the guy writing this class sucks.
3223 # If you want the basis to be orthonormalized, orthonormalize
3225 FiniteDimensionalEJA
.__init
__(self
,
3230 orthonormalize
=False,
3231 associative
=associative
,
3232 cartesian_product
=True,
3236 ones
= tuple(J
.one().to_matrix() for J
in factors
)
3237 self
.one
.set_cache(self(ones
))
3238 self
.rank
.set_cache(sum(J
.rank() for J
in factors
))
3240 def cartesian_factors(self
):
3241 # Copy/pasted from CombinatorialFreeModule_CartesianProduct.
3244 def cartesian_factor(self
, i
):
3246 Return the ``i``th factor of this algebra.
3248 return self
._sets
[i
]
3251 # Copy/pasted from CombinatorialFreeModule_CartesianProduct.
3252 from sage
.categories
.cartesian_product
import cartesian_product
3253 return cartesian_product
.symbol
.join("%s" % factor
3254 for factor
in self
._sets
)
3256 def matrix_space(self
):
3258 Return the space that our matrix basis lives in as a Cartesian
3261 We don't simply use the ``cartesian_product()`` functor here
3262 because it acts differently on SageMath MatrixSpaces and our
3263 custom MatrixAlgebras, which are CombinatorialFreeModules. We
3264 always want the result to be represented (and indexed) as
3269 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
3271 ....: OctonionHermitianEJA,
3272 ....: RealSymmetricEJA)
3276 sage: J1 = HadamardEJA(1)
3277 sage: J2 = RealSymmetricEJA(2)
3278 sage: J = cartesian_product([J1,J2])
3279 sage: J.matrix_space()
3280 The Cartesian product of (Full MatrixSpace of 1 by 1 dense
3281 matrices over Algebraic Real Field, Full MatrixSpace of 2
3282 by 2 dense matrices over Algebraic Real Field)
3286 sage: J1 = ComplexHermitianEJA(1)
3287 sage: J2 = ComplexHermitianEJA(1)
3288 sage: J = cartesian_product([J1,J2])
3289 sage: J.one().to_matrix()[0]
3292 sage: J.one().to_matrix()[1]
3298 sage: J1 = OctonionHermitianEJA(1)
3299 sage: J2 = OctonionHermitianEJA(1)
3300 sage: J = cartesian_product([J1,J2])
3301 sage: J.one().to_matrix()[0]
3305 sage: J.one().to_matrix()[1]
3311 scalars
= self
.cartesian_factor(0).base_ring()
3313 # This category isn't perfect, but is good enough for what we
3315 cat
= MagmaticAlgebras(scalars
).FiniteDimensional().WithBasis()
3316 cat
= cat
.Unital().CartesianProducts()
3317 factors
= tuple( J
.matrix_space() for J
in self
.cartesian_factors() )
3319 from sage
.sets
.cartesian_product
import CartesianProduct
3320 return CartesianProduct(factors
, cat
)
3324 def cartesian_projection(self
, i
):
3328 sage: from mjo.eja.eja_algebra import (random_eja,
3329 ....: JordanSpinEJA,
3331 ....: RealSymmetricEJA,
3332 ....: ComplexHermitianEJA)
3336 The projection morphisms are Euclidean Jordan algebra
3339 sage: J1 = HadamardEJA(2)
3340 sage: J2 = RealSymmetricEJA(2)
3341 sage: J = cartesian_product([J1,J2])
3342 sage: J.cartesian_projection(0)
3343 Linear operator between finite-dimensional Euclidean Jordan
3344 algebras represented by the matrix:
3347 Domain: Euclidean Jordan algebra of dimension 2 over Algebraic
3348 Real Field (+) Euclidean Jordan algebra of dimension 3 over
3349 Algebraic Real Field
3350 Codomain: Euclidean Jordan algebra of dimension 2 over Algebraic
3352 sage: J.cartesian_projection(1)
3353 Linear operator between finite-dimensional Euclidean Jordan
3354 algebras represented by the matrix:
3358 Domain: Euclidean Jordan algebra of dimension 2 over Algebraic
3359 Real Field (+) Euclidean Jordan algebra of dimension 3 over
3360 Algebraic Real Field
3361 Codomain: Euclidean Jordan algebra of dimension 3 over Algebraic
3364 The projections work the way you'd expect on the vector
3365 representation of an element::
3367 sage: J1 = JordanSpinEJA(2)
3368 sage: J2 = ComplexHermitianEJA(2)
3369 sage: J = cartesian_product([J1,J2])
3370 sage: pi_left = J.cartesian_projection(0)
3371 sage: pi_right = J.cartesian_projection(1)
3372 sage: pi_left(J.one()).to_vector()
3374 sage: pi_right(J.one()).to_vector()
3376 sage: J.one().to_vector()
3381 The answer never changes::
3383 sage: set_random_seed()
3384 sage: J1 = random_eja()
3385 sage: J2 = random_eja()
3386 sage: J = cartesian_product([J1,J2])
3387 sage: P0 = J.cartesian_projection(0)
3388 sage: P1 = J.cartesian_projection(0)
3393 offset
= sum( self
.cartesian_factor(k
).dimension()
3395 Ji
= self
.cartesian_factor(i
)
3396 Pi
= self
._module
_morphism
(lambda j
: Ji
.monomial(j
- offset
),
3399 return FiniteDimensionalEJAOperator(self
,Ji
,Pi
.matrix())
3402 def cartesian_embedding(self
, i
):
3406 sage: from mjo.eja.eja_algebra import (random_eja,
3407 ....: JordanSpinEJA,
3409 ....: RealSymmetricEJA)
3413 The embedding morphisms are Euclidean Jordan algebra
3416 sage: J1 = HadamardEJA(2)
3417 sage: J2 = RealSymmetricEJA(2)
3418 sage: J = cartesian_product([J1,J2])
3419 sage: J.cartesian_embedding(0)
3420 Linear operator between finite-dimensional Euclidean Jordan
3421 algebras represented by the matrix:
3427 Domain: Euclidean Jordan algebra of dimension 2 over
3428 Algebraic Real Field
3429 Codomain: Euclidean Jordan algebra of dimension 2 over
3430 Algebraic Real Field (+) Euclidean Jordan algebra of
3431 dimension 3 over Algebraic Real Field
3432 sage: J.cartesian_embedding(1)
3433 Linear operator between finite-dimensional Euclidean Jordan
3434 algebras represented by the matrix:
3440 Domain: Euclidean Jordan algebra of dimension 3 over
3441 Algebraic Real Field
3442 Codomain: Euclidean Jordan algebra of dimension 2 over
3443 Algebraic Real Field (+) Euclidean Jordan algebra of
3444 dimension 3 over Algebraic Real Field
3446 The embeddings work the way you'd expect on the vector
3447 representation of an element::
3449 sage: J1 = JordanSpinEJA(3)
3450 sage: J2 = RealSymmetricEJA(2)
3451 sage: J = cartesian_product([J1,J2])
3452 sage: iota_left = J.cartesian_embedding(0)
3453 sage: iota_right = J.cartesian_embedding(1)
3454 sage: iota_left(J1.zero()) == J.zero()
3456 sage: iota_right(J2.zero()) == J.zero()
3458 sage: J1.one().to_vector()
3460 sage: iota_left(J1.one()).to_vector()
3462 sage: J2.one().to_vector()
3464 sage: iota_right(J2.one()).to_vector()
3466 sage: J.one().to_vector()
3471 The answer never changes::
3473 sage: set_random_seed()
3474 sage: J1 = random_eja()
3475 sage: J2 = random_eja()
3476 sage: J = cartesian_product([J1,J2])
3477 sage: E0 = J.cartesian_embedding(0)
3478 sage: E1 = J.cartesian_embedding(0)
3482 Composing a projection with the corresponding inclusion should
3483 produce the identity map, and mismatching them should produce
3486 sage: set_random_seed()
3487 sage: J1 = random_eja()
3488 sage: J2 = random_eja()
3489 sage: J = cartesian_product([J1,J2])
3490 sage: iota_left = J.cartesian_embedding(0)
3491 sage: iota_right = J.cartesian_embedding(1)
3492 sage: pi_left = J.cartesian_projection(0)
3493 sage: pi_right = J.cartesian_projection(1)
3494 sage: pi_left*iota_left == J1.one().operator()
3496 sage: pi_right*iota_right == J2.one().operator()
3498 sage: (pi_left*iota_right).is_zero()
3500 sage: (pi_right*iota_left).is_zero()
3504 offset
= sum( self
.cartesian_factor(k
).dimension()
3506 Ji
= self
.cartesian_factor(i
)
3507 Ei
= Ji
._module
_morphism
(lambda j
: self
.monomial(j
+ offset
),
3509 return FiniteDimensionalEJAOperator(Ji
,self
,Ei
.matrix())
3513 FiniteDimensionalEJA
.CartesianProduct
= CartesianProductEJA
3515 class RationalBasisCartesianProductEJA(CartesianProductEJA
,
3518 A separate class for products of algebras for which we know a
3523 sage: from mjo.eja.eja_algebra import (HadamardEJA,
3524 ....: JordanSpinEJA,
3525 ....: OctonionHermitianEJA,
3526 ....: RealSymmetricEJA)
3530 This gives us fast characteristic polynomial computations in
3531 product algebras, too::
3534 sage: J1 = JordanSpinEJA(2)
3535 sage: J2 = RealSymmetricEJA(3)
3536 sage: J = cartesian_product([J1,J2])
3537 sage: J.characteristic_polynomial_of().degree()
3544 The ``cartesian_product()`` function only uses the first factor to
3545 decide where the result will live; thus we have to be careful to
3546 check that all factors do indeed have a `_rational_algebra` member
3547 before we try to access it::
3549 sage: J1 = OctonionHermitianEJA(1) # no rational basis
3550 sage: J2 = HadamardEJA(2)
3551 sage: cartesian_product([J1,J2])
3552 Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
3553 (+) Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
3554 sage: cartesian_product([J2,J1])
3555 Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
3556 (+) Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
3559 def __init__(self
, algebras
, **kwargs
):
3560 CartesianProductEJA
.__init
__(self
, algebras
, **kwargs
)
3562 self
._rational
_algebra
= None
3563 if self
.vector_space().base_field() is not QQ
:
3564 if all( hasattr(r
, "_rational_algebra") for r
in algebras
):
3565 self
._rational
_algebra
= cartesian_product([
3566 r
._rational
_algebra
for r
in algebras
3570 RationalBasisEJA
.CartesianProduct
= RationalBasisCartesianProductEJA
3572 def random_eja(*args
, **kwargs
):
3573 J1
= ConcreteEJA
.random_instance(*args
, **kwargs
)
3575 # This might make Cartesian products appear roughly as often as
3576 # any other ConcreteEJA.
3577 if ZZ
.random_element(len(ConcreteEJA
.__subclasses
__()) + 1) == 0:
3578 # Use random_eja() again so we can get more than two factors.
3579 J2
= random_eja(*args
, **kwargs
)
3580 J
= cartesian_product([J1
,J2
])