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.
55 At a minimum, the following are required to construct a Euclidean
58 * A basis of matrices, column vectors, or MatrixAlgebra elements
59 * A Jordan product defined on the basis
60 * Its inner product defined on the basis
62 The real numbers form a Euclidean Jordan algebra when both the Jordan
63 and inner products are the usual multiplication. We use this as our
64 example, and demonstrate a few ways to construct an EJA.
66 First, we can use one-by-one SageMath matrices with algebraic real
67 entries to represent real numbers. We define the Jordan and inner
68 products to be essentially real-number multiplication, with the only
69 difference being that the Jordan product again returns a one-by-one
70 matrix, whereas the inner product must return a scalar. Our basis for
71 the one-by-one matrices is of course the set consisting of a single
72 matrix with its sole entry non-zero::
74 sage: from mjo.eja.eja_algebra import FiniteDimensionalEJA
75 sage: jp = lambda X,Y: X*Y
76 sage: ip = lambda X,Y: X[0,0]*Y[0,0]
77 sage: b1 = matrix(AA, [[1]])
78 sage: J1 = FiniteDimensionalEJA((b1,), jp, ip)
80 Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
82 In fact, any positive scalar multiple of that inner-product would work::
84 sage: ip2 = lambda X,Y: 16*ip(X,Y)
85 sage: J2 = FiniteDimensionalEJA((b1,), jp, ip2)
87 Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
89 But beware that your basis will be orthonormalized _with respect to the
90 given inner-product_ unless you pass ``orthonormalize=False`` to the
91 constructor. For example::
93 sage: J3 = FiniteDimensionalEJA((b1,), jp, ip2, orthonormalize=False)
95 Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
97 To see the difference, you can take the first and only basis element
98 of the resulting algebra, and ask for it to be converted back into
101 sage: J1.basis()[0].to_matrix()
103 sage: J2.basis()[0].to_matrix()
105 sage: J3.basis()[0].to_matrix()
108 Since square roots are used in that process, the default scalar field
109 that we use is the field of algebraic real numbers, ``AA``. You can
110 also Use rational numbers, but only if you either pass
111 ``orthonormalize=False`` or know that orthonormalizing your basis
112 won't stray beyond the rational numbers. The example above would
113 have worked only because ``sqrt(16) == 4`` is rational.
115 Another option for your basis is to use elemebts of a
116 :class:`MatrixAlgebra`::
118 sage: from mjo.matrix_algebra import MatrixAlgebra
119 sage: A = MatrixAlgebra(1,AA,AA)
120 sage: J4 = FiniteDimensionalEJA(A.gens(), jp, ip)
122 Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
123 sage: J4.basis()[0].to_matrix()
128 An easier way to view the entire EJA basis in its original (but
129 perhaps orthonormalized) matrix form is to use the ``matrix_basis``
132 sage: J4.matrix_basis()
137 In particular, a :class:`MatrixAlgebra` is needed to work around the
138 fact that matrices in SageMath must have entries in the same
139 (commutative and associative) ring as its scalars. There are many
140 Euclidean Jordan algebras whose elements are matrices that violate
141 those assumptions. The complex, quaternion, and octonion Hermitian
142 matrices all have entries in a ring (the complex numbers, quaternions,
143 or octonions...) that differs from the algebra's scalar ring (the real
144 numbers). Quaternions are also non-commutative; the octonions are
145 neither commutative nor associative.
149 sage: from mjo.eja.eja_algebra import random_eja
154 Euclidean Jordan algebra of dimension...
157 from sage
.algebras
.quatalg
.quaternion_algebra
import QuaternionAlgebra
158 from sage
.categories
.magmatic_algebras
import MagmaticAlgebras
159 from sage
.categories
.sets_cat
import cartesian_product
160 from sage
.combinat
.free_module
import CombinatorialFreeModule
161 from sage
.matrix
.constructor
import matrix
162 from sage
.matrix
.matrix_space
import MatrixSpace
163 from sage
.misc
.cachefunc
import cached_method
164 from sage
.misc
.table
import table
165 from sage
.modules
.free_module
import FreeModule
, VectorSpace
166 from sage
.rings
.all
import (ZZ
, QQ
, AA
, QQbar
, RR
, RLF
, CLF
,
169 from mjo
.eja
.eja_element
import FiniteDimensionalEJAElement
170 from mjo
.eja
.eja_operator
import FiniteDimensionalEJAOperator
171 from mjo
.eja
.eja_utils
import _all2list
, _mat2vec
173 class FiniteDimensionalEJA(CombinatorialFreeModule
):
175 A finite-dimensional Euclidean Jordan algebra.
179 - ``basis`` -- a tuple; a tuple of basis elements in "matrix
180 form," which must be the same form as the arguments to
181 ``jordan_product`` and ``inner_product``. In reality, "matrix
182 form" can be either vectors, matrices, or a Cartesian product
183 (ordered tuple) of vectors or matrices. All of these would
184 ideally be vector spaces in sage with no special-casing
185 needed; but in reality we turn vectors into column-matrices
186 and Cartesian products `(a,b)` into column matrices
187 `(a,b)^{T}` after converting `a` and `b` themselves.
189 - ``jordan_product`` -- a function; afunction of two ``basis``
190 elements (in matrix form) that returns their jordan product,
191 also in matrix form; this will be applied to ``basis`` to
192 compute a multiplication table for the algebra.
194 - ``inner_product`` -- a function; a function of two ``basis``
195 elements (in matrix form) that returns their inner
196 product. This will be applied to ``basis`` to compute an
197 inner-product table (basically a matrix) for this algebra.
199 - ``matrix_space`` -- the space that your matrix basis lives in,
200 or ``None`` (the default). So long as your basis does not have
201 length zero you can omit this. But in trivial algebras, it is
204 - ``field`` -- a subfield of the reals (default: ``AA``); the scalar
205 field for the algebra.
207 - ``orthonormalize`` -- boolean (default: ``True``); whether or
208 not to orthonormalize the basis. Doing so is expensive and
209 generally rules out using the rationals as your ``field``, but
210 is required for spectral decompositions.
214 sage: from mjo.eja.eja_algebra import random_eja
218 We should compute that an element subalgebra is associative even
219 if we circumvent the element method::
221 sage: set_random_seed()
222 sage: J = random_eja(field=QQ,orthonormalize=False)
223 sage: x = J.random_element()
224 sage: A = x.subalgebra_generated_by(orthonormalize=False)
225 sage: basis = tuple(b.superalgebra_element() for b in A.basis())
226 sage: J.subalgebra(basis, orthonormalize=False).is_associative()
229 Element
= FiniteDimensionalEJAElement
239 cartesian_product
=False,
247 if not field
.is_subring(RR
):
248 # Note: this does return true for the real algebraic
249 # field, the rationals, and any quadratic field where
250 # we've specified a real embedding.
251 raise ValueError("scalar field is not real")
254 # Check commutativity of the Jordan and inner-products.
255 # This has to be done before we build the multiplication
256 # and inner-product tables/matrices, because we take
257 # advantage of symmetry in the process.
258 if not all( jordan_product(bi
,bj
) == jordan_product(bj
,bi
)
261 raise ValueError("Jordan product is not commutative")
263 if not all( inner_product(bi
,bj
) == inner_product(bj
,bi
)
266 raise ValueError("inner-product is not commutative")
269 category
= MagmaticAlgebras(field
).FiniteDimensional()
270 category
= category
.WithBasis().Unital().Commutative()
273 # All zero- and one-dimensional algebras are just the real
274 # numbers with (some positive multiples of) the usual
275 # multiplication as its Jordan and inner-product.
277 if associative
is None:
278 # We should figure it out. As with check_axioms, we have to do
279 # this without the help of the _jordan_product_is_associative()
280 # method because we need to know the category before we
281 # initialize the algebra.
282 associative
= all( jordan_product(jordan_product(bi
,bj
),bk
)
284 jordan_product(bi
,jordan_product(bj
,bk
))
290 # Element subalgebras can take advantage of this.
291 category
= category
.Associative()
292 if cartesian_product
:
293 # Use join() here because otherwise we only get the
294 # "Cartesian product of..." and not the things themselves.
295 category
= category
.join([category
,
296 category
.CartesianProducts()])
298 # Call the superclass constructor so that we can use its from_vector()
299 # method to build our multiplication table.
300 CombinatorialFreeModule
.__init
__(self
,
307 # Now comes all of the hard work. We'll be constructing an
308 # ambient vector space V that our (vectorized) basis lives in,
309 # as well as a subspace W of V spanned by those (vectorized)
310 # basis elements. The W-coordinates are the coefficients that
311 # we see in things like x = 1*b1 + 2*b2.
316 degree
= len(_all2list(basis
[0]))
318 # Build an ambient space that fits our matrix basis when
319 # written out as "long vectors."
320 V
= VectorSpace(field
, degree
)
322 # The matrix that will hold the orthonormal -> unorthonormal
323 # coordinate transformation. Default to an identity matrix of
324 # the appropriate size to avoid special cases for None
326 self
._deortho
_matrix
= matrix
.identity(field
,n
)
329 # Save a copy of the un-orthonormalized basis for later.
330 # Convert it to ambient V (vector) coordinates while we're
331 # at it, because we'd have to do it later anyway.
332 deortho_vector_basis
= tuple( V(_all2list(b
)) for b
in basis
)
334 from mjo
.eja
.eja_utils
import gram_schmidt
335 basis
= tuple(gram_schmidt(basis
, inner_product
))
337 # Save the (possibly orthonormalized) matrix basis for
338 # later, as well as the space that its elements live in.
339 # In most cases we can deduce the matrix space, but when
340 # n == 0 (that is, there are no basis elements) we cannot.
341 self
._matrix
_basis
= basis
342 if matrix_space
is None:
343 self
._matrix
_space
= self
._matrix
_basis
[0].parent()
345 self
._matrix
_space
= matrix_space
347 # Now create the vector space for the algebra, which will have
348 # its own set of non-ambient coordinates (in terms of the
350 vector_basis
= tuple( V(_all2list(b
)) for b
in basis
)
351 W
= V
.span_of_basis( vector_basis
, check
=check_axioms
)
354 # Now "W" is the vector space of our algebra coordinates. The
355 # variables "X1", "X2",... refer to the entries of vectors in
356 # W. Thus to convert back and forth between the orthonormal
357 # coordinates and the given ones, we need to stick the original
359 U
= V
.span_of_basis( deortho_vector_basis
, check
=check_axioms
)
360 self
._deortho
_matrix
= matrix
.column( U
.coordinate_vector(q
)
361 for q
in vector_basis
)
364 # Now we actually compute the multiplication and inner-product
365 # tables/matrices using the possibly-orthonormalized basis.
366 self
._inner
_product
_matrix
= matrix
.identity(field
, n
)
367 self
._multiplication
_table
= [ [0 for j
in range(i
+1)]
370 # Note: the Jordan and inner-products are defined in terms
371 # of the ambient basis. It's important that their arguments
372 # are in ambient coordinates as well.
375 # ortho basis w.r.t. ambient coords
379 # The jordan product returns a matrixy answer, so we
380 # have to convert it to the algebra coordinates.
381 elt
= jordan_product(q_i
, q_j
)
382 elt
= W
.coordinate_vector(V(_all2list(elt
)))
383 self
._multiplication
_table
[i
][j
] = self
.from_vector(elt
)
385 if not orthonormalize
:
386 # If we're orthonormalizing the basis with respect
387 # to an inner-product, then the inner-product
388 # matrix with respect to the resulting basis is
389 # just going to be the identity.
390 ip
= inner_product(q_i
, q_j
)
391 self
._inner
_product
_matrix
[i
,j
] = ip
392 self
._inner
_product
_matrix
[j
,i
] = ip
394 self
._inner
_product
_matrix
._cache
= {'hermitian': True}
395 self
._inner
_product
_matrix
.set_immutable()
398 if not self
._is
_jordanian
():
399 raise ValueError("Jordan identity does not hold")
400 if not self
._inner
_product
_is
_associative
():
401 raise ValueError("inner product is not associative")
404 def _coerce_map_from_base_ring(self
):
406 Disable the map from the base ring into the algebra.
408 Performing a nonsense conversion like this automatically
409 is counterpedagogical. The fallback is to try the usual
410 element constructor, which should also fail.
414 sage: from mjo.eja.eja_algebra import random_eja
418 sage: set_random_seed()
419 sage: J = random_eja()
421 Traceback (most recent call last):
423 ValueError: not an element of this algebra
429 def product_on_basis(self
, i
, j
):
431 Returns the Jordan product of the `i` and `j`th basis elements.
433 This completely defines the Jordan product on the algebra, and
434 is used direclty by our superclass machinery to implement
439 sage: from mjo.eja.eja_algebra import random_eja
443 sage: set_random_seed()
444 sage: J = random_eja()
445 sage: n = J.dimension()
448 sage: bi_bj = J.zero()*J.zero()
450 ....: i = ZZ.random_element(n)
451 ....: j = ZZ.random_element(n)
452 ....: bi = J.monomial(i)
453 ....: bj = J.monomial(j)
454 ....: bi_bj = J.product_on_basis(i,j)
459 # We only stored the lower-triangular portion of the
460 # multiplication table.
462 return self
._multiplication
_table
[i
][j
]
464 return self
._multiplication
_table
[j
][i
]
466 def inner_product(self
, x
, y
):
468 The inner product associated with this Euclidean Jordan algebra.
470 Defaults to the trace inner product, but can be overridden by
471 subclasses if they are sure that the necessary properties are
476 sage: from mjo.eja.eja_algebra import (random_eja,
478 ....: BilinearFormEJA)
482 Our inner product is "associative," which means the following for
483 a symmetric bilinear form::
485 sage: set_random_seed()
486 sage: J = random_eja()
487 sage: x,y,z = J.random_elements(3)
488 sage: (x*y).inner_product(z) == y.inner_product(x*z)
493 Ensure that this is the usual inner product for the algebras
496 sage: set_random_seed()
497 sage: J = HadamardEJA.random_instance()
498 sage: x,y = J.random_elements(2)
499 sage: actual = x.inner_product(y)
500 sage: expected = x.to_vector().inner_product(y.to_vector())
501 sage: actual == expected
504 Ensure that this is one-half of the trace inner-product in a
505 BilinearFormEJA that isn't just the reals (when ``n`` isn't
506 one). This is in Faraut and Koranyi, and also my "On the
509 sage: set_random_seed()
510 sage: J = BilinearFormEJA.random_instance()
511 sage: n = J.dimension()
512 sage: x = J.random_element()
513 sage: y = J.random_element()
514 sage: (n == 1) or (x.inner_product(y) == (x*y).trace()/2)
518 B
= self
._inner
_product
_matrix
519 return (B
*x
.to_vector()).inner_product(y
.to_vector())
522 def is_associative(self
):
524 Return whether or not this algebra's Jordan product is associative.
528 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
532 sage: J = ComplexHermitianEJA(3, field=QQ, orthonormalize=False)
533 sage: J.is_associative()
535 sage: x = sum(J.gens())
536 sage: A = x.subalgebra_generated_by(orthonormalize=False)
537 sage: A.is_associative()
541 return "Associative" in self
.category().axioms()
543 def _is_commutative(self
):
545 Whether or not this algebra's multiplication table is commutative.
547 This method should of course always return ``True``, unless
548 this algebra was constructed with ``check_axioms=False`` and
549 passed an invalid multiplication table.
551 return all( x
*y
== y
*x
for x
in self
.gens() for y
in self
.gens() )
553 def _is_jordanian(self
):
555 Whether or not this algebra's multiplication table respects the
556 Jordan identity `(x^{2})(xy) = x(x^{2}y)`.
558 We only check one arrangement of `x` and `y`, so for a
559 ``True`` result to be truly true, you should also check
560 :meth:`_is_commutative`. This method should of course always
561 return ``True``, unless this algebra was constructed with
562 ``check_axioms=False`` and passed an invalid multiplication table.
564 return all( (self
.monomial(i
)**2)*(self
.monomial(i
)*self
.monomial(j
))
566 (self
.monomial(i
))*((self
.monomial(i
)**2)*self
.monomial(j
))
567 for i
in range(self
.dimension())
568 for j
in range(self
.dimension()) )
570 def _jordan_product_is_associative(self
):
572 Return whether or not this algebra's Jordan product is
573 associative; that is, whether or not `x*(y*z) = (x*y)*z`
576 This method should agree with :meth:`is_associative` unless
577 you lied about the value of the ``associative`` parameter
578 when you constructed the algebra.
582 sage: from mjo.eja.eja_algebra import (random_eja,
583 ....: RealSymmetricEJA,
584 ....: ComplexHermitianEJA,
585 ....: QuaternionHermitianEJA)
589 sage: J = RealSymmetricEJA(4, orthonormalize=False)
590 sage: J._jordan_product_is_associative()
592 sage: x = sum(J.gens())
593 sage: A = x.subalgebra_generated_by()
594 sage: A._jordan_product_is_associative()
599 sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
600 sage: J._jordan_product_is_associative()
602 sage: x = sum(J.gens())
603 sage: A = x.subalgebra_generated_by(orthonormalize=False)
604 sage: A._jordan_product_is_associative()
609 sage: J = QuaternionHermitianEJA(2)
610 sage: J._jordan_product_is_associative()
612 sage: x = sum(J.gens())
613 sage: A = x.subalgebra_generated_by()
614 sage: A._jordan_product_is_associative()
619 The values we've presupplied to the constructors agree with
622 sage: set_random_seed()
623 sage: J = random_eja()
624 sage: J.is_associative() == J._jordan_product_is_associative()
630 # Used to check whether or not something is zero.
633 # I don't know of any examples that make this magnitude
634 # necessary because I don't know how to make an
635 # associative algebra when the element subalgebra
636 # construction is unreliable (as it is over RDF; we can't
637 # find the degree of an element because we can't compute
638 # the rank of a matrix). But even multiplication of floats
639 # is non-associative, so *some* epsilon is needed... let's
640 # just take the one from _inner_product_is_associative?
643 for i
in range(self
.dimension()):
644 for j
in range(self
.dimension()):
645 for k
in range(self
.dimension()):
649 diff
= (x
*y
)*z
- x
*(y
*z
)
651 if diff
.norm() > epsilon
:
656 def _inner_product_is_associative(self
):
658 Return whether or not this algebra's inner product `B` is
659 associative; that is, whether or not `B(xy,z) = B(x,yz)`.
661 This method should of course always return ``True``, unless
662 this algebra was constructed with ``check_axioms=False`` and
663 passed an invalid Jordan or inner-product.
667 # Used to check whether or not something is zero.
670 # This choice is sufficient to allow the construction of
671 # QuaternionHermitianEJA(2, field=RDF) with check_axioms=True.
674 for i
in range(self
.dimension()):
675 for j
in range(self
.dimension()):
676 for k
in range(self
.dimension()):
680 diff
= (x
*y
).inner_product(z
) - x
.inner_product(y
*z
)
682 if diff
.abs() > epsilon
:
687 def _element_constructor_(self
, elt
):
689 Construct an element of this algebra from its vector or matrix
692 This gets called only after the parent element _call_ method
693 fails to find a coercion for the argument.
697 sage: from mjo.eja.eja_algebra import (random_eja,
700 ....: RealSymmetricEJA)
704 The identity in `S^n` is converted to the identity in the EJA::
706 sage: J = RealSymmetricEJA(3)
707 sage: I = matrix.identity(QQ,3)
708 sage: J(I) == J.one()
711 This skew-symmetric matrix can't be represented in the EJA::
713 sage: J = RealSymmetricEJA(3)
714 sage: A = matrix(QQ,3, lambda i,j: i-j)
716 Traceback (most recent call last):
718 ValueError: not an element of this algebra
720 Tuples work as well, provided that the matrix basis for the
721 algebra consists of them::
723 sage: J1 = HadamardEJA(3)
724 sage: J2 = RealSymmetricEJA(2)
725 sage: J = cartesian_product([J1,J2])
726 sage: J( (J1.matrix_basis()[1], J2.matrix_basis()[2]) )
731 Ensure that we can convert any element back and forth
732 faithfully between its matrix and algebra representations::
734 sage: set_random_seed()
735 sage: J = random_eja()
736 sage: x = J.random_element()
737 sage: J(x.to_matrix()) == x
740 We cannot coerce elements between algebras just because their
741 matrix representations are compatible::
743 sage: J1 = HadamardEJA(3)
744 sage: J2 = JordanSpinEJA(3)
746 Traceback (most recent call last):
748 ValueError: not an element of this algebra
750 Traceback (most recent call last):
752 ValueError: not an element of this algebra
754 msg
= "not an element of this algebra"
755 if elt
in self
.base_ring():
756 # Ensure that no base ring -> algebra coercion is performed
757 # by this method. There's some stupidity in sage that would
758 # otherwise propagate to this method; for example, sage thinks
759 # that the integer 3 belongs to the space of 2-by-2 matrices.
760 raise ValueError(msg
)
763 # Try to convert a vector into a column-matrix...
765 except (AttributeError, TypeError):
766 # and ignore failure, because we weren't really expecting
767 # a vector as an argument anyway.
770 if elt
not in self
.matrix_space():
771 raise ValueError(msg
)
773 # Thanks for nothing! Matrix spaces aren't vector spaces in
774 # Sage, so we have to figure out its matrix-basis coordinates
775 # ourselves. We use the basis space's ring instead of the
776 # element's ring because the basis space might be an algebraic
777 # closure whereas the base ring of the 3-by-3 identity matrix
778 # could be QQ instead of QQbar.
780 # And, we also have to handle Cartesian product bases (when
781 # the matrix basis consists of tuples) here. The "good news"
782 # is that we're already converting everything to long vectors,
783 # and that strategy works for tuples as well.
785 # We pass check=False because the matrix basis is "guaranteed"
786 # to be linearly independent... right? Ha ha.
788 V
= VectorSpace(self
.base_ring(), len(elt
))
789 W
= V
.span_of_basis( (V(_all2list(s
)) for s
in self
.matrix_basis()),
793 coords
= W
.coordinate_vector(V(elt
))
794 except ArithmeticError: # vector is not in free module
795 raise ValueError(msg
)
797 return self
.from_vector(coords
)
801 Return a string representation of ``self``.
805 sage: from mjo.eja.eja_algebra import JordanSpinEJA
809 Ensure that it says what we think it says::
811 sage: JordanSpinEJA(2, field=AA)
812 Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
813 sage: JordanSpinEJA(3, field=RDF)
814 Euclidean Jordan algebra of dimension 3 over Real Double Field
817 fmt
= "Euclidean Jordan algebra of dimension {} over {}"
818 return fmt
.format(self
.dimension(), self
.base_ring())
822 def characteristic_polynomial_of(self
):
824 Return the algebra's "characteristic polynomial of" function,
825 which is itself a multivariate polynomial that, when evaluated
826 at the coordinates of some algebra element, returns that
827 element's characteristic polynomial.
829 The resulting polynomial has `n+1` variables, where `n` is the
830 dimension of this algebra. The first `n` variables correspond to
831 the coordinates of an algebra element: when evaluated at the
832 coordinates of an algebra element with respect to a certain
833 basis, the result is a univariate polynomial (in the one
834 remaining variable ``t``), namely the characteristic polynomial
839 sage: from mjo.eja.eja_algebra import JordanSpinEJA, TrivialEJA
843 The characteristic polynomial in the spin algebra is given in
844 Alizadeh, Example 11.11::
846 sage: J = JordanSpinEJA(3)
847 sage: p = J.characteristic_polynomial_of(); p
848 X1^2 - X2^2 - X3^2 + (-2*t)*X1 + t^2
849 sage: xvec = J.one().to_vector()
853 By definition, the characteristic polynomial is a monic
854 degree-zero polynomial in a rank-zero algebra. Note that
855 Cayley-Hamilton is indeed satisfied since the polynomial
856 ``1`` evaluates to the identity element of the algebra on
859 sage: J = TrivialEJA()
860 sage: J.characteristic_polynomial_of()
867 # The list of coefficient polynomials a_0, a_1, a_2, ..., a_(r-1).
868 a
= self
._charpoly
_coefficients
()
870 # We go to a bit of trouble here to reorder the
871 # indeterminates, so that it's easier to evaluate the
872 # characteristic polynomial at x's coordinates and get back
873 # something in terms of t, which is what we want.
874 S
= PolynomialRing(self
.base_ring(),'t')
878 S
= PolynomialRing(S
, R
.variable_names())
881 return (t
**r
+ sum( a
[k
]*(t
**k
) for k
in range(r
) ))
883 def coordinate_polynomial_ring(self
):
885 The multivariate polynomial ring in which this algebra's
886 :meth:`characteristic_polynomial_of` lives.
890 sage: from mjo.eja.eja_algebra import (HadamardEJA,
891 ....: RealSymmetricEJA)
895 sage: J = HadamardEJA(2)
896 sage: J.coordinate_polynomial_ring()
897 Multivariate Polynomial Ring in X1, X2...
898 sage: J = RealSymmetricEJA(3,field=QQ,orthonormalize=False)
899 sage: J.coordinate_polynomial_ring()
900 Multivariate Polynomial Ring in X1, X2, X3, X4, X5, X6...
903 var_names
= tuple( "X%d" % z
for z
in range(1, self
.dimension()+1) )
904 return PolynomialRing(self
.base_ring(), var_names
)
906 def inner_product(self
, x
, y
):
908 The inner product associated with this Euclidean Jordan algebra.
910 Defaults to the trace inner product, but can be overridden by
911 subclasses if they are sure that the necessary properties are
916 sage: from mjo.eja.eja_algebra import (random_eja,
918 ....: BilinearFormEJA)
922 Our inner product is "associative," which means the following for
923 a symmetric bilinear form::
925 sage: set_random_seed()
926 sage: J = random_eja()
927 sage: x,y,z = J.random_elements(3)
928 sage: (x*y).inner_product(z) == y.inner_product(x*z)
933 Ensure that this is the usual inner product for the algebras
936 sage: set_random_seed()
937 sage: J = HadamardEJA.random_instance()
938 sage: x,y = J.random_elements(2)
939 sage: actual = x.inner_product(y)
940 sage: expected = x.to_vector().inner_product(y.to_vector())
941 sage: actual == expected
944 Ensure that this is one-half of the trace inner-product in a
945 BilinearFormEJA that isn't just the reals (when ``n`` isn't
946 one). This is in Faraut and Koranyi, and also my "On the
949 sage: set_random_seed()
950 sage: J = BilinearFormEJA.random_instance()
951 sage: n = J.dimension()
952 sage: x = J.random_element()
953 sage: y = J.random_element()
954 sage: (n == 1) or (x.inner_product(y) == (x*y).trace()/2)
957 B
= self
._inner
_product
_matrix
958 return (B
*x
.to_vector()).inner_product(y
.to_vector())
961 def is_trivial(self
):
963 Return whether or not this algebra is trivial.
965 A trivial algebra contains only the zero element.
969 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
974 sage: J = ComplexHermitianEJA(3)
980 sage: J = TrivialEJA()
985 return self
.dimension() == 0
988 def multiplication_table(self
):
990 Return a visual representation of this algebra's multiplication
991 table (on basis elements).
995 sage: from mjo.eja.eja_algebra import JordanSpinEJA
999 sage: J = JordanSpinEJA(4)
1000 sage: J.multiplication_table()
1001 +----++----+----+----+----+
1002 | * || b0 | b1 | b2 | b3 |
1003 +====++====+====+====+====+
1004 | b0 || b0 | b1 | b2 | b3 |
1005 +----++----+----+----+----+
1006 | b1 || b1 | b0 | 0 | 0 |
1007 +----++----+----+----+----+
1008 | b2 || b2 | 0 | b0 | 0 |
1009 +----++----+----+----+----+
1010 | b3 || b3 | 0 | 0 | b0 |
1011 +----++----+----+----+----+
1014 n
= self
.dimension()
1015 # Prepend the header row.
1016 M
= [["*"] + list(self
.gens())]
1018 # And to each subsequent row, prepend an entry that belongs to
1019 # the left-side "header column."
1020 M
+= [ [self
.monomial(i
)] + [ self
.monomial(i
)*self
.monomial(j
)
1024 return table(M
, header_row
=True, header_column
=True, frame
=True)
1027 def matrix_basis(self
):
1029 Return an (often more natural) representation of this algebras
1030 basis as an ordered tuple of matrices.
1032 Every finite-dimensional Euclidean Jordan Algebra is a, up to
1033 Jordan isomorphism, a direct sum of five simple
1034 algebras---four of which comprise Hermitian matrices. And the
1035 last type of algebra can of course be thought of as `n`-by-`1`
1036 column matrices (ambiguusly called column vectors) to avoid
1037 special cases. As a result, matrices (and column vectors) are
1038 a natural representation format for Euclidean Jordan algebra
1041 But, when we construct an algebra from a basis of matrices,
1042 those matrix representations are lost in favor of coordinate
1043 vectors *with respect to* that basis. We could eventually
1044 convert back if we tried hard enough, but having the original
1045 representations handy is valuable enough that we simply store
1046 them and return them from this method.
1048 Why implement this for non-matrix algebras? Avoiding special
1049 cases for the :class:`BilinearFormEJA` pays with simplicity in
1050 its own right. But mainly, we would like to be able to assume
1051 that elements of a :class:`CartesianProductEJA` can be displayed
1052 nicely, without having to have special classes for direct sums
1053 one of whose components was a matrix algebra.
1057 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
1058 ....: RealSymmetricEJA)
1062 sage: J = RealSymmetricEJA(2)
1064 Finite family {0: b0, 1: b1, 2: b2}
1065 sage: J.matrix_basis()
1067 [1 0] [ 0 0.7071067811865475?] [0 0]
1068 [0 0], [0.7071067811865475? 0], [0 1]
1073 sage: J = JordanSpinEJA(2)
1075 Finite family {0: b0, 1: b1}
1076 sage: J.matrix_basis()
1082 return self
._matrix
_basis
1085 def matrix_space(self
):
1087 Return the matrix space in which this algebra's elements live, if
1088 we think of them as matrices (including column vectors of the
1091 "By default" this will be an `n`-by-`1` column-matrix space,
1092 except when the algebra is trivial. There it's `n`-by-`n`
1093 (where `n` is zero), to ensure that two elements of the matrix
1094 space (empty matrices) can be multiplied. For algebras of
1095 matrices, this returns the space in which their
1096 real embeddings live.
1100 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
1101 ....: JordanSpinEJA,
1102 ....: QuaternionHermitianEJA,
1107 By default, the matrix representation is just a column-matrix
1108 equivalent to the vector representation::
1110 sage: J = JordanSpinEJA(3)
1111 sage: J.matrix_space()
1112 Full MatrixSpace of 3 by 1 dense matrices over Algebraic
1115 The matrix representation in the trivial algebra is
1116 zero-by-zero instead of the usual `n`-by-one::
1118 sage: J = TrivialEJA()
1119 sage: J.matrix_space()
1120 Full MatrixSpace of 0 by 0 dense matrices over Algebraic
1123 The matrix space for complex/quaternion Hermitian matrix EJA
1124 is the space in which their real-embeddings live, not the
1125 original complex/quaternion matrix space::
1127 sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
1128 sage: J.matrix_space()
1129 Module of 2 by 2 matrices with entries in Algebraic Field over
1130 the scalar ring Rational Field
1131 sage: J = QuaternionHermitianEJA(1,field=QQ,orthonormalize=False)
1132 sage: J.matrix_space()
1133 Module of 1 by 1 matrices with entries in Quaternion
1134 Algebra (-1, -1) with base ring Rational Field over
1135 the scalar ring Rational Field
1138 return self
._matrix
_space
1144 Return the unit element of this algebra.
1148 sage: from mjo.eja.eja_algebra import (HadamardEJA,
1153 We can compute unit element in the Hadamard EJA::
1155 sage: J = HadamardEJA(5)
1157 b0 + b1 + b2 + b3 + b4
1159 The unit element in the Hadamard EJA is inherited in the
1160 subalgebras generated by its elements::
1162 sage: J = HadamardEJA(5)
1164 b0 + b1 + b2 + b3 + b4
1165 sage: x = sum(J.gens())
1166 sage: A = x.subalgebra_generated_by(orthonormalize=False)
1169 sage: A.one().superalgebra_element()
1170 b0 + b1 + b2 + b3 + b4
1174 The identity element acts like the identity, regardless of
1175 whether or not we orthonormalize::
1177 sage: set_random_seed()
1178 sage: J = random_eja()
1179 sage: x = J.random_element()
1180 sage: J.one()*x == x and x*J.one() == x
1182 sage: A = x.subalgebra_generated_by()
1183 sage: y = A.random_element()
1184 sage: A.one()*y == y and y*A.one() == y
1189 sage: set_random_seed()
1190 sage: J = random_eja(field=QQ, orthonormalize=False)
1191 sage: x = J.random_element()
1192 sage: J.one()*x == x and x*J.one() == x
1194 sage: A = x.subalgebra_generated_by(orthonormalize=False)
1195 sage: y = A.random_element()
1196 sage: A.one()*y == y and y*A.one() == y
1199 The matrix of the unit element's operator is the identity,
1200 regardless of the base field and whether or not we
1203 sage: set_random_seed()
1204 sage: J = random_eja()
1205 sage: actual = J.one().operator().matrix()
1206 sage: expected = matrix.identity(J.base_ring(), J.dimension())
1207 sage: actual == expected
1209 sage: x = J.random_element()
1210 sage: A = x.subalgebra_generated_by()
1211 sage: actual = A.one().operator().matrix()
1212 sage: expected = matrix.identity(A.base_ring(), A.dimension())
1213 sage: actual == expected
1218 sage: set_random_seed()
1219 sage: J = random_eja(field=QQ, orthonormalize=False)
1220 sage: actual = J.one().operator().matrix()
1221 sage: expected = matrix.identity(J.base_ring(), J.dimension())
1222 sage: actual == expected
1224 sage: x = J.random_element()
1225 sage: A = x.subalgebra_generated_by(orthonormalize=False)
1226 sage: actual = A.one().operator().matrix()
1227 sage: expected = matrix.identity(A.base_ring(), A.dimension())
1228 sage: actual == expected
1231 Ensure that the cached unit element (often precomputed by
1232 hand) agrees with the computed one::
1234 sage: set_random_seed()
1235 sage: J = random_eja()
1236 sage: cached = J.one()
1237 sage: J.one.clear_cache()
1238 sage: J.one() == cached
1243 sage: set_random_seed()
1244 sage: J = random_eja(field=QQ, orthonormalize=False)
1245 sage: cached = J.one()
1246 sage: J.one.clear_cache()
1247 sage: J.one() == cached
1251 # We can brute-force compute the matrices of the operators
1252 # that correspond to the basis elements of this algebra.
1253 # If some linear combination of those basis elements is the
1254 # algebra identity, then the same linear combination of
1255 # their matrices has to be the identity matrix.
1257 # Of course, matrices aren't vectors in sage, so we have to
1258 # appeal to the "long vectors" isometry.
1259 oper_vecs
= [ _mat2vec(g
.operator().matrix()) for g
in self
.gens() ]
1261 # Now we use basic linear algebra to find the coefficients,
1262 # of the matrices-as-vectors-linear-combination, which should
1263 # work for the original algebra basis too.
1264 A
= matrix(self
.base_ring(), oper_vecs
)
1266 # We used the isometry on the left-hand side already, but we
1267 # still need to do it for the right-hand side. Recall that we
1268 # wanted something that summed to the identity matrix.
1269 b
= _mat2vec( matrix
.identity(self
.base_ring(), self
.dimension()) )
1271 # Now if there's an identity element in the algebra, this
1272 # should work. We solve on the left to avoid having to
1273 # transpose the matrix "A".
1274 return self
.from_vector(A
.solve_left(b
))
1277 def peirce_decomposition(self
, c
):
1279 The Peirce decomposition of this algebra relative to the
1282 In the future, this can be extended to a complete system of
1283 orthogonal idempotents.
1287 - ``c`` -- an idempotent of this algebra.
1291 A triple (J0, J5, J1) containing two subalgebras and one subspace
1294 - ``J0`` -- the algebra on the eigenspace of ``c.operator()``
1295 corresponding to the eigenvalue zero.
1297 - ``J5`` -- the eigenspace (NOT a subalgebra) of ``c.operator()``
1298 corresponding to the eigenvalue one-half.
1300 - ``J1`` -- the algebra on the eigenspace of ``c.operator()``
1301 corresponding to the eigenvalue one.
1303 These are the only possible eigenspaces for that operator, and this
1304 algebra is a direct sum of them. The spaces ``J0`` and ``J1`` are
1305 orthogonal, and are subalgebras of this algebra with the appropriate
1310 sage: from mjo.eja.eja_algebra import random_eja, RealSymmetricEJA
1314 The canonical example comes from the symmetric matrices, which
1315 decompose into diagonal and off-diagonal parts::
1317 sage: J = RealSymmetricEJA(3)
1318 sage: C = matrix(QQ, [ [1,0,0],
1322 sage: J0,J5,J1 = J.peirce_decomposition(c)
1324 Euclidean Jordan algebra of dimension 1...
1326 Vector space of degree 6 and dimension 2...
1328 Euclidean Jordan algebra of dimension 3...
1329 sage: J0.one().to_matrix()
1333 sage: orig_df = AA.options.display_format
1334 sage: AA.options.display_format = 'radical'
1335 sage: J.from_vector(J5.basis()[0]).to_matrix()
1339 sage: J.from_vector(J5.basis()[1]).to_matrix()
1343 sage: AA.options.display_format = orig_df
1344 sage: J1.one().to_matrix()
1351 Every algebra decomposes trivially with respect to its identity
1354 sage: set_random_seed()
1355 sage: J = random_eja()
1356 sage: J0,J5,J1 = J.peirce_decomposition(J.one())
1357 sage: J0.dimension() == 0 and J5.dimension() == 0
1359 sage: J1.superalgebra() == J and J1.dimension() == J.dimension()
1362 The decomposition is into eigenspaces, and its components are
1363 therefore necessarily orthogonal. Moreover, the identity
1364 elements in the two subalgebras are the projections onto their
1365 respective subspaces of the superalgebra's identity element::
1367 sage: set_random_seed()
1368 sage: J = random_eja()
1369 sage: x = J.random_element()
1370 sage: if not J.is_trivial():
1371 ....: while x.is_nilpotent():
1372 ....: x = J.random_element()
1373 sage: c = x.subalgebra_idempotent()
1374 sage: J0,J5,J1 = J.peirce_decomposition(c)
1376 sage: for (w,y,z) in zip(J0.basis(), J5.basis(), J1.basis()):
1377 ....: w = w.superalgebra_element()
1378 ....: y = J.from_vector(y)
1379 ....: z = z.superalgebra_element()
1380 ....: ipsum += w.inner_product(y).abs()
1381 ....: ipsum += w.inner_product(z).abs()
1382 ....: ipsum += y.inner_product(z).abs()
1385 sage: J1(c) == J1.one()
1387 sage: J0(J.one() - c) == J0.one()
1391 if not c
.is_idempotent():
1392 raise ValueError("element is not idempotent: %s" % c
)
1394 # Default these to what they should be if they turn out to be
1395 # trivial, because eigenspaces_left() won't return eigenvalues
1396 # corresponding to trivial spaces (e.g. it returns only the
1397 # eigenspace corresponding to lambda=1 if you take the
1398 # decomposition relative to the identity element).
1399 trivial
= self
.subalgebra(())
1400 J0
= trivial
# eigenvalue zero
1401 J5
= VectorSpace(self
.base_ring(), 0) # eigenvalue one-half
1402 J1
= trivial
# eigenvalue one
1404 for (eigval
, eigspace
) in c
.operator().matrix().right_eigenspaces():
1405 if eigval
== ~
(self
.base_ring()(2)):
1408 gens
= tuple( self
.from_vector(b
) for b
in eigspace
.basis() )
1409 subalg
= self
.subalgebra(gens
, check_axioms
=False)
1415 raise ValueError("unexpected eigenvalue: %s" % eigval
)
1420 def random_element(self
, thorough
=False):
1422 Return a random element of this algebra.
1424 Our algebra superclass method only returns a linear
1425 combination of at most two basis elements. We instead
1426 want the vector space "random element" method that
1427 returns a more diverse selection.
1431 - ``thorough`` -- (boolean; default False) whether or not we
1432 should generate irrational coefficients for the random
1433 element when our base ring is irrational; this slows the
1434 algebra operations to a crawl, but any truly random method
1438 # For a general base ring... maybe we can trust this to do the
1439 # right thing? Unlikely, but.
1440 V
= self
.vector_space()
1441 v
= V
.random_element()
1443 if self
.base_ring() is AA
:
1444 # The "random element" method of the algebraic reals is
1445 # stupid at the moment, and only returns integers between
1446 # -2 and 2, inclusive:
1448 # https://trac.sagemath.org/ticket/30875
1450 # Instead, we implement our own "random vector" method,
1451 # and then coerce that into the algebra. We use the vector
1452 # space degree here instead of the dimension because a
1453 # subalgebra could (for example) be spanned by only two
1454 # vectors, each with five coordinates. We need to
1455 # generate all five coordinates.
1457 v
*= QQbar
.random_element().real()
1459 v
*= QQ
.random_element()
1461 return self
.from_vector(V
.coordinate_vector(v
))
1463 def random_elements(self
, count
, thorough
=False):
1465 Return ``count`` random elements as a tuple.
1469 - ``thorough`` -- (boolean; default False) whether or not we
1470 should generate irrational coefficients for the random
1471 elements when our base ring is irrational; this slows the
1472 algebra operations to a crawl, but any truly random method
1477 sage: from mjo.eja.eja_algebra import JordanSpinEJA
1481 sage: J = JordanSpinEJA(3)
1482 sage: x,y,z = J.random_elements(3)
1483 sage: all( [ x in J, y in J, z in J ])
1485 sage: len( J.random_elements(10) ) == 10
1489 return tuple( self
.random_element(thorough
)
1490 for idx
in range(count
) )
1494 def _charpoly_coefficients(self
):
1496 The `r` polynomial coefficients of the "characteristic polynomial
1501 sage: from mjo.eja.eja_algebra import random_eja
1505 The theory shows that these are all homogeneous polynomials of
1508 sage: set_random_seed()
1509 sage: J = random_eja()
1510 sage: all(p.is_homogeneous() for p in J._charpoly_coefficients())
1514 n
= self
.dimension()
1515 R
= self
.coordinate_polynomial_ring()
1517 F
= R
.fraction_field()
1520 # From a result in my book, these are the entries of the
1521 # basis representation of L_x.
1522 return sum( vars[k
]*self
.monomial(k
).operator().matrix()[i
,j
]
1525 L_x
= matrix(F
, n
, n
, L_x_i_j
)
1528 if self
.rank
.is_in_cache():
1530 # There's no need to pad the system with redundant
1531 # columns if we *know* they'll be redundant.
1534 # Compute an extra power in case the rank is equal to
1535 # the dimension (otherwise, we would stop at x^(r-1)).
1536 x_powers
= [ (L_x
**k
)*self
.one().to_vector()
1537 for k
in range(n
+1) ]
1538 A
= matrix
.column(F
, x_powers
[:n
])
1539 AE
= A
.extended_echelon_form()
1546 # The theory says that only the first "r" coefficients are
1547 # nonzero, and they actually live in the original polynomial
1548 # ring and not the fraction field. We negate them because in
1549 # the actual characteristic polynomial, they get moved to the
1550 # other side where x^r lives. We don't bother to trim A_rref
1551 # down to a square matrix and solve the resulting system,
1552 # because the upper-left r-by-r portion of A_rref is
1553 # guaranteed to be the identity matrix, so e.g.
1555 # A_rref.solve_right(Y)
1557 # would just be returning Y.
1558 return (-E
*b
)[:r
].change_ring(R
)
1563 Return the rank of this EJA.
1565 This is a cached method because we know the rank a priori for
1566 all of the algebras we can construct. Thus we can avoid the
1567 expensive ``_charpoly_coefficients()`` call unless we truly
1568 need to compute the whole characteristic polynomial.
1572 sage: from mjo.eja.eja_algebra import (HadamardEJA,
1573 ....: JordanSpinEJA,
1574 ....: RealSymmetricEJA,
1575 ....: ComplexHermitianEJA,
1576 ....: QuaternionHermitianEJA,
1581 The rank of the Jordan spin algebra is always two::
1583 sage: JordanSpinEJA(2).rank()
1585 sage: JordanSpinEJA(3).rank()
1587 sage: JordanSpinEJA(4).rank()
1590 The rank of the `n`-by-`n` Hermitian real, complex, or
1591 quaternion matrices is `n`::
1593 sage: RealSymmetricEJA(4).rank()
1595 sage: ComplexHermitianEJA(3).rank()
1597 sage: QuaternionHermitianEJA(2).rank()
1602 Ensure that every EJA that we know how to construct has a
1603 positive integer rank, unless the algebra is trivial in
1604 which case its rank will be zero::
1606 sage: set_random_seed()
1607 sage: J = random_eja()
1611 sage: r > 0 or (r == 0 and J.is_trivial())
1614 Ensure that computing the rank actually works, since the ranks
1615 of all simple algebras are known and will be cached by default::
1617 sage: set_random_seed() # long time
1618 sage: J = random_eja() # long time
1619 sage: cached = J.rank() # long time
1620 sage: J.rank.clear_cache() # long time
1621 sage: J.rank() == cached # long time
1625 return len(self
._charpoly
_coefficients
())
1628 def subalgebra(self
, basis
, **kwargs
):
1630 Create a subalgebra of this algebra from the given basis.
1632 from mjo
.eja
.eja_subalgebra
import FiniteDimensionalEJASubalgebra
1633 return FiniteDimensionalEJASubalgebra(self
, basis
, **kwargs
)
1636 def vector_space(self
):
1638 Return the vector space that underlies this algebra.
1642 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1646 sage: J = RealSymmetricEJA(2)
1647 sage: J.vector_space()
1648 Vector space of dimension 3 over...
1651 return self
.zero().to_vector().parent().ambient_vector_space()
1655 class RationalBasisEJA(FiniteDimensionalEJA
):
1657 Algebras whose supplied basis elements have all rational entries.
1661 sage: from mjo.eja.eja_algebra import BilinearFormEJA
1665 The supplied basis is orthonormalized by default::
1667 sage: B = matrix(QQ, [[1, 0, 0], [0, 25, -32], [0, -32, 41]])
1668 sage: J = BilinearFormEJA(B)
1669 sage: J.matrix_basis()
1686 # Abuse the check_field parameter to check that the entries of
1687 # out basis (in ambient coordinates) are in the field QQ.
1688 # Use _all2list to get the vector coordinates of octonion
1689 # entries and not the octonions themselves (which are not
1691 if not all( all(b_i
in QQ
for b_i
in _all2list(b
))
1693 raise TypeError("basis not rational")
1695 super().__init
__(basis
,
1699 check_field
=check_field
,
1702 self
._rational
_algebra
= None
1704 # There's no point in constructing the extra algebra if this
1705 # one is already rational.
1707 # Note: the same Jordan and inner-products work here,
1708 # because they are necessarily defined with respect to
1709 # ambient coordinates and not any particular basis.
1710 self
._rational
_algebra
= FiniteDimensionalEJA(
1715 matrix_space
=self
.matrix_space(),
1716 associative
=self
.is_associative(),
1717 orthonormalize
=False,
1722 def _charpoly_coefficients(self
):
1726 sage: from mjo.eja.eja_algebra import (BilinearFormEJA,
1727 ....: JordanSpinEJA)
1731 The base ring of the resulting polynomial coefficients is what
1732 it should be, and not the rationals (unless the algebra was
1733 already over the rationals)::
1735 sage: J = JordanSpinEJA(3)
1736 sage: J._charpoly_coefficients()
1737 (X1^2 - X2^2 - X3^2, -2*X1)
1738 sage: a0 = J._charpoly_coefficients()[0]
1740 Algebraic Real Field
1741 sage: a0.base_ring()
1742 Algebraic Real Field
1745 if self
._rational
_algebra
is None:
1746 # There's no need to construct *another* algebra over the
1747 # rationals if this one is already over the
1748 # rationals. Likewise, if we never orthonormalized our
1749 # basis, we might as well just use the given one.
1750 return super()._charpoly
_coefficients
()
1752 # Do the computation over the rationals. The answer will be
1753 # the same, because all we've done is a change of basis.
1754 # Then, change back from QQ to our real base ring
1755 a
= ( a_i
.change_ring(self
.base_ring())
1756 for a_i
in self
._rational
_algebra
._charpoly
_coefficients
() )
1758 # Otherwise, convert the coordinate variables back to the
1759 # deorthonormalized ones.
1760 R
= self
.coordinate_polynomial_ring()
1761 from sage
.modules
.free_module_element
import vector
1762 X
= vector(R
, R
.gens())
1763 BX
= self
._deortho
_matrix
*X
1765 subs_dict
= { X[i]: BX[i] for i in range(len(X)) }
1766 return tuple( a_i
.subs(subs_dict
) for a_i
in a
)
1768 class ConcreteEJA(FiniteDimensionalEJA
):
1770 A class for the Euclidean Jordan algebras that we know by name.
1772 These are the Jordan algebras whose basis, multiplication table,
1773 rank, and so on are known a priori. More to the point, they are
1774 the Euclidean Jordan algebras for which we are able to conjure up
1775 a "random instance."
1779 sage: from mjo.eja.eja_algebra import ConcreteEJA
1783 Our basis is normalized with respect to the algebra's inner
1784 product, unless we specify otherwise::
1786 sage: set_random_seed()
1787 sage: J = ConcreteEJA.random_instance()
1788 sage: all( b.norm() == 1 for b in J.gens() )
1791 Since our basis is orthonormal with respect to the algebra's inner
1792 product, and since we know that this algebra is an EJA, any
1793 left-multiplication operator's matrix will be symmetric because
1794 natural->EJA basis representation is an isometry and within the
1795 EJA the operator is self-adjoint by the Jordan axiom::
1797 sage: set_random_seed()
1798 sage: J = ConcreteEJA.random_instance()
1799 sage: x = J.random_element()
1800 sage: x.operator().is_self_adjoint()
1805 def _max_random_instance_dimension():
1807 The maximum dimension of any random instance. Ten dimensions seems
1808 to be about the point where everything takes a turn for the
1809 worse. And dimension ten (but not nine) allows the 4-by-4 real
1810 Hermitian matrices, the 2-by-2 quaternion Hermitian matrices,
1811 and the 2-by-2 octonion Hermitian matrices.
1816 def _max_random_instance_size(max_dimension
):
1818 Return an integer "size" that is an upper bound on the size of
1819 this algebra when it is used in a random test case. This size
1820 (which can be passed to the algebra's constructor) is itself
1821 based on the ``max_dimension`` parameter.
1823 This method must be implemented in each subclass.
1825 raise NotImplementedError
1828 def random_instance(cls
, max_dimension
=None, *args
, **kwargs
):
1830 Return a random instance of this type of algebra whose dimension
1831 is less than or equal to ``max_dimension``. If the dimension bound
1832 is omitted, then the ``_max_random_instance_dimension()`` is used
1833 to get a suitable bound.
1835 This method should be implemented in each subclass.
1837 from sage
.misc
.prandom
import choice
1838 eja_class
= choice(cls
.__subclasses
__())
1840 # These all bubble up to the RationalBasisEJA superclass
1841 # constructor, so any (kw)args valid there are also valid
1843 return eja_class
.random_instance(*args
, **kwargs
)
1846 class MatrixEJA(FiniteDimensionalEJA
):
1848 def _denormalized_basis(A
):
1850 Returns a basis for the space of complex Hermitian n-by-n matrices.
1852 Why do we embed these? Basically, because all of numerical linear
1853 algebra assumes that you're working with vectors consisting of `n`
1854 entries from a field and scalars from the same field. There's no way
1855 to tell SageMath that (for example) the vectors contain complex
1856 numbers, while the scalar field is real.
1860 sage: from mjo.hurwitz import (ComplexMatrixAlgebra,
1861 ....: QuaternionMatrixAlgebra,
1862 ....: OctonionMatrixAlgebra)
1863 sage: from mjo.eja.eja_algebra import MatrixEJA
1867 sage: set_random_seed()
1868 sage: n = ZZ.random_element(1,5)
1869 sage: A = MatrixSpace(QQ, n)
1870 sage: B = MatrixEJA._denormalized_basis(A)
1871 sage: all( M.is_hermitian() for M in B)
1876 sage: set_random_seed()
1877 sage: n = ZZ.random_element(1,5)
1878 sage: A = ComplexMatrixAlgebra(n, scalars=QQ)
1879 sage: B = MatrixEJA._denormalized_basis(A)
1880 sage: all( M.is_hermitian() for M in B)
1885 sage: set_random_seed()
1886 sage: n = ZZ.random_element(1,5)
1887 sage: A = QuaternionMatrixAlgebra(n, scalars=QQ)
1888 sage: B = MatrixEJA._denormalized_basis(A)
1889 sage: all( M.is_hermitian() for M in B )
1894 sage: set_random_seed()
1895 sage: n = ZZ.random_element(1,5)
1896 sage: A = OctonionMatrixAlgebra(n, scalars=QQ)
1897 sage: B = MatrixEJA._denormalized_basis(A)
1898 sage: all( M.is_hermitian() for M in B )
1902 # These work for real MatrixSpace, whose monomials only have
1903 # two coordinates (because the last one would always be "1").
1904 es
= A
.base_ring().gens()
1905 gen
= lambda A
,m
: A
.monomial(m
[:2])
1907 if hasattr(A
, 'entry_algebra_gens'):
1908 # We've got a MatrixAlgebra, and its monomials will have
1909 # three coordinates.
1910 es
= A
.entry_algebra_gens()
1911 gen
= lambda A
,m
: A
.monomial(m
)
1914 for i
in range(A
.nrows()):
1915 for j
in range(i
+1):
1917 E_ii
= gen(A
, (i
,j
,es
[0]))
1921 E_ij
= gen(A
, (i
,j
,e
))
1922 E_ij
+= E_ij
.conjugate_transpose()
1925 return tuple( basis
)
1928 def jordan_product(X
,Y
):
1929 return (X
*Y
+ Y
*X
)/2
1932 def trace_inner_product(X
,Y
):
1934 A trace inner-product for matrices that aren't embedded in the
1935 reals. It takes MATRICES as arguments, not EJA elements.
1939 sage: from mjo.eja.eja_algebra import (RealSymmetricEJA,
1940 ....: ComplexHermitianEJA,
1941 ....: QuaternionHermitianEJA,
1942 ....: OctonionHermitianEJA)
1946 sage: J = RealSymmetricEJA(2,field=QQ,orthonormalize=False)
1947 sage: I = J.one().to_matrix()
1948 sage: J.trace_inner_product(I, -I)
1953 sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
1954 sage: I = J.one().to_matrix()
1955 sage: J.trace_inner_product(I, -I)
1960 sage: J = QuaternionHermitianEJA(2,field=QQ,orthonormalize=False)
1961 sage: I = J.one().to_matrix()
1962 sage: J.trace_inner_product(I, -I)
1967 sage: J = OctonionHermitianEJA(2,field=QQ,orthonormalize=False)
1968 sage: I = J.one().to_matrix()
1969 sage: J.trace_inner_product(I, -I)
1974 if hasattr(tr
, 'coefficient'):
1975 # Works for octonions, and has to come first because they
1976 # also have a "real()" method that doesn't return an
1977 # element of the scalar ring.
1978 return tr
.coefficient(0)
1979 elif hasattr(tr
, 'coefficient_tuple'):
1980 # Works for quaternions.
1981 return tr
.coefficient_tuple()[0]
1983 # Works for real and complex numbers.
1987 def __init__(self
, matrix_space
, **kwargs
):
1988 # We know this is a valid EJA, but will double-check
1989 # if the user passes check_axioms=True.
1990 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
1993 super().__init
__(self
._denormalized
_basis
(matrix_space
),
1994 self
.jordan_product
,
1995 self
.trace_inner_product
,
1996 field
=matrix_space
.base_ring(),
1997 matrix_space
=matrix_space
,
2000 self
.rank
.set_cache(matrix_space
.nrows())
2001 self
.one
.set_cache( self(matrix_space
.one()) )
2003 class RealSymmetricEJA(MatrixEJA
, RationalBasisEJA
, ConcreteEJA
):
2005 The rank-n simple EJA consisting of real symmetric n-by-n
2006 matrices, the usual symmetric Jordan product, and the trace inner
2007 product. It has dimension `(n^2 + n)/2` over the reals.
2011 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
2015 sage: J = RealSymmetricEJA(2)
2016 sage: b0, b1, b2 = J.gens()
2024 In theory, our "field" can be any subfield of the reals::
2026 sage: RealSymmetricEJA(2, field=RDF, check_axioms=True)
2027 Euclidean Jordan algebra of dimension 3 over Real Double Field
2028 sage: RealSymmetricEJA(2, field=RR, check_axioms=True)
2029 Euclidean Jordan algebra of dimension 3 over Real Field with
2030 53 bits of precision
2034 The dimension of this algebra is `(n^2 + n) / 2`::
2036 sage: set_random_seed()
2037 sage: d = RealSymmetricEJA._max_random_instance_dimension()
2038 sage: n = RealSymmetricEJA._max_random_instance_size(d)
2039 sage: J = RealSymmetricEJA(n)
2040 sage: J.dimension() == (n^2 + n)/2
2043 The Jordan multiplication is what we think it is::
2045 sage: set_random_seed()
2046 sage: J = RealSymmetricEJA.random_instance()
2047 sage: x,y = J.random_elements(2)
2048 sage: actual = (x*y).to_matrix()
2049 sage: X = x.to_matrix()
2050 sage: Y = y.to_matrix()
2051 sage: expected = (X*Y + Y*X)/2
2052 sage: actual == expected
2054 sage: J(expected) == x*y
2057 We can change the generator prefix::
2059 sage: RealSymmetricEJA(3, prefix='q').gens()
2060 (q0, q1, q2, q3, q4, q5)
2062 We can construct the (trivial) algebra of rank zero::
2064 sage: RealSymmetricEJA(0)
2065 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2069 def _max_random_instance_size(max_dimension
):
2070 # Obtained by solving d = (n^2 + n)/2.
2071 # The ZZ-int-ZZ thing is just "floor."
2072 return ZZ(int(ZZ(8*max_dimension
+ 1).sqrt()/2 - 1/2))
2075 def random_instance(cls
, max_dimension
=None, **kwargs
):
2077 Return a random instance of this type of algebra.
2079 if max_dimension
is None:
2080 max_dimension
= cls
._max
_random
_instance
_dimension
()
2081 max_size
= cls
._max
_random
_instance
_size
(max_dimension
) + 1
2082 n
= ZZ
.random_element(max_size
)
2083 return cls(n
, **kwargs
)
2085 def __init__(self
, n
, field
=AA
, **kwargs
):
2086 # We know this is a valid EJA, but will double-check
2087 # if the user passes check_axioms=True.
2088 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2090 A
= MatrixSpace(field
, n
)
2091 super().__init
__(A
, **kwargs
)
2093 from mjo
.eja
.eja_cache
import real_symmetric_eja_coeffs
2094 a
= real_symmetric_eja_coeffs(self
)
2096 if self
._rational
_algebra
is None:
2097 self
._charpoly
_coefficients
.set_cache(a
)
2099 self
._rational
_algebra
._charpoly
_coefficients
.set_cache(a
)
2103 class ComplexHermitianEJA(MatrixEJA
, RationalBasisEJA
, ConcreteEJA
):
2105 The rank-n simple EJA consisting of complex Hermitian n-by-n
2106 matrices over the real numbers, the usual symmetric Jordan product,
2107 and the real-part-of-trace inner product. It has dimension `n^2` over
2112 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
2116 In theory, our "field" can be any subfield of the reals, but we
2117 can't use inexact real fields at the moment because SageMath
2118 doesn't know how to convert their elements into complex numbers,
2119 or even into algebraic reals::
2122 Traceback (most recent call last):
2124 TypeError: Illegal initializer for algebraic number
2126 Traceback (most recent call last):
2128 TypeError: Illegal initializer for algebraic number
2130 This causes the following error when we try to scale a matrix of
2131 complex numbers by an inexact real number::
2133 sage: ComplexHermitianEJA(2,field=RR)
2134 Traceback (most recent call last):
2136 TypeError: Unable to coerce entries (=(1.00000000000000,
2137 -0.000000000000000)) to coefficients in Algebraic Real Field
2141 The dimension of this algebra is `n^2`::
2143 sage: set_random_seed()
2144 sage: d = ComplexHermitianEJA._max_random_instance_dimension()
2145 sage: n = ComplexHermitianEJA._max_random_instance_size(d)
2146 sage: J = ComplexHermitianEJA(n)
2147 sage: J.dimension() == n^2
2150 The Jordan multiplication is what we think it is::
2152 sage: set_random_seed()
2153 sage: J = ComplexHermitianEJA.random_instance()
2154 sage: x,y = J.random_elements(2)
2155 sage: actual = (x*y).to_matrix()
2156 sage: X = x.to_matrix()
2157 sage: Y = y.to_matrix()
2158 sage: expected = (X*Y + Y*X)/2
2159 sage: actual == expected
2161 sage: J(expected) == x*y
2164 We can change the generator prefix::
2166 sage: ComplexHermitianEJA(2, prefix='z').gens()
2169 We can construct the (trivial) algebra of rank zero::
2171 sage: ComplexHermitianEJA(0)
2172 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2175 def __init__(self
, n
, field
=AA
, **kwargs
):
2176 # We know this is a valid EJA, but will double-check
2177 # if the user passes check_axioms=True.
2178 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2180 from mjo
.hurwitz
import ComplexMatrixAlgebra
2181 A
= ComplexMatrixAlgebra(n
, scalars
=field
)
2182 super().__init
__(A
, **kwargs
)
2184 from mjo
.eja
.eja_cache
import complex_hermitian_eja_coeffs
2185 a
= complex_hermitian_eja_coeffs(self
)
2187 if self
._rational
_algebra
is None:
2188 self
._charpoly
_coefficients
.set_cache(a
)
2190 self
._rational
_algebra
._charpoly
_coefficients
.set_cache(a
)
2193 def _max_random_instance_size(max_dimension
):
2194 # Obtained by solving d = n^2.
2195 # The ZZ-int-ZZ thing is just "floor."
2196 return ZZ(int(ZZ(max_dimension
).sqrt()))
2199 def random_instance(cls
, max_dimension
=None, **kwargs
):
2201 Return a random instance of this type of algebra.
2203 if max_dimension
is None:
2204 max_dimension
= cls
._max
_random
_instance
_dimension
()
2205 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
(max_dimension
) + 1)
2206 return cls(n
, **kwargs
)
2209 class QuaternionHermitianEJA(MatrixEJA
, RationalBasisEJA
, ConcreteEJA
):
2211 The rank-n simple EJA consisting of self-adjoint n-by-n quaternion
2212 matrices, the usual symmetric Jordan product, and the
2213 real-part-of-trace inner product. It has dimension `2n^2 - n` over
2218 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
2222 In theory, our "field" can be any subfield of the reals::
2224 sage: QuaternionHermitianEJA(2, field=RDF, check_axioms=True)
2225 Euclidean Jordan algebra of dimension 6 over Real Double Field
2226 sage: QuaternionHermitianEJA(2, field=RR, check_axioms=True)
2227 Euclidean Jordan algebra of dimension 6 over Real Field with
2228 53 bits of precision
2232 The dimension of this algebra is `2*n^2 - n`::
2234 sage: set_random_seed()
2235 sage: d = QuaternionHermitianEJA._max_random_instance_dimension()
2236 sage: n = QuaternionHermitianEJA._max_random_instance_size(d)
2237 sage: J = QuaternionHermitianEJA(n)
2238 sage: J.dimension() == 2*(n^2) - n
2241 The Jordan multiplication is what we think it is::
2243 sage: set_random_seed()
2244 sage: J = QuaternionHermitianEJA.random_instance()
2245 sage: x,y = J.random_elements(2)
2246 sage: actual = (x*y).to_matrix()
2247 sage: X = x.to_matrix()
2248 sage: Y = y.to_matrix()
2249 sage: expected = (X*Y + Y*X)/2
2250 sage: actual == expected
2252 sage: J(expected) == x*y
2255 We can change the generator prefix::
2257 sage: QuaternionHermitianEJA(2, prefix='a').gens()
2258 (a0, a1, a2, a3, a4, a5)
2260 We can construct the (trivial) algebra of rank zero::
2262 sage: QuaternionHermitianEJA(0)
2263 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2266 def __init__(self
, n
, field
=AA
, **kwargs
):
2267 # We know this is a valid EJA, but will double-check
2268 # if the user passes check_axioms=True.
2269 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2271 from mjo
.hurwitz
import QuaternionMatrixAlgebra
2272 A
= QuaternionMatrixAlgebra(n
, scalars
=field
)
2273 super().__init
__(A
, **kwargs
)
2275 from mjo
.eja
.eja_cache
import quaternion_hermitian_eja_coeffs
2276 a
= quaternion_hermitian_eja_coeffs(self
)
2278 if self
._rational
_algebra
is None:
2279 self
._charpoly
_coefficients
.set_cache(a
)
2281 self
._rational
_algebra
._charpoly
_coefficients
.set_cache(a
)
2286 def _max_random_instance_size(max_dimension
):
2288 The maximum rank of a random QuaternionHermitianEJA.
2290 # Obtained by solving d = 2n^2 - n.
2291 # The ZZ-int-ZZ thing is just "floor."
2292 return ZZ(int(ZZ(8*max_dimension
+ 1).sqrt()/4 + 1/4))
2295 def random_instance(cls
, max_dimension
=None, **kwargs
):
2297 Return a random instance of this type of algebra.
2299 if max_dimension
is None:
2300 max_dimension
= cls
._max
_random
_instance
_dimension
()
2301 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
(max_dimension
) + 1)
2302 return cls(n
, **kwargs
)
2304 class OctonionHermitianEJA(MatrixEJA
, RationalBasisEJA
, ConcreteEJA
):
2308 sage: from mjo.eja.eja_algebra import (FiniteDimensionalEJA,
2309 ....: OctonionHermitianEJA)
2310 sage: from mjo.hurwitz import Octonions, OctonionMatrixAlgebra
2314 The 3-by-3 algebra satisfies the axioms of an EJA::
2316 sage: OctonionHermitianEJA(3, # long time
2317 ....: field=QQ, # long time
2318 ....: orthonormalize=False, # long time
2319 ....: check_axioms=True) # long time
2320 Euclidean Jordan algebra of dimension 27 over Rational Field
2322 After a change-of-basis, the 2-by-2 algebra has the same
2323 multiplication table as the ten-dimensional Jordan spin algebra::
2325 sage: A = OctonionMatrixAlgebra(2,Octonions(QQ),QQ)
2326 sage: b = OctonionHermitianEJA._denormalized_basis(A)
2327 sage: basis = (b[0] + b[9],) + b[1:9] + (b[0] - b[9],)
2328 sage: jp = OctonionHermitianEJA.jordan_product
2329 sage: ip = OctonionHermitianEJA.trace_inner_product
2330 sage: J = FiniteDimensionalEJA(basis,
2334 ....: orthonormalize=False)
2335 sage: J.multiplication_table()
2336 +----++----+----+----+----+----+----+----+----+----+----+
2337 | * || b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7 | b8 | b9 |
2338 +====++====+====+====+====+====+====+====+====+====+====+
2339 | b0 || b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7 | b8 | b9 |
2340 +----++----+----+----+----+----+----+----+----+----+----+
2341 | b1 || b1 | b0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2342 +----++----+----+----+----+----+----+----+----+----+----+
2343 | b2 || b2 | 0 | b0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2344 +----++----+----+----+----+----+----+----+----+----+----+
2345 | b3 || b3 | 0 | 0 | b0 | 0 | 0 | 0 | 0 | 0 | 0 |
2346 +----++----+----+----+----+----+----+----+----+----+----+
2347 | b4 || b4 | 0 | 0 | 0 | b0 | 0 | 0 | 0 | 0 | 0 |
2348 +----++----+----+----+----+----+----+----+----+----+----+
2349 | b5 || b5 | 0 | 0 | 0 | 0 | b0 | 0 | 0 | 0 | 0 |
2350 +----++----+----+----+----+----+----+----+----+----+----+
2351 | b6 || b6 | 0 | 0 | 0 | 0 | 0 | b0 | 0 | 0 | 0 |
2352 +----++----+----+----+----+----+----+----+----+----+----+
2353 | b7 || b7 | 0 | 0 | 0 | 0 | 0 | 0 | b0 | 0 | 0 |
2354 +----++----+----+----+----+----+----+----+----+----+----+
2355 | b8 || b8 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | b0 | 0 |
2356 +----++----+----+----+----+----+----+----+----+----+----+
2357 | b9 || b9 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | b0 |
2358 +----++----+----+----+----+----+----+----+----+----+----+
2362 We can actually construct the 27-dimensional Albert algebra,
2363 and we get the right unit element if we recompute it::
2365 sage: J = OctonionHermitianEJA(3, # long time
2366 ....: field=QQ, # long time
2367 ....: orthonormalize=False) # long time
2368 sage: J.one.clear_cache() # long time
2369 sage: J.one() # long time
2371 sage: J.one().to_matrix() # long time
2380 The 2-by-2 algebra is isomorphic to the ten-dimensional Jordan
2381 spin algebra, but just to be sure, we recompute its rank::
2383 sage: J = OctonionHermitianEJA(2, # long time
2384 ....: field=QQ, # long time
2385 ....: orthonormalize=False) # long time
2386 sage: J.rank.clear_cache() # long time
2387 sage: J.rank() # long time
2392 def _max_random_instance_size(max_dimension
):
2394 The maximum rank of a random QuaternionHermitianEJA.
2396 # There's certainly a formula for this, but with only four
2397 # cases to worry about, I'm not that motivated to derive it.
2398 if max_dimension
>= 27:
2400 elif max_dimension
>= 10:
2402 elif max_dimension
>= 1:
2408 def random_instance(cls
, max_dimension
=None, **kwargs
):
2410 Return a random instance of this type of algebra.
2412 if max_dimension
is None:
2413 max_dimension
= cls
._max
_random
_instance
_dimension
()
2414 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
(max_dimension
) + 1)
2415 return cls(n
, **kwargs
)
2417 def __init__(self
, n
, field
=AA
, **kwargs
):
2419 # Otherwise we don't get an EJA.
2420 raise ValueError("n cannot exceed 3")
2422 # We know this is a valid EJA, but will double-check
2423 # if the user passes check_axioms=True.
2424 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2426 from mjo
.hurwitz
import OctonionMatrixAlgebra
2427 A
= OctonionMatrixAlgebra(n
, scalars
=field
)
2428 super().__init
__(A
, **kwargs
)
2430 from mjo
.eja
.eja_cache
import octonion_hermitian_eja_coeffs
2431 a
= octonion_hermitian_eja_coeffs(self
)
2433 if self
._rational
_algebra
is None:
2434 self
._charpoly
_coefficients
.set_cache(a
)
2436 self
._rational
_algebra
._charpoly
_coefficients
.set_cache(a
)
2439 class AlbertEJA(OctonionHermitianEJA
):
2441 The Albert algebra is the algebra of three-by-three Hermitian
2442 matrices whose entries are octonions.
2446 sage: from mjo.eja.eja_algebra import AlbertEJA
2450 sage: AlbertEJA(field=QQ, orthonormalize=False)
2451 Euclidean Jordan algebra of dimension 27 over Rational Field
2452 sage: AlbertEJA() # long time
2453 Euclidean Jordan algebra of dimension 27 over Algebraic Real Field
2456 def __init__(self
, *args
, **kwargs
):
2457 super().__init
__(3, *args
, **kwargs
)
2460 class HadamardEJA(RationalBasisEJA
, ConcreteEJA
):
2462 Return the Euclidean Jordan algebra on `R^n` with the Hadamard
2463 (pointwise real-number multiplication) Jordan product and the
2464 usual inner-product.
2466 This is nothing more than the Cartesian product of ``n`` copies of
2467 the one-dimensional Jordan spin algebra, and is the most common
2468 example of a non-simple Euclidean Jordan algebra.
2472 sage: from mjo.eja.eja_algebra import HadamardEJA
2476 This multiplication table can be verified by hand::
2478 sage: J = HadamardEJA(3)
2479 sage: b0,b1,b2 = J.gens()
2495 We can change the generator prefix::
2497 sage: HadamardEJA(3, prefix='r').gens()
2500 def __init__(self
, n
, field
=AA
, **kwargs
):
2501 MS
= MatrixSpace(field
, n
, 1)
2504 jordan_product
= lambda x
,y
: x
2505 inner_product
= lambda x
,y
: x
2507 def jordan_product(x
,y
):
2508 return MS( xi
*yi
for (xi
,yi
) in zip(x
,y
) )
2510 def inner_product(x
,y
):
2513 # New defaults for keyword arguments. Don't orthonormalize
2514 # because our basis is already orthonormal with respect to our
2515 # inner-product. Don't check the axioms, because we know this
2516 # is a valid EJA... but do double-check if the user passes
2517 # check_axioms=True. Note: we DON'T override the "check_field"
2518 # default here, because the user can pass in a field!
2519 if "orthonormalize" not in kwargs
: kwargs
["orthonormalize"] = False
2520 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2522 column_basis
= tuple( MS(b
) for b
in FreeModule(field
, n
).basis() )
2523 super().__init
__(column_basis
,
2530 self
.rank
.set_cache(n
)
2532 self
.one
.set_cache( self
.sum(self
.gens()) )
2535 def _max_random_instance_dimension():
2537 There's no reason to go higher than five here. That's
2538 enough to get the point across.
2543 def _max_random_instance_size(max_dimension
):
2545 The maximum size (=dimension) of a random HadamardEJA.
2547 return max_dimension
2550 def random_instance(cls
, max_dimension
=None, **kwargs
):
2552 Return a random instance of this type of algebra.
2554 if max_dimension
is None:
2555 max_dimension
= cls
._max
_random
_instance
_dimension
()
2556 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
(max_dimension
) + 1)
2557 return cls(n
, **kwargs
)
2560 class BilinearFormEJA(RationalBasisEJA
, ConcreteEJA
):
2562 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
2563 with the half-trace inner product and jordan product ``x*y =
2564 (<Bx,y>,y_bar>, x0*y_bar + y0*x_bar)`` where `B = 1 \times B22` is
2565 a symmetric positive-definite "bilinear form" matrix. Its
2566 dimension is the size of `B`, and it has rank two in dimensions
2567 larger than two. It reduces to the ``JordanSpinEJA`` when `B` is
2568 the identity matrix of order ``n``.
2570 We insist that the one-by-one upper-left identity block of `B` be
2571 passed in as well so that we can be passed a matrix of size zero
2572 to construct a trivial algebra.
2576 sage: from mjo.eja.eja_algebra import (BilinearFormEJA,
2577 ....: JordanSpinEJA)
2581 When no bilinear form is specified, the identity matrix is used,
2582 and the resulting algebra is the Jordan spin algebra::
2584 sage: B = matrix.identity(AA,3)
2585 sage: J0 = BilinearFormEJA(B)
2586 sage: J1 = JordanSpinEJA(3)
2587 sage: J0.multiplication_table() == J0.multiplication_table()
2590 An error is raised if the matrix `B` does not correspond to a
2591 positive-definite bilinear form::
2593 sage: B = matrix.random(QQ,2,3)
2594 sage: J = BilinearFormEJA(B)
2595 Traceback (most recent call last):
2597 ValueError: bilinear form is not positive-definite
2598 sage: B = matrix.zero(QQ,3)
2599 sage: J = BilinearFormEJA(B)
2600 Traceback (most recent call last):
2602 ValueError: bilinear form is not positive-definite
2606 We can create a zero-dimensional algebra::
2608 sage: B = matrix.identity(AA,0)
2609 sage: J = BilinearFormEJA(B)
2613 We can check the multiplication condition given in the Jordan, von
2614 Neumann, and Wigner paper (and also discussed on my "On the
2615 symmetry..." paper). Note that this relies heavily on the standard
2616 choice of basis, as does anything utilizing the bilinear form
2617 matrix. We opt not to orthonormalize the basis, because if we
2618 did, we would have to normalize the `s_{i}` in a similar manner::
2620 sage: set_random_seed()
2621 sage: n = ZZ.random_element(5)
2622 sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular')
2623 sage: B11 = matrix.identity(QQ,1)
2624 sage: B22 = M.transpose()*M
2625 sage: B = block_matrix(2,2,[ [B11,0 ],
2627 sage: J = BilinearFormEJA(B, orthonormalize=False)
2628 sage: eis = VectorSpace(M.base_ring(), M.ncols()).basis()
2629 sage: V = J.vector_space()
2630 sage: sis = [ J( V([0] + (M.inverse()*ei).list()).column() )
2631 ....: for ei in eis ]
2632 sage: actual = [ sis[i]*sis[j]
2633 ....: for i in range(n-1)
2634 ....: for j in range(n-1) ]
2635 sage: expected = [ J.one() if i == j else J.zero()
2636 ....: for i in range(n-1)
2637 ....: for j in range(n-1) ]
2638 sage: actual == expected
2642 def __init__(self
, B
, field
=AA
, **kwargs
):
2643 # The matrix "B" is supplied by the user in most cases,
2644 # so it makes sense to check whether or not its positive-
2645 # definite unless we are specifically asked not to...
2646 if ("check_axioms" not in kwargs
) or kwargs
["check_axioms"]:
2647 if not B
.is_positive_definite():
2648 raise ValueError("bilinear form is not positive-definite")
2650 # However, all of the other data for this EJA is computed
2651 # by us in manner that guarantees the axioms are
2652 # satisfied. So, again, unless we are specifically asked to
2653 # verify things, we'll skip the rest of the checks.
2654 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2657 MS
= MatrixSpace(field
, n
, 1)
2659 def inner_product(x
,y
):
2660 return (y
.T
*B
*x
)[0,0]
2662 def jordan_product(x
,y
):
2667 z0
= inner_product(y
,x
)
2668 zbar
= y0
*xbar
+ x0
*ybar
2669 return MS([z0
] + zbar
.list())
2671 column_basis
= tuple( MS(b
) for b
in FreeModule(field
, n
).basis() )
2673 # TODO: I haven't actually checked this, but it seems legit.
2678 super().__init
__(column_basis
,
2683 associative
=associative
,
2686 # The rank of this algebra is two, unless we're in a
2687 # one-dimensional ambient space (because the rank is bounded
2688 # by the ambient dimension).
2689 self
.rank
.set_cache(min(n
,2))
2691 self
.one
.set_cache( self
.zero() )
2693 self
.one
.set_cache( self
.monomial(0) )
2696 def _max_random_instance_dimension():
2698 There's no reason to go higher than five here. That's
2699 enough to get the point across.
2704 def _max_random_instance_size(max_dimension
):
2706 The maximum size (=dimension) of a random BilinearFormEJA.
2708 return max_dimension
2711 def random_instance(cls
, max_dimension
=None, **kwargs
):
2713 Return a random instance of this algebra.
2715 if max_dimension
is None:
2716 max_dimension
= cls
._max
_random
_instance
_dimension
()
2717 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
(max_dimension
) + 1)
2719 B
= matrix
.identity(ZZ
, n
)
2720 return cls(B
, **kwargs
)
2722 B11
= matrix
.identity(ZZ
, 1)
2723 M
= matrix
.random(ZZ
, n
-1)
2724 I
= matrix
.identity(ZZ
, n
-1)
2726 while alpha
.is_zero():
2727 alpha
= ZZ
.random_element().abs()
2728 B22
= M
.transpose()*M
+ alpha
*I
2730 from sage
.matrix
.special
import block_matrix
2731 B
= block_matrix(2,2, [ [B11
, ZZ(0) ],
2734 return cls(B
, **kwargs
)
2737 class JordanSpinEJA(BilinearFormEJA
):
2739 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
2740 with the usual inner product and jordan product ``x*y =
2741 (<x,y>, x0*y_bar + y0*x_bar)``. It has dimension `n` over
2746 sage: from mjo.eja.eja_algebra import JordanSpinEJA
2750 This multiplication table can be verified by hand::
2752 sage: J = JordanSpinEJA(4)
2753 sage: b0,b1,b2,b3 = J.gens()
2769 We can change the generator prefix::
2771 sage: JordanSpinEJA(2, prefix='B').gens()
2776 Ensure that we have the usual inner product on `R^n`::
2778 sage: set_random_seed()
2779 sage: J = JordanSpinEJA.random_instance()
2780 sage: x,y = J.random_elements(2)
2781 sage: actual = x.inner_product(y)
2782 sage: expected = x.to_vector().inner_product(y.to_vector())
2783 sage: actual == expected
2787 def __init__(self
, n
, *args
, **kwargs
):
2788 # This is a special case of the BilinearFormEJA with the
2789 # identity matrix as its bilinear form.
2790 B
= matrix
.identity(ZZ
, n
)
2792 # Don't orthonormalize because our basis is already
2793 # orthonormal with respect to our inner-product.
2794 if "orthonormalize" not in kwargs
: kwargs
["orthonormalize"] = False
2796 # But also don't pass check_field=False here, because the user
2797 # can pass in a field!
2798 super().__init
__(B
, *args
, **kwargs
)
2801 def random_instance(cls
, max_dimension
=None, **kwargs
):
2803 Return a random instance of this type of algebra.
2805 Needed here to override the implementation for ``BilinearFormEJA``.
2807 if max_dimension
is None:
2808 max_dimension
= cls
._max
_random
_instance
_dimension
()
2809 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
(max_dimension
) + 1)
2810 return cls(n
, **kwargs
)
2813 class TrivialEJA(RationalBasisEJA
, ConcreteEJA
):
2815 The trivial Euclidean Jordan algebra consisting of only a zero element.
2819 sage: from mjo.eja.eja_algebra import TrivialEJA
2823 sage: J = TrivialEJA()
2830 sage: 7*J.one()*12*J.one()
2832 sage: J.one().inner_product(J.one())
2834 sage: J.one().norm()
2836 sage: J.one().subalgebra_generated_by()
2837 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2842 def __init__(self
, field
=AA
, **kwargs
):
2843 jordan_product
= lambda x
,y
: x
2844 inner_product
= lambda x
,y
: field
.zero()
2846 MS
= MatrixSpace(field
,0)
2848 # New defaults for keyword arguments
2849 if "orthonormalize" not in kwargs
: kwargs
["orthonormalize"] = False
2850 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2852 super().__init
__(basis
,
2860 # The rank is zero using my definition, namely the dimension of the
2861 # largest subalgebra generated by any element.
2862 self
.rank
.set_cache(0)
2863 self
.one
.set_cache( self
.zero() )
2866 def random_instance(cls
, **kwargs
):
2867 # We don't take a "size" argument so the superclass method is
2868 # inappropriate for us.
2869 return cls(**kwargs
)
2872 class CartesianProductEJA(FiniteDimensionalEJA
):
2874 The external (orthogonal) direct sum of two or more Euclidean
2875 Jordan algebras. Every Euclidean Jordan algebra decomposes into an
2876 orthogonal direct sum of simple Euclidean Jordan algebras which is
2877 then isometric to a Cartesian product, so no generality is lost by
2878 providing only this construction.
2882 sage: from mjo.eja.eja_algebra import (random_eja,
2883 ....: CartesianProductEJA,
2885 ....: JordanSpinEJA,
2886 ....: RealSymmetricEJA)
2890 The Jordan product is inherited from our factors and implemented by
2891 our CombinatorialFreeModule Cartesian product superclass::
2893 sage: set_random_seed()
2894 sage: J1 = HadamardEJA(2)
2895 sage: J2 = RealSymmetricEJA(2)
2896 sage: J = cartesian_product([J1,J2])
2897 sage: x,y = J.random_elements(2)
2901 The ability to retrieve the original factors is implemented by our
2902 CombinatorialFreeModule Cartesian product superclass::
2904 sage: J1 = HadamardEJA(2, field=QQ)
2905 sage: J2 = JordanSpinEJA(3, field=QQ)
2906 sage: J = cartesian_product([J1,J2])
2907 sage: J.cartesian_factors()
2908 (Euclidean Jordan algebra of dimension 2 over Rational Field,
2909 Euclidean Jordan algebra of dimension 3 over Rational Field)
2911 You can provide more than two factors::
2913 sage: J1 = HadamardEJA(2)
2914 sage: J2 = JordanSpinEJA(3)
2915 sage: J3 = RealSymmetricEJA(3)
2916 sage: cartesian_product([J1,J2,J3])
2917 Euclidean Jordan algebra of dimension 2 over Algebraic Real
2918 Field (+) Euclidean Jordan algebra of dimension 3 over Algebraic
2919 Real Field (+) Euclidean Jordan algebra of dimension 6 over
2920 Algebraic Real Field
2922 Rank is additive on a Cartesian product::
2924 sage: J1 = HadamardEJA(1)
2925 sage: J2 = RealSymmetricEJA(2)
2926 sage: J = cartesian_product([J1,J2])
2927 sage: J1.rank.clear_cache()
2928 sage: J2.rank.clear_cache()
2929 sage: J.rank.clear_cache()
2932 sage: J.rank() == J1.rank() + J2.rank()
2935 The same rank computation works over the rationals, with whatever
2938 sage: J1 = HadamardEJA(1, field=QQ, orthonormalize=False)
2939 sage: J2 = RealSymmetricEJA(2, field=QQ, orthonormalize=False)
2940 sage: J = cartesian_product([J1,J2])
2941 sage: J1.rank.clear_cache()
2942 sage: J2.rank.clear_cache()
2943 sage: J.rank.clear_cache()
2946 sage: J.rank() == J1.rank() + J2.rank()
2949 The product algebra will be associative if and only if all of its
2950 components are associative::
2952 sage: J1 = HadamardEJA(2)
2953 sage: J1.is_associative()
2955 sage: J2 = HadamardEJA(3)
2956 sage: J2.is_associative()
2958 sage: J3 = RealSymmetricEJA(3)
2959 sage: J3.is_associative()
2961 sage: CP1 = cartesian_product([J1,J2])
2962 sage: CP1.is_associative()
2964 sage: CP2 = cartesian_product([J1,J3])
2965 sage: CP2.is_associative()
2968 Cartesian products of Cartesian products work::
2970 sage: J1 = JordanSpinEJA(1)
2971 sage: J2 = JordanSpinEJA(1)
2972 sage: J3 = JordanSpinEJA(1)
2973 sage: J = cartesian_product([J1,cartesian_product([J2,J3])])
2974 sage: J.multiplication_table()
2975 +----++----+----+----+
2976 | * || b0 | b1 | b2 |
2977 +====++====+====+====+
2978 | b0 || b0 | 0 | 0 |
2979 +----++----+----+----+
2980 | b1 || 0 | b1 | 0 |
2981 +----++----+----+----+
2982 | b2 || 0 | 0 | b2 |
2983 +----++----+----+----+
2984 sage: HadamardEJA(3).multiplication_table()
2985 +----++----+----+----+
2986 | * || b0 | b1 | b2 |
2987 +====++====+====+====+
2988 | b0 || b0 | 0 | 0 |
2989 +----++----+----+----+
2990 | b1 || 0 | b1 | 0 |
2991 +----++----+----+----+
2992 | b2 || 0 | 0 | b2 |
2993 +----++----+----+----+
2997 All factors must share the same base field::
2999 sage: J1 = HadamardEJA(2, field=QQ)
3000 sage: J2 = RealSymmetricEJA(2)
3001 sage: CartesianProductEJA((J1,J2))
3002 Traceback (most recent call last):
3004 ValueError: all factors must share the same base field
3006 The cached unit element is the same one that would be computed::
3008 sage: set_random_seed() # long time
3009 sage: J1 = random_eja() # long time
3010 sage: J2 = random_eja() # long time
3011 sage: J = cartesian_product([J1,J2]) # long time
3012 sage: actual = J.one() # long time
3013 sage: J.one.clear_cache() # long time
3014 sage: expected = J.one() # long time
3015 sage: actual == expected # long time
3019 Element
= FiniteDimensionalEJAElement
3022 def __init__(self
, factors
, **kwargs
):
3027 self
._sets
= factors
3029 field
= factors
[0].base_ring()
3030 if not all( J
.base_ring() == field
for J
in factors
):
3031 raise ValueError("all factors must share the same base field")
3033 associative
= all( f
.is_associative() for f
in factors
)
3035 # Compute my matrix space. This category isn't perfect, but
3036 # is good enough for what we need to do.
3037 MS_cat
= MagmaticAlgebras(field
).FiniteDimensional().WithBasis()
3038 MS_cat
= MS_cat
.Unital().CartesianProducts()
3039 MS_factors
= tuple( J
.matrix_space() for J
in factors
)
3040 from sage
.sets
.cartesian_product
import CartesianProduct
3041 MS
= CartesianProduct(MS_factors
, MS_cat
)
3046 for b
in factors
[i
].matrix_basis():
3051 basis
= tuple( MS(b
) for b
in basis
)
3053 # Define jordan/inner products that operate on that matrix_basis.
3054 def jordan_product(x
,y
):
3056 (factors
[i
](x
[i
])*factors
[i
](y
[i
])).to_matrix()
3060 def inner_product(x
, y
):
3062 factors
[i
](x
[i
]).inner_product(factors
[i
](y
[i
]))
3066 # There's no need to check the field since it already came
3067 # from an EJA. Likewise the axioms are guaranteed to be
3068 # satisfied, unless the guy writing this class sucks.
3070 # If you want the basis to be orthonormalized, orthonormalize
3072 FiniteDimensionalEJA
.__init
__(self
,
3078 orthonormalize
=False,
3079 associative
=associative
,
3080 cartesian_product
=True,
3084 # Since we don't (re)orthonormalize the basis, the FDEJA
3085 # constructor is going to set self._deortho_matrix to the
3086 # identity matrix. Here we set it to the correct value using
3087 # the deortho matrices from our factors.
3088 self
._deortho
_matrix
= matrix
.block_diagonal( [J
._deortho
_matrix
3091 self
.rank
.set_cache(sum(J
.rank() for J
in factors
))
3092 ones
= tuple(J
.one().to_matrix() for J
in factors
)
3093 self
.one
.set_cache(self(ones
))
3095 def cartesian_factors(self
):
3096 # Copy/pasted from CombinatorialFreeModule_CartesianProduct.
3099 def cartesian_factor(self
, i
):
3101 Return the ``i``th factor of this algebra.
3103 return self
._sets
[i
]
3106 # Copy/pasted from CombinatorialFreeModule_CartesianProduct.
3107 from sage
.categories
.cartesian_product
import cartesian_product
3108 return cartesian_product
.symbol
.join("%s" % factor
3109 for factor
in self
._sets
)
3111 def matrix_space(self
):
3113 Return the space that our matrix basis lives in as a Cartesian
3116 We don't simply use the ``cartesian_product()`` functor here
3117 because it acts differently on SageMath MatrixSpaces and our
3118 custom MatrixAlgebras, which are CombinatorialFreeModules. We
3119 always want the result to be represented (and indexed) as
3124 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
3126 ....: OctonionHermitianEJA,
3127 ....: RealSymmetricEJA)
3131 sage: J1 = HadamardEJA(1)
3132 sage: J2 = RealSymmetricEJA(2)
3133 sage: J = cartesian_product([J1,J2])
3134 sage: J.matrix_space()
3135 The Cartesian product of (Full MatrixSpace of 1 by 1 dense
3136 matrices over Algebraic Real Field, Full MatrixSpace of 2
3137 by 2 dense matrices over Algebraic Real Field)
3141 sage: J1 = ComplexHermitianEJA(1)
3142 sage: J2 = ComplexHermitianEJA(1)
3143 sage: J = cartesian_product([J1,J2])
3144 sage: J.one().to_matrix()[0]
3148 sage: J.one().to_matrix()[1]
3155 sage: J1 = OctonionHermitianEJA(1)
3156 sage: J2 = OctonionHermitianEJA(1)
3157 sage: J = cartesian_product([J1,J2])
3158 sage: J.one().to_matrix()[0]
3162 sage: J.one().to_matrix()[1]
3168 return super().matrix_space()
3172 def cartesian_projection(self
, i
):
3176 sage: from mjo.eja.eja_algebra import (random_eja,
3177 ....: JordanSpinEJA,
3179 ....: RealSymmetricEJA,
3180 ....: ComplexHermitianEJA)
3184 The projection morphisms are Euclidean Jordan algebra
3187 sage: J1 = HadamardEJA(2)
3188 sage: J2 = RealSymmetricEJA(2)
3189 sage: J = cartesian_product([J1,J2])
3190 sage: J.cartesian_projection(0)
3191 Linear operator between finite-dimensional Euclidean Jordan
3192 algebras represented by the matrix:
3195 Domain: Euclidean Jordan algebra of dimension 2 over Algebraic
3196 Real Field (+) Euclidean Jordan algebra of dimension 3 over
3197 Algebraic Real Field
3198 Codomain: Euclidean Jordan algebra of dimension 2 over Algebraic
3200 sage: J.cartesian_projection(1)
3201 Linear operator between finite-dimensional Euclidean Jordan
3202 algebras represented by the matrix:
3206 Domain: Euclidean Jordan algebra of dimension 2 over Algebraic
3207 Real Field (+) Euclidean Jordan algebra of dimension 3 over
3208 Algebraic Real Field
3209 Codomain: Euclidean Jordan algebra of dimension 3 over Algebraic
3212 The projections work the way you'd expect on the vector
3213 representation of an element::
3215 sage: J1 = JordanSpinEJA(2)
3216 sage: J2 = ComplexHermitianEJA(2)
3217 sage: J = cartesian_product([J1,J2])
3218 sage: pi_left = J.cartesian_projection(0)
3219 sage: pi_right = J.cartesian_projection(1)
3220 sage: pi_left(J.one()).to_vector()
3222 sage: pi_right(J.one()).to_vector()
3224 sage: J.one().to_vector()
3229 The answer never changes::
3231 sage: set_random_seed()
3232 sage: J1 = random_eja()
3233 sage: J2 = random_eja()
3234 sage: J = cartesian_product([J1,J2])
3235 sage: P0 = J.cartesian_projection(0)
3236 sage: P1 = J.cartesian_projection(0)
3241 offset
= sum( self
.cartesian_factor(k
).dimension()
3243 Ji
= self
.cartesian_factor(i
)
3244 Pi
= self
._module
_morphism
(lambda j
: Ji
.monomial(j
- offset
),
3247 return FiniteDimensionalEJAOperator(self
,Ji
,Pi
.matrix())
3250 def cartesian_embedding(self
, i
):
3254 sage: from mjo.eja.eja_algebra import (random_eja,
3255 ....: JordanSpinEJA,
3257 ....: RealSymmetricEJA)
3261 The embedding morphisms are Euclidean Jordan algebra
3264 sage: J1 = HadamardEJA(2)
3265 sage: J2 = RealSymmetricEJA(2)
3266 sage: J = cartesian_product([J1,J2])
3267 sage: J.cartesian_embedding(0)
3268 Linear operator between finite-dimensional Euclidean Jordan
3269 algebras represented by the matrix:
3275 Domain: Euclidean Jordan algebra of dimension 2 over
3276 Algebraic Real Field
3277 Codomain: Euclidean Jordan algebra of dimension 2 over
3278 Algebraic Real Field (+) Euclidean Jordan algebra of
3279 dimension 3 over Algebraic Real Field
3280 sage: J.cartesian_embedding(1)
3281 Linear operator between finite-dimensional Euclidean Jordan
3282 algebras represented by the matrix:
3288 Domain: Euclidean Jordan algebra of dimension 3 over
3289 Algebraic Real Field
3290 Codomain: Euclidean Jordan algebra of dimension 2 over
3291 Algebraic Real Field (+) Euclidean Jordan algebra of
3292 dimension 3 over Algebraic Real Field
3294 The embeddings work the way you'd expect on the vector
3295 representation of an element::
3297 sage: J1 = JordanSpinEJA(3)
3298 sage: J2 = RealSymmetricEJA(2)
3299 sage: J = cartesian_product([J1,J2])
3300 sage: iota_left = J.cartesian_embedding(0)
3301 sage: iota_right = J.cartesian_embedding(1)
3302 sage: iota_left(J1.zero()) == J.zero()
3304 sage: iota_right(J2.zero()) == J.zero()
3306 sage: J1.one().to_vector()
3308 sage: iota_left(J1.one()).to_vector()
3310 sage: J2.one().to_vector()
3312 sage: iota_right(J2.one()).to_vector()
3314 sage: J.one().to_vector()
3319 The answer never changes::
3321 sage: set_random_seed()
3322 sage: J1 = random_eja()
3323 sage: J2 = random_eja()
3324 sage: J = cartesian_product([J1,J2])
3325 sage: E0 = J.cartesian_embedding(0)
3326 sage: E1 = J.cartesian_embedding(0)
3330 Composing a projection with the corresponding inclusion should
3331 produce the identity map, and mismatching them should produce
3334 sage: set_random_seed()
3335 sage: J1 = random_eja()
3336 sage: J2 = random_eja()
3337 sage: J = cartesian_product([J1,J2])
3338 sage: iota_left = J.cartesian_embedding(0)
3339 sage: iota_right = J.cartesian_embedding(1)
3340 sage: pi_left = J.cartesian_projection(0)
3341 sage: pi_right = J.cartesian_projection(1)
3342 sage: pi_left*iota_left == J1.one().operator()
3344 sage: pi_right*iota_right == J2.one().operator()
3346 sage: (pi_left*iota_right).is_zero()
3348 sage: (pi_right*iota_left).is_zero()
3352 offset
= sum( self
.cartesian_factor(k
).dimension()
3354 Ji
= self
.cartesian_factor(i
)
3355 Ei
= Ji
._module
_morphism
(lambda j
: self
.monomial(j
+ offset
),
3357 return FiniteDimensionalEJAOperator(Ji
,self
,Ei
.matrix())
3361 FiniteDimensionalEJA
.CartesianProduct
= CartesianProductEJA
3363 class RationalBasisCartesianProductEJA(CartesianProductEJA
,
3366 A separate class for products of algebras for which we know a
3371 sage: from mjo.eja.eja_algebra import (HadamardEJA,
3372 ....: JordanSpinEJA,
3373 ....: OctonionHermitianEJA,
3374 ....: RealSymmetricEJA)
3378 This gives us fast characteristic polynomial computations in
3379 product algebras, too::
3382 sage: J1 = JordanSpinEJA(2)
3383 sage: J2 = RealSymmetricEJA(3)
3384 sage: J = cartesian_product([J1,J2])
3385 sage: J.characteristic_polynomial_of().degree()
3392 The ``cartesian_product()`` function only uses the first factor to
3393 decide where the result will live; thus we have to be careful to
3394 check that all factors do indeed have a `_rational_algebra` member
3395 before we try to access it::
3397 sage: J1 = OctonionHermitianEJA(1) # no rational basis
3398 sage: J2 = HadamardEJA(2)
3399 sage: cartesian_product([J1,J2])
3400 Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
3401 (+) Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
3402 sage: cartesian_product([J2,J1])
3403 Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
3404 (+) Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
3407 def __init__(self
, algebras
, **kwargs
):
3408 CartesianProductEJA
.__init
__(self
, algebras
, **kwargs
)
3410 self
._rational
_algebra
= None
3411 if self
.vector_space().base_field() is not QQ
:
3412 if all( hasattr(r
, "_rational_algebra") for r
in algebras
):
3413 self
._rational
_algebra
= cartesian_product([
3414 r
._rational
_algebra
for r
in algebras
3418 RationalBasisEJA
.CartesianProduct
= RationalBasisCartesianProductEJA
3420 def random_eja(max_dimension
=None, *args
, **kwargs
):
3421 # Use the ConcreteEJA default as the total upper bound (regardless
3422 # of any whether or not any individual factors set a lower limit).
3423 if max_dimension
is None:
3424 max_dimension
= ConcreteEJA
._max
_random
_instance
_dimension
()
3425 J1
= ConcreteEJA
.random_instance(max_dimension
, *args
, **kwargs
)
3428 # Roll the dice to see if we attempt a Cartesian product.
3429 dice_roll
= ZZ
.random_element(len(ConcreteEJA
.__subclasses
__()) + 1)
3430 new_max_dimension
= max_dimension
- J1
.dimension()
3431 if new_max_dimension
== 0 or dice_roll
!= 0:
3432 # If it's already as big as we're willing to tolerate, just
3433 # return it and don't worry about Cartesian products.
3436 # Use random_eja() again so we can get more than two factors
3437 # if the sub-call also Decides on a cartesian product.
3438 J2
= random_eja(new_max_dimension
, *args
, **kwargs
)
3439 return cartesian_product([J1
,J2
])