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.
315 degree
= len(_all2list(basis
[0]))
317 # Build an ambient space that fits our matrix basis when
318 # written out as "long vectors."
319 V
= VectorSpace(field
, degree
)
321 # The matrix that will hold the orthonormal -> unorthonormal
322 # coordinate transformation. Default to an identity matrix of
323 # the appropriate size to avoid special cases for None
325 self
._deortho
_matrix
= matrix
.identity(field
,n
)
328 # Save a copy of the un-orthonormalized basis for later.
329 # Convert it to ambient V (vector) coordinates while we're
330 # at it, because we'd have to do it later anyway.
331 deortho_vector_basis
= tuple( V(_all2list(b
)) for b
in basis
)
333 from mjo
.eja
.eja_utils
import gram_schmidt
334 basis
= tuple(gram_schmidt(basis
, inner_product
))
336 # Save the (possibly orthonormalized) matrix basis for
337 # later, as well as the space that its elements live in.
338 # In most cases we can deduce the matrix space, but when
339 # n == 0 (that is, there are no basis elements) we cannot.
340 self
._matrix
_basis
= basis
341 if matrix_space
is None:
342 self
._matrix
_space
= self
._matrix
_basis
[0].parent()
344 self
._matrix
_space
= matrix_space
346 # Now create the vector space for the algebra, which will have
347 # its own set of non-ambient coordinates (in terms of the
349 vector_basis
= tuple( V(_all2list(b
)) for b
in basis
)
350 W
= V
.span_of_basis( vector_basis
, check
=check_axioms
)
353 # Now "W" is the vector space of our algebra coordinates. The
354 # variables "X1", "X2",... refer to the entries of vectors in
355 # W. Thus to convert back and forth between the orthonormal
356 # coordinates and the given ones, we need to stick the original
358 U
= V
.span_of_basis( deortho_vector_basis
, check
=check_axioms
)
359 self
._deortho
_matrix
= matrix
.column( U
.coordinate_vector(q
)
360 for q
in vector_basis
)
363 # Now we actually compute the multiplication and inner-product
364 # tables/matrices using the possibly-orthonormalized basis.
365 self
._inner
_product
_matrix
= matrix
.identity(field
, n
)
366 self
._multiplication
_table
= [ [0 for j
in range(i
+1)]
369 # Note: the Jordan and inner-products are defined in terms
370 # of the ambient basis. It's important that their arguments
371 # are in ambient coordinates as well.
374 # ortho basis w.r.t. ambient coords
378 # The jordan product returns a matrixy answer, so we
379 # have to convert it to the algebra coordinates.
380 elt
= jordan_product(q_i
, q_j
)
381 elt
= W
.coordinate_vector(V(_all2list(elt
)))
382 self
._multiplication
_table
[i
][j
] = self
.from_vector(elt
)
384 if not orthonormalize
:
385 # If we're orthonormalizing the basis with respect
386 # to an inner-product, then the inner-product
387 # matrix with respect to the resulting basis is
388 # just going to be the identity.
389 ip
= inner_product(q_i
, q_j
)
390 self
._inner
_product
_matrix
[i
,j
] = ip
391 self
._inner
_product
_matrix
[j
,i
] = ip
393 self
._inner
_product
_matrix
._cache
= {'hermitian': True}
394 self
._inner
_product
_matrix
.set_immutable()
397 if not self
._is
_jordanian
():
398 raise ValueError("Jordan identity does not hold")
399 if not self
._inner
_product
_is
_associative
():
400 raise ValueError("inner product is not associative")
403 def _coerce_map_from_base_ring(self
):
405 Disable the map from the base ring into the algebra.
407 Performing a nonsense conversion like this automatically
408 is counterpedagogical. The fallback is to try the usual
409 element constructor, which should also fail.
413 sage: from mjo.eja.eja_algebra import random_eja
417 sage: set_random_seed()
418 sage: J = random_eja()
420 Traceback (most recent call last):
422 ValueError: not an element of this algebra
428 def product_on_basis(self
, i
, j
):
430 Returns the Jordan product of the `i` and `j`th basis elements.
432 This completely defines the Jordan product on the algebra, and
433 is used direclty by our superclass machinery to implement
438 sage: from mjo.eja.eja_algebra import random_eja
442 sage: set_random_seed()
443 sage: J = random_eja()
444 sage: n = J.dimension()
447 sage: bi_bj = J.zero()*J.zero()
449 ....: i = ZZ.random_element(n)
450 ....: j = ZZ.random_element(n)
451 ....: bi = J.monomial(i)
452 ....: bj = J.monomial(j)
453 ....: bi_bj = J.product_on_basis(i,j)
458 # We only stored the lower-triangular portion of the
459 # multiplication table.
461 return self
._multiplication
_table
[i
][j
]
463 return self
._multiplication
_table
[j
][i
]
465 def inner_product(self
, x
, y
):
467 The inner product associated with this Euclidean Jordan algebra.
469 Defaults to the trace inner product, but can be overridden by
470 subclasses if they are sure that the necessary properties are
475 sage: from mjo.eja.eja_algebra import (random_eja,
477 ....: BilinearFormEJA)
481 Our inner product is "associative," which means the following for
482 a symmetric bilinear form::
484 sage: set_random_seed()
485 sage: J = random_eja()
486 sage: x,y,z = J.random_elements(3)
487 sage: (x*y).inner_product(z) == y.inner_product(x*z)
492 Ensure that this is the usual inner product for the algebras
495 sage: set_random_seed()
496 sage: J = HadamardEJA.random_instance()
497 sage: x,y = J.random_elements(2)
498 sage: actual = x.inner_product(y)
499 sage: expected = x.to_vector().inner_product(y.to_vector())
500 sage: actual == expected
503 Ensure that this is one-half of the trace inner-product in a
504 BilinearFormEJA that isn't just the reals (when ``n`` isn't
505 one). This is in Faraut and Koranyi, and also my "On the
508 sage: set_random_seed()
509 sage: J = BilinearFormEJA.random_instance()
510 sage: n = J.dimension()
511 sage: x = J.random_element()
512 sage: y = J.random_element()
513 sage: (n == 1) or (x.inner_product(y) == (x*y).trace()/2)
517 B
= self
._inner
_product
_matrix
518 return (B
*x
.to_vector()).inner_product(y
.to_vector())
521 def is_associative(self
):
523 Return whether or not this algebra's Jordan product is associative.
527 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
531 sage: J = ComplexHermitianEJA(3, field=QQ, orthonormalize=False)
532 sage: J.is_associative()
534 sage: x = sum(J.gens())
535 sage: A = x.subalgebra_generated_by(orthonormalize=False)
536 sage: A.is_associative()
540 return "Associative" in self
.category().axioms()
542 def _is_commutative(self
):
544 Whether or not this algebra's multiplication table is commutative.
546 This method should of course always return ``True``, unless
547 this algebra was constructed with ``check_axioms=False`` and
548 passed an invalid multiplication table.
550 return all( x
*y
== y
*x
for x
in self
.gens() for y
in self
.gens() )
552 def _is_jordanian(self
):
554 Whether or not this algebra's multiplication table respects the
555 Jordan identity `(x^{2})(xy) = x(x^{2}y)`.
557 We only check one arrangement of `x` and `y`, so for a
558 ``True`` result to be truly true, you should also check
559 :meth:`_is_commutative`. This method should of course always
560 return ``True``, unless this algebra was constructed with
561 ``check_axioms=False`` and passed an invalid multiplication table.
563 return all( (self
.monomial(i
)**2)*(self
.monomial(i
)*self
.monomial(j
))
565 (self
.monomial(i
))*((self
.monomial(i
)**2)*self
.monomial(j
))
566 for i
in range(self
.dimension())
567 for j
in range(self
.dimension()) )
569 def _jordan_product_is_associative(self
):
571 Return whether or not this algebra's Jordan product is
572 associative; that is, whether or not `x*(y*z) = (x*y)*z`
575 This method should agree with :meth:`is_associative` unless
576 you lied about the value of the ``associative`` parameter
577 when you constructed the algebra.
581 sage: from mjo.eja.eja_algebra import (random_eja,
582 ....: RealSymmetricEJA,
583 ....: ComplexHermitianEJA,
584 ....: QuaternionHermitianEJA)
588 sage: J = RealSymmetricEJA(4, orthonormalize=False)
589 sage: J._jordan_product_is_associative()
591 sage: x = sum(J.gens())
592 sage: A = x.subalgebra_generated_by()
593 sage: A._jordan_product_is_associative()
598 sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
599 sage: J._jordan_product_is_associative()
601 sage: x = sum(J.gens())
602 sage: A = x.subalgebra_generated_by(orthonormalize=False)
603 sage: A._jordan_product_is_associative()
608 sage: J = QuaternionHermitianEJA(2)
609 sage: J._jordan_product_is_associative()
611 sage: x = sum(J.gens())
612 sage: A = x.subalgebra_generated_by()
613 sage: A._jordan_product_is_associative()
618 The values we've presupplied to the constructors agree with
621 sage: set_random_seed()
622 sage: J = random_eja()
623 sage: J.is_associative() == J._jordan_product_is_associative()
629 # Used to check whether or not something is zero.
632 # I don't know of any examples that make this magnitude
633 # necessary because I don't know how to make an
634 # associative algebra when the element subalgebra
635 # construction is unreliable (as it is over RDF; we can't
636 # find the degree of an element because we can't compute
637 # the rank of a matrix). But even multiplication of floats
638 # is non-associative, so *some* epsilon is needed... let's
639 # just take the one from _inner_product_is_associative?
642 for i
in range(self
.dimension()):
643 for j
in range(self
.dimension()):
644 for k
in range(self
.dimension()):
648 diff
= (x
*y
)*z
- x
*(y
*z
)
650 if diff
.norm() > epsilon
:
655 def _inner_product_is_associative(self
):
657 Return whether or not this algebra's inner product `B` is
658 associative; that is, whether or not `B(xy,z) = B(x,yz)`.
660 This method should of course always return ``True``, unless
661 this algebra was constructed with ``check_axioms=False`` and
662 passed an invalid Jordan or inner-product.
666 # Used to check whether or not something is zero.
669 # This choice is sufficient to allow the construction of
670 # QuaternionHermitianEJA(2, field=RDF) with check_axioms=True.
673 for i
in range(self
.dimension()):
674 for j
in range(self
.dimension()):
675 for k
in range(self
.dimension()):
679 diff
= (x
*y
).inner_product(z
) - x
.inner_product(y
*z
)
681 if diff
.abs() > epsilon
:
686 def _element_constructor_(self
, elt
):
688 Construct an element of this algebra from its vector or matrix
691 This gets called only after the parent element _call_ method
692 fails to find a coercion for the argument.
696 sage: from mjo.eja.eja_algebra import (random_eja,
699 ....: RealSymmetricEJA)
703 The identity in `S^n` is converted to the identity in the EJA::
705 sage: J = RealSymmetricEJA(3)
706 sage: I = matrix.identity(QQ,3)
707 sage: J(I) == J.one()
710 This skew-symmetric matrix can't be represented in the EJA::
712 sage: J = RealSymmetricEJA(3)
713 sage: A = matrix(QQ,3, lambda i,j: i-j)
715 Traceback (most recent call last):
717 ValueError: not an element of this algebra
719 Tuples work as well, provided that the matrix basis for the
720 algebra consists of them::
722 sage: J1 = HadamardEJA(3)
723 sage: J2 = RealSymmetricEJA(2)
724 sage: J = cartesian_product([J1,J2])
725 sage: J( (J1.matrix_basis()[1], J2.matrix_basis()[2]) )
730 Ensure that we can convert any element back and forth
731 faithfully between its matrix and algebra representations::
733 sage: set_random_seed()
734 sage: J = random_eja()
735 sage: x = J.random_element()
736 sage: J(x.to_matrix()) == x
739 We cannot coerce elements between algebras just because their
740 matrix representations are compatible::
742 sage: J1 = HadamardEJA(3)
743 sage: J2 = JordanSpinEJA(3)
745 Traceback (most recent call last):
747 ValueError: not an element of this algebra
749 Traceback (most recent call last):
751 ValueError: not an element of this algebra
753 msg
= "not an element of this algebra"
754 if elt
in self
.base_ring():
755 # Ensure that no base ring -> algebra coercion is performed
756 # by this method. There's some stupidity in sage that would
757 # otherwise propagate to this method; for example, sage thinks
758 # that the integer 3 belongs to the space of 2-by-2 matrices.
759 raise ValueError(msg
)
762 # Try to convert a vector into a column-matrix...
764 except (AttributeError, TypeError):
765 # and ignore failure, because we weren't really expecting
766 # a vector as an argument anyway.
769 if elt
not in self
.matrix_space():
770 raise ValueError(msg
)
772 # Thanks for nothing! Matrix spaces aren't vector spaces in
773 # Sage, so we have to figure out its matrix-basis coordinates
774 # ourselves. We use the basis space's ring instead of the
775 # element's ring because the basis space might be an algebraic
776 # closure whereas the base ring of the 3-by-3 identity matrix
777 # could be QQ instead of QQbar.
779 # And, we also have to handle Cartesian product bases (when
780 # the matrix basis consists of tuples) here. The "good news"
781 # is that we're already converting everything to long vectors,
782 # and that strategy works for tuples as well.
784 # We pass check=False because the matrix basis is "guaranteed"
785 # to be linearly independent... right? Ha ha.
787 V
= VectorSpace(self
.base_ring(), len(elt
))
788 W
= V
.span_of_basis( (V(_all2list(s
)) for s
in self
.matrix_basis()),
792 coords
= W
.coordinate_vector(V(elt
))
793 except ArithmeticError: # vector is not in free module
794 raise ValueError(msg
)
796 return self
.from_vector(coords
)
800 Return a string representation of ``self``.
804 sage: from mjo.eja.eja_algebra import JordanSpinEJA
808 Ensure that it says what we think it says::
810 sage: JordanSpinEJA(2, field=AA)
811 Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
812 sage: JordanSpinEJA(3, field=RDF)
813 Euclidean Jordan algebra of dimension 3 over Real Double Field
816 fmt
= "Euclidean Jordan algebra of dimension {} over {}"
817 return fmt
.format(self
.dimension(), self
.base_ring())
821 def characteristic_polynomial_of(self
):
823 Return the algebra's "characteristic polynomial of" function,
824 which is itself a multivariate polynomial that, when evaluated
825 at the coordinates of some algebra element, returns that
826 element's characteristic polynomial.
828 The resulting polynomial has `n+1` variables, where `n` is the
829 dimension of this algebra. The first `n` variables correspond to
830 the coordinates of an algebra element: when evaluated at the
831 coordinates of an algebra element with respect to a certain
832 basis, the result is a univariate polynomial (in the one
833 remaining variable ``t``), namely the characteristic polynomial
838 sage: from mjo.eja.eja_algebra import JordanSpinEJA, TrivialEJA
842 The characteristic polynomial in the spin algebra is given in
843 Alizadeh, Example 11.11::
845 sage: J = JordanSpinEJA(3)
846 sage: p = J.characteristic_polynomial_of(); p
847 X1^2 - X2^2 - X3^2 + (-2*t)*X1 + t^2
848 sage: xvec = J.one().to_vector()
852 By definition, the characteristic polynomial is a monic
853 degree-zero polynomial in a rank-zero algebra. Note that
854 Cayley-Hamilton is indeed satisfied since the polynomial
855 ``1`` evaluates to the identity element of the algebra on
858 sage: J = TrivialEJA()
859 sage: J.characteristic_polynomial_of()
866 # The list of coefficient polynomials a_0, a_1, a_2, ..., a_(r-1).
867 a
= self
._charpoly
_coefficients
()
869 # We go to a bit of trouble here to reorder the
870 # indeterminates, so that it's easier to evaluate the
871 # characteristic polynomial at x's coordinates and get back
872 # something in terms of t, which is what we want.
873 S
= PolynomialRing(self
.base_ring(),'t')
877 S
= PolynomialRing(S
, R
.variable_names())
880 return (t
**r
+ sum( a
[k
]*(t
**k
) for k
in range(r
) ))
882 def coordinate_polynomial_ring(self
):
884 The multivariate polynomial ring in which this algebra's
885 :meth:`characteristic_polynomial_of` lives.
889 sage: from mjo.eja.eja_algebra import (HadamardEJA,
890 ....: RealSymmetricEJA)
894 sage: J = HadamardEJA(2)
895 sage: J.coordinate_polynomial_ring()
896 Multivariate Polynomial Ring in X1, X2...
897 sage: J = RealSymmetricEJA(3,field=QQ,orthonormalize=False)
898 sage: J.coordinate_polynomial_ring()
899 Multivariate Polynomial Ring in X1, X2, X3, X4, X5, X6...
902 var_names
= tuple( "X%d" % z
for z
in range(1, self
.dimension()+1) )
903 return PolynomialRing(self
.base_ring(), var_names
)
905 def inner_product(self
, x
, y
):
907 The inner product associated with this Euclidean Jordan algebra.
909 Defaults to the trace inner product, but can be overridden by
910 subclasses if they are sure that the necessary properties are
915 sage: from mjo.eja.eja_algebra import (random_eja,
917 ....: BilinearFormEJA)
921 Our inner product is "associative," which means the following for
922 a symmetric bilinear form::
924 sage: set_random_seed()
925 sage: J = random_eja()
926 sage: x,y,z = J.random_elements(3)
927 sage: (x*y).inner_product(z) == y.inner_product(x*z)
932 Ensure that this is the usual inner product for the algebras
935 sage: set_random_seed()
936 sage: J = HadamardEJA.random_instance()
937 sage: x,y = J.random_elements(2)
938 sage: actual = x.inner_product(y)
939 sage: expected = x.to_vector().inner_product(y.to_vector())
940 sage: actual == expected
943 Ensure that this is one-half of the trace inner-product in a
944 BilinearFormEJA that isn't just the reals (when ``n`` isn't
945 one). This is in Faraut and Koranyi, and also my "On the
948 sage: set_random_seed()
949 sage: J = BilinearFormEJA.random_instance()
950 sage: n = J.dimension()
951 sage: x = J.random_element()
952 sage: y = J.random_element()
953 sage: (n == 1) or (x.inner_product(y) == (x*y).trace()/2)
956 B
= self
._inner
_product
_matrix
957 return (B
*x
.to_vector()).inner_product(y
.to_vector())
960 def is_trivial(self
):
962 Return whether or not this algebra is trivial.
964 A trivial algebra contains only the zero element.
968 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
973 sage: J = ComplexHermitianEJA(3)
979 sage: J = TrivialEJA()
984 return self
.dimension() == 0
987 def multiplication_table(self
):
989 Return a visual representation of this algebra's multiplication
990 table (on basis elements).
994 sage: from mjo.eja.eja_algebra import JordanSpinEJA
998 sage: J = JordanSpinEJA(4)
999 sage: J.multiplication_table()
1000 +----++----+----+----+----+
1001 | * || b0 | b1 | b2 | b3 |
1002 +====++====+====+====+====+
1003 | b0 || b0 | b1 | b2 | b3 |
1004 +----++----+----+----+----+
1005 | b1 || b1 | b0 | 0 | 0 |
1006 +----++----+----+----+----+
1007 | b2 || b2 | 0 | b0 | 0 |
1008 +----++----+----+----+----+
1009 | b3 || b3 | 0 | 0 | b0 |
1010 +----++----+----+----+----+
1013 n
= self
.dimension()
1014 # Prepend the header row.
1015 M
= [["*"] + list(self
.gens())]
1017 # And to each subsequent row, prepend an entry that belongs to
1018 # the left-side "header column."
1019 M
+= [ [self
.monomial(i
)] + [ self
.monomial(i
)*self
.monomial(j
)
1023 return table(M
, header_row
=True, header_column
=True, frame
=True)
1026 def matrix_basis(self
):
1028 Return an (often more natural) representation of this algebras
1029 basis as an ordered tuple of matrices.
1031 Every finite-dimensional Euclidean Jordan Algebra is a, up to
1032 Jordan isomorphism, a direct sum of five simple
1033 algebras---four of which comprise Hermitian matrices. And the
1034 last type of algebra can of course be thought of as `n`-by-`1`
1035 column matrices (ambiguusly called column vectors) to avoid
1036 special cases. As a result, matrices (and column vectors) are
1037 a natural representation format for Euclidean Jordan algebra
1040 But, when we construct an algebra from a basis of matrices,
1041 those matrix representations are lost in favor of coordinate
1042 vectors *with respect to* that basis. We could eventually
1043 convert back if we tried hard enough, but having the original
1044 representations handy is valuable enough that we simply store
1045 them and return them from this method.
1047 Why implement this for non-matrix algebras? Avoiding special
1048 cases for the :class:`BilinearFormEJA` pays with simplicity in
1049 its own right. But mainly, we would like to be able to assume
1050 that elements of a :class:`CartesianProductEJA` can be displayed
1051 nicely, without having to have special classes for direct sums
1052 one of whose components was a matrix algebra.
1056 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
1057 ....: RealSymmetricEJA)
1061 sage: J = RealSymmetricEJA(2)
1063 Finite family {0: b0, 1: b1, 2: b2}
1064 sage: J.matrix_basis()
1066 [1 0] [ 0 0.7071067811865475?] [0 0]
1067 [0 0], [0.7071067811865475? 0], [0 1]
1072 sage: J = JordanSpinEJA(2)
1074 Finite family {0: b0, 1: b1}
1075 sage: J.matrix_basis()
1081 return self
._matrix
_basis
1084 def matrix_space(self
):
1086 Return the matrix space in which this algebra's elements live, if
1087 we think of them as matrices (including column vectors of the
1090 "By default" this will be an `n`-by-`1` column-matrix space,
1091 except when the algebra is trivial. There it's `n`-by-`n`
1092 (where `n` is zero), to ensure that two elements of the matrix
1093 space (empty matrices) can be multiplied. For algebras of
1094 matrices, this returns the space in which their
1095 real embeddings live.
1099 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
1100 ....: JordanSpinEJA,
1101 ....: QuaternionHermitianEJA,
1106 By default, the matrix representation is just a column-matrix
1107 equivalent to the vector representation::
1109 sage: J = JordanSpinEJA(3)
1110 sage: J.matrix_space()
1111 Full MatrixSpace of 3 by 1 dense matrices over Algebraic
1114 The matrix representation in the trivial algebra is
1115 zero-by-zero instead of the usual `n`-by-one::
1117 sage: J = TrivialEJA()
1118 sage: J.matrix_space()
1119 Full MatrixSpace of 0 by 0 dense matrices over Algebraic
1122 The matrix space for complex/quaternion Hermitian matrix EJA
1123 is the space in which their real-embeddings live, not the
1124 original complex/quaternion matrix space::
1126 sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
1127 sage: J.matrix_space()
1128 Module of 2 by 2 matrices with entries in Algebraic Field over
1129 the scalar ring Rational Field
1130 sage: J = QuaternionHermitianEJA(1,field=QQ,orthonormalize=False)
1131 sage: J.matrix_space()
1132 Module of 1 by 1 matrices with entries in Quaternion
1133 Algebra (-1, -1) with base ring Rational Field over
1134 the scalar ring Rational Field
1137 return self
._matrix
_space
1143 Return the unit element of this algebra.
1147 sage: from mjo.eja.eja_algebra import (HadamardEJA,
1152 We can compute unit element in the Hadamard EJA::
1154 sage: J = HadamardEJA(5)
1156 b0 + b1 + b2 + b3 + b4
1158 The unit element in the Hadamard EJA is inherited in the
1159 subalgebras generated by its elements::
1161 sage: J = HadamardEJA(5)
1163 b0 + b1 + b2 + b3 + b4
1164 sage: x = sum(J.gens())
1165 sage: A = x.subalgebra_generated_by(orthonormalize=False)
1168 sage: A.one().superalgebra_element()
1169 b0 + b1 + b2 + b3 + b4
1173 The identity element acts like the identity, regardless of
1174 whether or not we orthonormalize::
1176 sage: set_random_seed()
1177 sage: J = random_eja()
1178 sage: x = J.random_element()
1179 sage: J.one()*x == x and x*J.one() == x
1181 sage: A = x.subalgebra_generated_by()
1182 sage: y = A.random_element()
1183 sage: A.one()*y == y and y*A.one() == y
1188 sage: set_random_seed()
1189 sage: J = random_eja(field=QQ, orthonormalize=False)
1190 sage: x = J.random_element()
1191 sage: J.one()*x == x and x*J.one() == x
1193 sage: A = x.subalgebra_generated_by(orthonormalize=False)
1194 sage: y = A.random_element()
1195 sage: A.one()*y == y and y*A.one() == y
1198 The matrix of the unit element's operator is the identity,
1199 regardless of the base field and whether or not we
1202 sage: set_random_seed()
1203 sage: J = random_eja()
1204 sage: actual = J.one().operator().matrix()
1205 sage: expected = matrix.identity(J.base_ring(), J.dimension())
1206 sage: actual == expected
1208 sage: x = J.random_element()
1209 sage: A = x.subalgebra_generated_by()
1210 sage: actual = A.one().operator().matrix()
1211 sage: expected = matrix.identity(A.base_ring(), A.dimension())
1212 sage: actual == expected
1217 sage: set_random_seed()
1218 sage: J = random_eja(field=QQ, orthonormalize=False)
1219 sage: actual = J.one().operator().matrix()
1220 sage: expected = matrix.identity(J.base_ring(), J.dimension())
1221 sage: actual == expected
1223 sage: x = J.random_element()
1224 sage: A = x.subalgebra_generated_by(orthonormalize=False)
1225 sage: actual = A.one().operator().matrix()
1226 sage: expected = matrix.identity(A.base_ring(), A.dimension())
1227 sage: actual == expected
1230 Ensure that the cached unit element (often precomputed by
1231 hand) agrees with the computed one::
1233 sage: set_random_seed()
1234 sage: J = random_eja()
1235 sage: cached = J.one()
1236 sage: J.one.clear_cache()
1237 sage: J.one() == cached
1242 sage: set_random_seed()
1243 sage: J = random_eja(field=QQ, orthonormalize=False)
1244 sage: cached = J.one()
1245 sage: J.one.clear_cache()
1246 sage: J.one() == cached
1250 # We can brute-force compute the matrices of the operators
1251 # that correspond to the basis elements of this algebra.
1252 # If some linear combination of those basis elements is the
1253 # algebra identity, then the same linear combination of
1254 # their matrices has to be the identity matrix.
1256 # Of course, matrices aren't vectors in sage, so we have to
1257 # appeal to the "long vectors" isometry.
1258 oper_vecs
= [ _mat2vec(g
.operator().matrix()) for g
in self
.gens() ]
1260 # Now we use basic linear algebra to find the coefficients,
1261 # of the matrices-as-vectors-linear-combination, which should
1262 # work for the original algebra basis too.
1263 A
= matrix(self
.base_ring(), oper_vecs
)
1265 # We used the isometry on the left-hand side already, but we
1266 # still need to do it for the right-hand side. Recall that we
1267 # wanted something that summed to the identity matrix.
1268 b
= _mat2vec( matrix
.identity(self
.base_ring(), self
.dimension()) )
1270 # Now if there's an identity element in the algebra, this
1271 # should work. We solve on the left to avoid having to
1272 # transpose the matrix "A".
1273 return self
.from_vector(A
.solve_left(b
))
1276 def peirce_decomposition(self
, c
):
1278 The Peirce decomposition of this algebra relative to the
1281 In the future, this can be extended to a complete system of
1282 orthogonal idempotents.
1286 - ``c`` -- an idempotent of this algebra.
1290 A triple (J0, J5, J1) containing two subalgebras and one subspace
1293 - ``J0`` -- the algebra on the eigenspace of ``c.operator()``
1294 corresponding to the eigenvalue zero.
1296 - ``J5`` -- the eigenspace (NOT a subalgebra) of ``c.operator()``
1297 corresponding to the eigenvalue one-half.
1299 - ``J1`` -- the algebra on the eigenspace of ``c.operator()``
1300 corresponding to the eigenvalue one.
1302 These are the only possible eigenspaces for that operator, and this
1303 algebra is a direct sum of them. The spaces ``J0`` and ``J1`` are
1304 orthogonal, and are subalgebras of this algebra with the appropriate
1309 sage: from mjo.eja.eja_algebra import random_eja, RealSymmetricEJA
1313 The canonical example comes from the symmetric matrices, which
1314 decompose into diagonal and off-diagonal parts::
1316 sage: J = RealSymmetricEJA(3)
1317 sage: C = matrix(QQ, [ [1,0,0],
1321 sage: J0,J5,J1 = J.peirce_decomposition(c)
1323 Euclidean Jordan algebra of dimension 1...
1325 Vector space of degree 6 and dimension 2...
1327 Euclidean Jordan algebra of dimension 3...
1328 sage: J0.one().to_matrix()
1332 sage: orig_df = AA.options.display_format
1333 sage: AA.options.display_format = 'radical'
1334 sage: J.from_vector(J5.basis()[0]).to_matrix()
1338 sage: J.from_vector(J5.basis()[1]).to_matrix()
1342 sage: AA.options.display_format = orig_df
1343 sage: J1.one().to_matrix()
1350 Every algebra decomposes trivially with respect to its identity
1353 sage: set_random_seed()
1354 sage: J = random_eja()
1355 sage: J0,J5,J1 = J.peirce_decomposition(J.one())
1356 sage: J0.dimension() == 0 and J5.dimension() == 0
1358 sage: J1.superalgebra() == J and J1.dimension() == J.dimension()
1361 The decomposition is into eigenspaces, and its components are
1362 therefore necessarily orthogonal. Moreover, the identity
1363 elements in the two subalgebras are the projections onto their
1364 respective subspaces of the superalgebra's identity element::
1366 sage: set_random_seed()
1367 sage: J = random_eja()
1368 sage: x = J.random_element()
1369 sage: if not J.is_trivial():
1370 ....: while x.is_nilpotent():
1371 ....: x = J.random_element()
1372 sage: c = x.subalgebra_idempotent()
1373 sage: J0,J5,J1 = J.peirce_decomposition(c)
1375 sage: for (w,y,z) in zip(J0.basis(), J5.basis(), J1.basis()):
1376 ....: w = w.superalgebra_element()
1377 ....: y = J.from_vector(y)
1378 ....: z = z.superalgebra_element()
1379 ....: ipsum += w.inner_product(y).abs()
1380 ....: ipsum += w.inner_product(z).abs()
1381 ....: ipsum += y.inner_product(z).abs()
1384 sage: J1(c) == J1.one()
1386 sage: J0(J.one() - c) == J0.one()
1390 if not c
.is_idempotent():
1391 raise ValueError("element is not idempotent: %s" % c
)
1393 # Default these to what they should be if they turn out to be
1394 # trivial, because eigenspaces_left() won't return eigenvalues
1395 # corresponding to trivial spaces (e.g. it returns only the
1396 # eigenspace corresponding to lambda=1 if you take the
1397 # decomposition relative to the identity element).
1398 trivial
= self
.subalgebra(())
1399 J0
= trivial
# eigenvalue zero
1400 J5
= VectorSpace(self
.base_ring(), 0) # eigenvalue one-half
1401 J1
= trivial
# eigenvalue one
1403 for (eigval
, eigspace
) in c
.operator().matrix().right_eigenspaces():
1404 if eigval
== ~
(self
.base_ring()(2)):
1407 gens
= tuple( self
.from_vector(b
) for b
in eigspace
.basis() )
1408 subalg
= self
.subalgebra(gens
, check_axioms
=False)
1414 raise ValueError("unexpected eigenvalue: %s" % eigval
)
1419 def random_element(self
, thorough
=False):
1421 Return a random element of this algebra.
1423 Our algebra superclass method only returns a linear
1424 combination of at most two basis elements. We instead
1425 want the vector space "random element" method that
1426 returns a more diverse selection.
1430 - ``thorough`` -- (boolean; default False) whether or not we
1431 should generate irrational coefficients for the random
1432 element when our base ring is irrational; this slows the
1433 algebra operations to a crawl, but any truly random method
1437 # For a general base ring... maybe we can trust this to do the
1438 # right thing? Unlikely, but.
1439 V
= self
.vector_space()
1440 v
= V
.random_element()
1442 if self
.base_ring() is AA
:
1443 # The "random element" method of the algebraic reals is
1444 # stupid at the moment, and only returns integers between
1445 # -2 and 2, inclusive:
1447 # https://trac.sagemath.org/ticket/30875
1449 # Instead, we implement our own "random vector" method,
1450 # and then coerce that into the algebra. We use the vector
1451 # space degree here instead of the dimension because a
1452 # subalgebra could (for example) be spanned by only two
1453 # vectors, each with five coordinates. We need to
1454 # generate all five coordinates.
1456 v
*= QQbar
.random_element().real()
1458 v
*= QQ
.random_element()
1460 return self
.from_vector(V
.coordinate_vector(v
))
1462 def random_elements(self
, count
, thorough
=False):
1464 Return ``count`` random elements as a tuple.
1468 - ``thorough`` -- (boolean; default False) whether or not we
1469 should generate irrational coefficients for the random
1470 elements when our base ring is irrational; this slows the
1471 algebra operations to a crawl, but any truly random method
1476 sage: from mjo.eja.eja_algebra import JordanSpinEJA
1480 sage: J = JordanSpinEJA(3)
1481 sage: x,y,z = J.random_elements(3)
1482 sage: all( [ x in J, y in J, z in J ])
1484 sage: len( J.random_elements(10) ) == 10
1488 return tuple( self
.random_element(thorough
)
1489 for idx
in range(count
) )
1493 def _charpoly_coefficients(self
):
1495 The `r` polynomial coefficients of the "characteristic polynomial
1500 sage: from mjo.eja.eja_algebra import random_eja
1504 The theory shows that these are all homogeneous polynomials of
1507 sage: set_random_seed()
1508 sage: J = random_eja()
1509 sage: all(p.is_homogeneous() for p in J._charpoly_coefficients())
1513 n
= self
.dimension()
1514 R
= self
.coordinate_polynomial_ring()
1516 F
= R
.fraction_field()
1519 # From a result in my book, these are the entries of the
1520 # basis representation of L_x.
1521 return sum( vars[k
]*self
.monomial(k
).operator().matrix()[i
,j
]
1524 L_x
= matrix(F
, n
, n
, L_x_i_j
)
1527 if self
.rank
.is_in_cache():
1529 # There's no need to pad the system with redundant
1530 # columns if we *know* they'll be redundant.
1533 # Compute an extra power in case the rank is equal to
1534 # the dimension (otherwise, we would stop at x^(r-1)).
1535 x_powers
= [ (L_x
**k
)*self
.one().to_vector()
1536 for k
in range(n
+1) ]
1537 A
= matrix
.column(F
, x_powers
[:n
])
1538 AE
= A
.extended_echelon_form()
1545 # The theory says that only the first "r" coefficients are
1546 # nonzero, and they actually live in the original polynomial
1547 # ring and not the fraction field. We negate them because in
1548 # the actual characteristic polynomial, they get moved to the
1549 # other side where x^r lives. We don't bother to trim A_rref
1550 # down to a square matrix and solve the resulting system,
1551 # because the upper-left r-by-r portion of A_rref is
1552 # guaranteed to be the identity matrix, so e.g.
1554 # A_rref.solve_right(Y)
1556 # would just be returning Y.
1557 return (-E
*b
)[:r
].change_ring(R
)
1562 Return the rank of this EJA.
1564 This is a cached method because we know the rank a priori for
1565 all of the algebras we can construct. Thus we can avoid the
1566 expensive ``_charpoly_coefficients()`` call unless we truly
1567 need to compute the whole characteristic polynomial.
1571 sage: from mjo.eja.eja_algebra import (HadamardEJA,
1572 ....: JordanSpinEJA,
1573 ....: RealSymmetricEJA,
1574 ....: ComplexHermitianEJA,
1575 ....: QuaternionHermitianEJA,
1580 The rank of the Jordan spin algebra is always two::
1582 sage: JordanSpinEJA(2).rank()
1584 sage: JordanSpinEJA(3).rank()
1586 sage: JordanSpinEJA(4).rank()
1589 The rank of the `n`-by-`n` Hermitian real, complex, or
1590 quaternion matrices is `n`::
1592 sage: RealSymmetricEJA(4).rank()
1594 sage: ComplexHermitianEJA(3).rank()
1596 sage: QuaternionHermitianEJA(2).rank()
1601 Ensure that every EJA that we know how to construct has a
1602 positive integer rank, unless the algebra is trivial in
1603 which case its rank will be zero::
1605 sage: set_random_seed()
1606 sage: J = random_eja()
1610 sage: r > 0 or (r == 0 and J.is_trivial())
1613 Ensure that computing the rank actually works, since the ranks
1614 of all simple algebras are known and will be cached by default::
1616 sage: set_random_seed() # long time
1617 sage: J = random_eja() # long time
1618 sage: cached = J.rank() # long time
1619 sage: J.rank.clear_cache() # long time
1620 sage: J.rank() == cached # long time
1624 return len(self
._charpoly
_coefficients
())
1627 def subalgebra(self
, basis
, **kwargs
):
1629 Create a subalgebra of this algebra from the given basis.
1631 from mjo
.eja
.eja_subalgebra
import FiniteDimensionalEJASubalgebra
1632 return FiniteDimensionalEJASubalgebra(self
, basis
, **kwargs
)
1635 def vector_space(self
):
1637 Return the vector space that underlies this algebra.
1641 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1645 sage: J = RealSymmetricEJA(2)
1646 sage: J.vector_space()
1647 Vector space of dimension 3 over...
1650 return self
.zero().to_vector().parent().ambient_vector_space()
1654 class RationalBasisEJA(FiniteDimensionalEJA
):
1656 Algebras whose supplied basis elements have all rational entries.
1660 sage: from mjo.eja.eja_algebra import BilinearFormEJA
1664 The supplied basis is orthonormalized by default::
1666 sage: B = matrix(QQ, [[1, 0, 0], [0, 25, -32], [0, -32, 41]])
1667 sage: J = BilinearFormEJA(B)
1668 sage: J.matrix_basis()
1685 # Abuse the check_field parameter to check that the entries of
1686 # out basis (in ambient coordinates) are in the field QQ.
1687 # Use _all2list to get the vector coordinates of octonion
1688 # entries and not the octonions themselves (which are not
1690 if not all( all(b_i
in QQ
for b_i
in _all2list(b
))
1692 raise TypeError("basis not rational")
1694 super().__init
__(basis
,
1698 check_field
=check_field
,
1701 self
._rational
_algebra
= None
1703 # There's no point in constructing the extra algebra if this
1704 # one is already rational.
1706 # Note: the same Jordan and inner-products work here,
1707 # because they are necessarily defined with respect to
1708 # ambient coordinates and not any particular basis.
1709 self
._rational
_algebra
= FiniteDimensionalEJA(
1714 matrix_space
=self
.matrix_space(),
1715 associative
=self
.is_associative(),
1716 orthonormalize
=False,
1721 def _charpoly_coefficients(self
):
1725 sage: from mjo.eja.eja_algebra import (BilinearFormEJA,
1726 ....: JordanSpinEJA)
1730 The base ring of the resulting polynomial coefficients is what
1731 it should be, and not the rationals (unless the algebra was
1732 already over the rationals)::
1734 sage: J = JordanSpinEJA(3)
1735 sage: J._charpoly_coefficients()
1736 (X1^2 - X2^2 - X3^2, -2*X1)
1737 sage: a0 = J._charpoly_coefficients()[0]
1739 Algebraic Real Field
1740 sage: a0.base_ring()
1741 Algebraic Real Field
1744 if self
._rational
_algebra
is None:
1745 # There's no need to construct *another* algebra over the
1746 # rationals if this one is already over the
1747 # rationals. Likewise, if we never orthonormalized our
1748 # basis, we might as well just use the given one.
1749 return super()._charpoly
_coefficients
()
1751 # Do the computation over the rationals. The answer will be
1752 # the same, because all we've done is a change of basis.
1753 # Then, change back from QQ to our real base ring
1754 a
= ( a_i
.change_ring(self
.base_ring())
1755 for a_i
in self
._rational
_algebra
._charpoly
_coefficients
() )
1757 # Otherwise, convert the coordinate variables back to the
1758 # deorthonormalized ones.
1759 R
= self
.coordinate_polynomial_ring()
1760 from sage
.modules
.free_module_element
import vector
1761 X
= vector(R
, R
.gens())
1762 BX
= self
._deortho
_matrix
*X
1764 subs_dict
= { X[i]: BX[i] for i in range(len(X)) }
1765 return tuple( a_i
.subs(subs_dict
) for a_i
in a
)
1767 class ConcreteEJA(FiniteDimensionalEJA
):
1769 A class for the Euclidean Jordan algebras that we know by name.
1771 These are the Jordan algebras whose basis, multiplication table,
1772 rank, and so on are known a priori. More to the point, they are
1773 the Euclidean Jordan algebras for which we are able to conjure up
1774 a "random instance."
1778 sage: from mjo.eja.eja_algebra import ConcreteEJA
1782 Our basis is normalized with respect to the algebra's inner
1783 product, unless we specify otherwise::
1785 sage: set_random_seed()
1786 sage: J = ConcreteEJA.random_instance()
1787 sage: all( b.norm() == 1 for b in J.gens() )
1790 Since our basis is orthonormal with respect to the algebra's inner
1791 product, and since we know that this algebra is an EJA, any
1792 left-multiplication operator's matrix will be symmetric because
1793 natural->EJA basis representation is an isometry and within the
1794 EJA the operator is self-adjoint by the Jordan axiom::
1796 sage: set_random_seed()
1797 sage: J = ConcreteEJA.random_instance()
1798 sage: x = J.random_element()
1799 sage: x.operator().is_self_adjoint()
1804 def _max_random_instance_dimension():
1806 The maximum dimension of any random instance. Ten dimensions seems
1807 to be about the point where everything takes a turn for the
1808 worse. And dimension ten (but not nine) allows the 4-by-4 real
1809 Hermitian matrices, the 2-by-2 quaternion Hermitian matrices,
1810 and the 2-by-2 octonion Hermitian matrices.
1815 def _max_random_instance_size(max_dimension
):
1817 Return an integer "size" that is an upper bound on the size of
1818 this algebra when it is used in a random test case. This size
1819 (which can be passed to the algebra's constructor) is itself
1820 based on the ``max_dimension`` parameter.
1822 This method must be implemented in each subclass.
1824 raise NotImplementedError
1827 def random_instance(cls
, max_dimension
=None, *args
, **kwargs
):
1829 Return a random instance of this type of algebra whose dimension
1830 is less than or equal to ``max_dimension``. If the dimension bound
1831 is omitted, then the ``_max_random_instance_dimension()`` is used
1832 to get a suitable bound.
1834 This method should be implemented in each subclass.
1836 from sage
.misc
.prandom
import choice
1837 eja_class
= choice(cls
.__subclasses
__())
1839 # These all bubble up to the RationalBasisEJA superclass
1840 # constructor, so any (kw)args valid there are also valid
1842 return eja_class
.random_instance(*args
, **kwargs
)
1845 class MatrixEJA(FiniteDimensionalEJA
):
1847 def _denormalized_basis(A
):
1849 Returns a basis for the space of complex Hermitian n-by-n matrices.
1851 Why do we embed these? Basically, because all of numerical linear
1852 algebra assumes that you're working with vectors consisting of `n`
1853 entries from a field and scalars from the same field. There's no way
1854 to tell SageMath that (for example) the vectors contain complex
1855 numbers, while the scalar field is real.
1859 sage: from mjo.hurwitz import (ComplexMatrixAlgebra,
1860 ....: QuaternionMatrixAlgebra,
1861 ....: OctonionMatrixAlgebra)
1862 sage: from mjo.eja.eja_algebra import MatrixEJA
1866 sage: set_random_seed()
1867 sage: n = ZZ.random_element(1,5)
1868 sage: A = MatrixSpace(QQ, n)
1869 sage: B = MatrixEJA._denormalized_basis(A)
1870 sage: all( M.is_hermitian() for M in B)
1875 sage: set_random_seed()
1876 sage: n = ZZ.random_element(1,5)
1877 sage: A = ComplexMatrixAlgebra(n, scalars=QQ)
1878 sage: B = MatrixEJA._denormalized_basis(A)
1879 sage: all( M.is_hermitian() for M in B)
1884 sage: set_random_seed()
1885 sage: n = ZZ.random_element(1,5)
1886 sage: A = QuaternionMatrixAlgebra(n, scalars=QQ)
1887 sage: B = MatrixEJA._denormalized_basis(A)
1888 sage: all( M.is_hermitian() for M in B )
1893 sage: set_random_seed()
1894 sage: n = ZZ.random_element(1,5)
1895 sage: A = OctonionMatrixAlgebra(n, scalars=QQ)
1896 sage: B = MatrixEJA._denormalized_basis(A)
1897 sage: all( M.is_hermitian() for M in B )
1901 # These work for real MatrixSpace, whose monomials only have
1902 # two coordinates (because the last one would always be "1").
1903 es
= A
.base_ring().gens()
1904 gen
= lambda A
,m
: A
.monomial(m
[:2])
1906 if hasattr(A
, 'entry_algebra_gens'):
1907 # We've got a MatrixAlgebra, and its monomials will have
1908 # three coordinates.
1909 es
= A
.entry_algebra_gens()
1910 gen
= lambda A
,m
: A
.monomial(m
)
1913 for i
in range(A
.nrows()):
1914 for j
in range(i
+1):
1916 E_ii
= gen(A
, (i
,j
,es
[0]))
1920 E_ij
= gen(A
, (i
,j
,e
))
1921 E_ij
+= E_ij
.conjugate_transpose()
1924 return tuple( basis
)
1927 def jordan_product(X
,Y
):
1928 return (X
*Y
+ Y
*X
)/2
1931 def trace_inner_product(X
,Y
):
1933 A trace inner-product for matrices that aren't embedded in the
1934 reals. It takes MATRICES as arguments, not EJA elements.
1938 sage: from mjo.eja.eja_algebra import (RealSymmetricEJA,
1939 ....: ComplexHermitianEJA,
1940 ....: QuaternionHermitianEJA,
1941 ....: OctonionHermitianEJA)
1945 sage: J = RealSymmetricEJA(2,field=QQ,orthonormalize=False)
1946 sage: I = J.one().to_matrix()
1947 sage: J.trace_inner_product(I, -I)
1952 sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
1953 sage: I = J.one().to_matrix()
1954 sage: J.trace_inner_product(I, -I)
1959 sage: J = QuaternionHermitianEJA(2,field=QQ,orthonormalize=False)
1960 sage: I = J.one().to_matrix()
1961 sage: J.trace_inner_product(I, -I)
1966 sage: J = OctonionHermitianEJA(2,field=QQ,orthonormalize=False)
1967 sage: I = J.one().to_matrix()
1968 sage: J.trace_inner_product(I, -I)
1973 if hasattr(tr
, 'coefficient'):
1974 # Works for octonions, and has to come first because they
1975 # also have a "real()" method that doesn't return an
1976 # element of the scalar ring.
1977 return tr
.coefficient(0)
1978 elif hasattr(tr
, 'coefficient_tuple'):
1979 # Works for quaternions.
1980 return tr
.coefficient_tuple()[0]
1982 # Works for real and complex numbers.
1986 def __init__(self
, matrix_space
, **kwargs
):
1987 # We know this is a valid EJA, but will double-check
1988 # if the user passes check_axioms=True.
1989 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
1992 super().__init
__(self
._denormalized
_basis
(matrix_space
),
1993 self
.jordan_product
,
1994 self
.trace_inner_product
,
1995 field
=matrix_space
.base_ring(),
1996 matrix_space
=matrix_space
,
1999 self
.rank
.set_cache(matrix_space
.nrows())
2000 self
.one
.set_cache( self(matrix_space
.one()) )
2002 class RealSymmetricEJA(MatrixEJA
, RationalBasisEJA
, ConcreteEJA
):
2004 The rank-n simple EJA consisting of real symmetric n-by-n
2005 matrices, the usual symmetric Jordan product, and the trace inner
2006 product. It has dimension `(n^2 + n)/2` over the reals.
2010 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
2014 sage: J = RealSymmetricEJA(2)
2015 sage: b0, b1, b2 = J.gens()
2023 In theory, our "field" can be any subfield of the reals::
2025 sage: RealSymmetricEJA(2, field=RDF, check_axioms=True)
2026 Euclidean Jordan algebra of dimension 3 over Real Double Field
2027 sage: RealSymmetricEJA(2, field=RR, check_axioms=True)
2028 Euclidean Jordan algebra of dimension 3 over Real Field with
2029 53 bits of precision
2033 The dimension of this algebra is `(n^2 + n) / 2`::
2035 sage: set_random_seed()
2036 sage: d = RealSymmetricEJA._max_random_instance_dimension()
2037 sage: n = RealSymmetricEJA._max_random_instance_size(d)
2038 sage: J = RealSymmetricEJA(n)
2039 sage: J.dimension() == (n^2 + n)/2
2042 The Jordan multiplication is what we think it is::
2044 sage: set_random_seed()
2045 sage: J = RealSymmetricEJA.random_instance()
2046 sage: x,y = J.random_elements(2)
2047 sage: actual = (x*y).to_matrix()
2048 sage: X = x.to_matrix()
2049 sage: Y = y.to_matrix()
2050 sage: expected = (X*Y + Y*X)/2
2051 sage: actual == expected
2053 sage: J(expected) == x*y
2056 We can change the generator prefix::
2058 sage: RealSymmetricEJA(3, prefix='q').gens()
2059 (q0, q1, q2, q3, q4, q5)
2061 We can construct the (trivial) algebra of rank zero::
2063 sage: RealSymmetricEJA(0)
2064 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2068 def _max_random_instance_size(max_dimension
):
2069 # Obtained by solving d = (n^2 + n)/2.
2070 # The ZZ-int-ZZ thing is just "floor."
2071 return ZZ(int(ZZ(8*max_dimension
+ 1).sqrt()/2 - 1/2))
2074 def random_instance(cls
, max_dimension
=None, **kwargs
):
2076 Return a random instance of this type of algebra.
2078 if max_dimension
is None:
2079 max_dimension
= cls
._max
_random
_instance
_dimension
()
2080 max_size
= cls
._max
_random
_instance
_size
(max_dimension
) + 1
2081 n
= ZZ
.random_element(max_size
)
2082 return cls(n
, **kwargs
)
2084 def __init__(self
, n
, field
=AA
, **kwargs
):
2085 # We know this is a valid EJA, but will double-check
2086 # if the user passes check_axioms=True.
2087 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2089 A
= MatrixSpace(field
, n
)
2090 super().__init
__(A
, **kwargs
)
2092 from mjo
.eja
.eja_cache
import real_symmetric_eja_coeffs
2093 a
= real_symmetric_eja_coeffs(self
)
2095 if self
._rational
_algebra
is None:
2096 self
._charpoly
_coefficients
.set_cache(a
)
2098 self
._rational
_algebra
._charpoly
_coefficients
.set_cache(a
)
2102 class ComplexHermitianEJA(MatrixEJA
, RationalBasisEJA
, ConcreteEJA
):
2104 The rank-n simple EJA consisting of complex Hermitian n-by-n
2105 matrices over the real numbers, the usual symmetric Jordan product,
2106 and the real-part-of-trace inner product. It has dimension `n^2` over
2111 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
2115 In theory, our "field" can be any subfield of the reals, but we
2116 can't use inexact real fields at the moment because SageMath
2117 doesn't know how to convert their elements into complex numbers,
2118 or even into algebraic reals::
2121 Traceback (most recent call last):
2123 TypeError: Illegal initializer for algebraic number
2125 Traceback (most recent call last):
2127 TypeError: Illegal initializer for algebraic number
2129 This causes the following error when we try to scale a matrix of
2130 complex numbers by an inexact real number::
2132 sage: ComplexHermitianEJA(2,field=RR)
2133 Traceback (most recent call last):
2135 TypeError: Unable to coerce entries (=(1.00000000000000,
2136 -0.000000000000000)) to coefficients in Algebraic Real Field
2140 The dimension of this algebra is `n^2`::
2142 sage: set_random_seed()
2143 sage: d = ComplexHermitianEJA._max_random_instance_dimension()
2144 sage: n = ComplexHermitianEJA._max_random_instance_size(d)
2145 sage: J = ComplexHermitianEJA(n)
2146 sage: J.dimension() == n^2
2149 The Jordan multiplication is what we think it is::
2151 sage: set_random_seed()
2152 sage: J = ComplexHermitianEJA.random_instance()
2153 sage: x,y = J.random_elements(2)
2154 sage: actual = (x*y).to_matrix()
2155 sage: X = x.to_matrix()
2156 sage: Y = y.to_matrix()
2157 sage: expected = (X*Y + Y*X)/2
2158 sage: actual == expected
2160 sage: J(expected) == x*y
2163 We can change the generator prefix::
2165 sage: ComplexHermitianEJA(2, prefix='z').gens()
2168 We can construct the (trivial) algebra of rank zero::
2170 sage: ComplexHermitianEJA(0)
2171 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2174 def __init__(self
, n
, field
=AA
, **kwargs
):
2175 # We know this is a valid EJA, but will double-check
2176 # if the user passes check_axioms=True.
2177 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2179 from mjo
.hurwitz
import ComplexMatrixAlgebra
2180 A
= ComplexMatrixAlgebra(n
, scalars
=field
)
2181 super().__init
__(A
, **kwargs
)
2183 from mjo
.eja
.eja_cache
import complex_hermitian_eja_coeffs
2184 a
= complex_hermitian_eja_coeffs(self
)
2186 if self
._rational
_algebra
is None:
2187 self
._charpoly
_coefficients
.set_cache(a
)
2189 self
._rational
_algebra
._charpoly
_coefficients
.set_cache(a
)
2192 def _max_random_instance_size(max_dimension
):
2193 # Obtained by solving d = n^2.
2194 # The ZZ-int-ZZ thing is just "floor."
2195 return ZZ(int(ZZ(max_dimension
).sqrt()))
2198 def random_instance(cls
, max_dimension
=None, **kwargs
):
2200 Return a random instance of this type of algebra.
2202 if max_dimension
is None:
2203 max_dimension
= cls
._max
_random
_instance
_dimension
()
2204 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
(max_dimension
) + 1)
2205 return cls(n
, **kwargs
)
2208 class QuaternionHermitianEJA(MatrixEJA
, RationalBasisEJA
, ConcreteEJA
):
2210 The rank-n simple EJA consisting of self-adjoint n-by-n quaternion
2211 matrices, the usual symmetric Jordan product, and the
2212 real-part-of-trace inner product. It has dimension `2n^2 - n` over
2217 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
2221 In theory, our "field" can be any subfield of the reals::
2223 sage: QuaternionHermitianEJA(2, field=RDF, check_axioms=True)
2224 Euclidean Jordan algebra of dimension 6 over Real Double Field
2225 sage: QuaternionHermitianEJA(2, field=RR, check_axioms=True)
2226 Euclidean Jordan algebra of dimension 6 over Real Field with
2227 53 bits of precision
2231 The dimension of this algebra is `2*n^2 - n`::
2233 sage: set_random_seed()
2234 sage: d = QuaternionHermitianEJA._max_random_instance_dimension()
2235 sage: n = QuaternionHermitianEJA._max_random_instance_size(d)
2236 sage: J = QuaternionHermitianEJA(n)
2237 sage: J.dimension() == 2*(n^2) - n
2240 The Jordan multiplication is what we think it is::
2242 sage: set_random_seed()
2243 sage: J = QuaternionHermitianEJA.random_instance()
2244 sage: x,y = J.random_elements(2)
2245 sage: actual = (x*y).to_matrix()
2246 sage: X = x.to_matrix()
2247 sage: Y = y.to_matrix()
2248 sage: expected = (X*Y + Y*X)/2
2249 sage: actual == expected
2251 sage: J(expected) == x*y
2254 We can change the generator prefix::
2256 sage: QuaternionHermitianEJA(2, prefix='a').gens()
2257 (a0, a1, a2, a3, a4, a5)
2259 We can construct the (trivial) algebra of rank zero::
2261 sage: QuaternionHermitianEJA(0)
2262 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2265 def __init__(self
, n
, field
=AA
, **kwargs
):
2266 # We know this is a valid EJA, but will double-check
2267 # if the user passes check_axioms=True.
2268 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2270 from mjo
.hurwitz
import QuaternionMatrixAlgebra
2271 A
= QuaternionMatrixAlgebra(n
, scalars
=field
)
2272 super().__init
__(A
, **kwargs
)
2274 from mjo
.eja
.eja_cache
import quaternion_hermitian_eja_coeffs
2275 a
= quaternion_hermitian_eja_coeffs(self
)
2277 if self
._rational
_algebra
is None:
2278 self
._charpoly
_coefficients
.set_cache(a
)
2280 self
._rational
_algebra
._charpoly
_coefficients
.set_cache(a
)
2285 def _max_random_instance_size(max_dimension
):
2287 The maximum rank of a random QuaternionHermitianEJA.
2289 # Obtained by solving d = 2n^2 - n.
2290 # The ZZ-int-ZZ thing is just "floor."
2291 return ZZ(int(ZZ(8*max_dimension
+ 1).sqrt()/4 + 1/4))
2294 def random_instance(cls
, max_dimension
=None, **kwargs
):
2296 Return a random instance of this type of algebra.
2298 if max_dimension
is None:
2299 max_dimension
= cls
._max
_random
_instance
_dimension
()
2300 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
(max_dimension
) + 1)
2301 return cls(n
, **kwargs
)
2303 class OctonionHermitianEJA(MatrixEJA
, RationalBasisEJA
, ConcreteEJA
):
2307 sage: from mjo.eja.eja_algebra import (FiniteDimensionalEJA,
2308 ....: OctonionHermitianEJA)
2309 sage: from mjo.hurwitz import Octonions, OctonionMatrixAlgebra
2313 The 3-by-3 algebra satisfies the axioms of an EJA::
2315 sage: OctonionHermitianEJA(3, # long time
2316 ....: field=QQ, # long time
2317 ....: orthonormalize=False, # long time
2318 ....: check_axioms=True) # long time
2319 Euclidean Jordan algebra of dimension 27 over Rational Field
2321 After a change-of-basis, the 2-by-2 algebra has the same
2322 multiplication table as the ten-dimensional Jordan spin algebra::
2324 sage: A = OctonionMatrixAlgebra(2,Octonions(QQ),QQ)
2325 sage: b = OctonionHermitianEJA._denormalized_basis(A)
2326 sage: basis = (b[0] + b[9],) + b[1:9] + (b[0] - b[9],)
2327 sage: jp = OctonionHermitianEJA.jordan_product
2328 sage: ip = OctonionHermitianEJA.trace_inner_product
2329 sage: J = FiniteDimensionalEJA(basis,
2333 ....: orthonormalize=False)
2334 sage: J.multiplication_table()
2335 +----++----+----+----+----+----+----+----+----+----+----+
2336 | * || b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7 | b8 | b9 |
2337 +====++====+====+====+====+====+====+====+====+====+====+
2338 | b0 || b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7 | b8 | b9 |
2339 +----++----+----+----+----+----+----+----+----+----+----+
2340 | b1 || b1 | b0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2341 +----++----+----+----+----+----+----+----+----+----+----+
2342 | b2 || b2 | 0 | b0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2343 +----++----+----+----+----+----+----+----+----+----+----+
2344 | b3 || b3 | 0 | 0 | b0 | 0 | 0 | 0 | 0 | 0 | 0 |
2345 +----++----+----+----+----+----+----+----+----+----+----+
2346 | b4 || b4 | 0 | 0 | 0 | b0 | 0 | 0 | 0 | 0 | 0 |
2347 +----++----+----+----+----+----+----+----+----+----+----+
2348 | b5 || b5 | 0 | 0 | 0 | 0 | b0 | 0 | 0 | 0 | 0 |
2349 +----++----+----+----+----+----+----+----+----+----+----+
2350 | b6 || b6 | 0 | 0 | 0 | 0 | 0 | b0 | 0 | 0 | 0 |
2351 +----++----+----+----+----+----+----+----+----+----+----+
2352 | b7 || b7 | 0 | 0 | 0 | 0 | 0 | 0 | b0 | 0 | 0 |
2353 +----++----+----+----+----+----+----+----+----+----+----+
2354 | b8 || b8 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | b0 | 0 |
2355 +----++----+----+----+----+----+----+----+----+----+----+
2356 | b9 || b9 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | b0 |
2357 +----++----+----+----+----+----+----+----+----+----+----+
2361 We can actually construct the 27-dimensional Albert algebra,
2362 and we get the right unit element if we recompute it::
2364 sage: J = OctonionHermitianEJA(3, # long time
2365 ....: field=QQ, # long time
2366 ....: orthonormalize=False) # long time
2367 sage: J.one.clear_cache() # long time
2368 sage: J.one() # long time
2370 sage: J.one().to_matrix() # long time
2379 The 2-by-2 algebra is isomorphic to the ten-dimensional Jordan
2380 spin algebra, but just to be sure, we recompute its rank::
2382 sage: J = OctonionHermitianEJA(2, # long time
2383 ....: field=QQ, # long time
2384 ....: orthonormalize=False) # long time
2385 sage: J.rank.clear_cache() # long time
2386 sage: J.rank() # long time
2391 def _max_random_instance_size(max_dimension
):
2393 The maximum rank of a random QuaternionHermitianEJA.
2395 # There's certainly a formula for this, but with only four
2396 # cases to worry about, I'm not that motivated to derive it.
2397 if max_dimension
>= 27:
2399 elif max_dimension
>= 10:
2401 elif max_dimension
>= 1:
2407 def random_instance(cls
, max_dimension
=None, **kwargs
):
2409 Return a random instance of this type of algebra.
2411 if max_dimension
is None:
2412 max_dimension
= cls
._max
_random
_instance
_dimension
()
2413 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
(max_dimension
) + 1)
2414 return cls(n
, **kwargs
)
2416 def __init__(self
, n
, field
=AA
, **kwargs
):
2418 # Otherwise we don't get an EJA.
2419 raise ValueError("n cannot exceed 3")
2421 # We know this is a valid EJA, but will double-check
2422 # if the user passes check_axioms=True.
2423 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2425 from mjo
.hurwitz
import OctonionMatrixAlgebra
2426 A
= OctonionMatrixAlgebra(n
, scalars
=field
)
2427 super().__init
__(A
, **kwargs
)
2429 from mjo
.eja
.eja_cache
import octonion_hermitian_eja_coeffs
2430 a
= octonion_hermitian_eja_coeffs(self
)
2432 if self
._rational
_algebra
is None:
2433 self
._charpoly
_coefficients
.set_cache(a
)
2435 self
._rational
_algebra
._charpoly
_coefficients
.set_cache(a
)
2438 class AlbertEJA(OctonionHermitianEJA
):
2440 The Albert algebra is the algebra of three-by-three Hermitian
2441 matrices whose entries are octonions.
2445 sage: from mjo.eja.eja_algebra import AlbertEJA
2449 sage: AlbertEJA(field=QQ, orthonormalize=False)
2450 Euclidean Jordan algebra of dimension 27 over Rational Field
2451 sage: AlbertEJA() # long time
2452 Euclidean Jordan algebra of dimension 27 over Algebraic Real Field
2455 def __init__(self
, *args
, **kwargs
):
2456 super().__init
__(3, *args
, **kwargs
)
2459 class HadamardEJA(RationalBasisEJA
, ConcreteEJA
):
2461 Return the Euclidean Jordan algebra on `R^n` with the Hadamard
2462 (pointwise real-number multiplication) Jordan product and the
2463 usual inner-product.
2465 This is nothing more than the Cartesian product of ``n`` copies of
2466 the one-dimensional Jordan spin algebra, and is the most common
2467 example of a non-simple Euclidean Jordan algebra.
2471 sage: from mjo.eja.eja_algebra import HadamardEJA
2475 This multiplication table can be verified by hand::
2477 sage: J = HadamardEJA(3)
2478 sage: b0,b1,b2 = J.gens()
2494 We can change the generator prefix::
2496 sage: HadamardEJA(3, prefix='r').gens()
2499 def __init__(self
, n
, field
=AA
, **kwargs
):
2500 MS
= MatrixSpace(field
, n
, 1)
2503 jordan_product
= lambda x
,y
: x
2504 inner_product
= lambda x
,y
: x
2506 def jordan_product(x
,y
):
2507 return MS( xi
*yi
for (xi
,yi
) in zip(x
,y
) )
2509 def inner_product(x
,y
):
2512 # New defaults for keyword arguments. Don't orthonormalize
2513 # because our basis is already orthonormal with respect to our
2514 # inner-product. Don't check the axioms, because we know this
2515 # is a valid EJA... but do double-check if the user passes
2516 # check_axioms=True. Note: we DON'T override the "check_field"
2517 # default here, because the user can pass in a field!
2518 if "orthonormalize" not in kwargs
: kwargs
["orthonormalize"] = False
2519 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2521 column_basis
= tuple( MS(b
) for b
in FreeModule(field
, n
).basis() )
2522 super().__init
__(column_basis
,
2529 self
.rank
.set_cache(n
)
2531 self
.one
.set_cache( self
.sum(self
.gens()) )
2534 def _max_random_instance_dimension():
2536 There's no reason to go higher than five here. That's
2537 enough to get the point across.
2542 def _max_random_instance_size(max_dimension
):
2544 The maximum size (=dimension) of a random HadamardEJA.
2546 return max_dimension
2549 def random_instance(cls
, max_dimension
=None, **kwargs
):
2551 Return a random instance of this type of algebra.
2553 if max_dimension
is None:
2554 max_dimension
= cls
._max
_random
_instance
_dimension
()
2555 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
(max_dimension
) + 1)
2556 return cls(n
, **kwargs
)
2559 class BilinearFormEJA(RationalBasisEJA
, ConcreteEJA
):
2561 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
2562 with the half-trace inner product and jordan product ``x*y =
2563 (<Bx,y>,y_bar>, x0*y_bar + y0*x_bar)`` where `B = 1 \times B22` is
2564 a symmetric positive-definite "bilinear form" matrix. Its
2565 dimension is the size of `B`, and it has rank two in dimensions
2566 larger than two. It reduces to the ``JordanSpinEJA`` when `B` is
2567 the identity matrix of order ``n``.
2569 We insist that the one-by-one upper-left identity block of `B` be
2570 passed in as well so that we can be passed a matrix of size zero
2571 to construct a trivial algebra.
2575 sage: from mjo.eja.eja_algebra import (BilinearFormEJA,
2576 ....: JordanSpinEJA)
2580 When no bilinear form is specified, the identity matrix is used,
2581 and the resulting algebra is the Jordan spin algebra::
2583 sage: B = matrix.identity(AA,3)
2584 sage: J0 = BilinearFormEJA(B)
2585 sage: J1 = JordanSpinEJA(3)
2586 sage: J0.multiplication_table() == J0.multiplication_table()
2589 An error is raised if the matrix `B` does not correspond to a
2590 positive-definite bilinear form::
2592 sage: B = matrix.random(QQ,2,3)
2593 sage: J = BilinearFormEJA(B)
2594 Traceback (most recent call last):
2596 ValueError: bilinear form is not positive-definite
2597 sage: B = matrix.zero(QQ,3)
2598 sage: J = BilinearFormEJA(B)
2599 Traceback (most recent call last):
2601 ValueError: bilinear form is not positive-definite
2605 We can create a zero-dimensional algebra::
2607 sage: B = matrix.identity(AA,0)
2608 sage: J = BilinearFormEJA(B)
2612 We can check the multiplication condition given in the Jordan, von
2613 Neumann, and Wigner paper (and also discussed on my "On the
2614 symmetry..." paper). Note that this relies heavily on the standard
2615 choice of basis, as does anything utilizing the bilinear form
2616 matrix. We opt not to orthonormalize the basis, because if we
2617 did, we would have to normalize the `s_{i}` in a similar manner::
2619 sage: set_random_seed()
2620 sage: n = ZZ.random_element(5)
2621 sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular')
2622 sage: B11 = matrix.identity(QQ,1)
2623 sage: B22 = M.transpose()*M
2624 sage: B = block_matrix(2,2,[ [B11,0 ],
2626 sage: J = BilinearFormEJA(B, orthonormalize=False)
2627 sage: eis = VectorSpace(M.base_ring(), M.ncols()).basis()
2628 sage: V = J.vector_space()
2629 sage: sis = [ J( V([0] + (M.inverse()*ei).list()).column() )
2630 ....: for ei in eis ]
2631 sage: actual = [ sis[i]*sis[j]
2632 ....: for i in range(n-1)
2633 ....: for j in range(n-1) ]
2634 sage: expected = [ J.one() if i == j else J.zero()
2635 ....: for i in range(n-1)
2636 ....: for j in range(n-1) ]
2637 sage: actual == expected
2641 def __init__(self
, B
, field
=AA
, **kwargs
):
2642 # The matrix "B" is supplied by the user in most cases,
2643 # so it makes sense to check whether or not its positive-
2644 # definite unless we are specifically asked not to...
2645 if ("check_axioms" not in kwargs
) or kwargs
["check_axioms"]:
2646 if not B
.is_positive_definite():
2647 raise ValueError("bilinear form is not positive-definite")
2649 # However, all of the other data for this EJA is computed
2650 # by us in manner that guarantees the axioms are
2651 # satisfied. So, again, unless we are specifically asked to
2652 # verify things, we'll skip the rest of the checks.
2653 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2656 MS
= MatrixSpace(field
, n
, 1)
2658 def inner_product(x
,y
):
2659 return (y
.T
*B
*x
)[0,0]
2661 def jordan_product(x
,y
):
2666 z0
= inner_product(y
,x
)
2667 zbar
= y0
*xbar
+ x0
*ybar
2668 return MS([z0
] + zbar
.list())
2670 column_basis
= tuple( MS(b
) for b
in FreeModule(field
, n
).basis() )
2672 # TODO: I haven't actually checked this, but it seems legit.
2677 super().__init
__(column_basis
,
2682 associative
=associative
,
2685 # The rank of this algebra is two, unless we're in a
2686 # one-dimensional ambient space (because the rank is bounded
2687 # by the ambient dimension).
2688 self
.rank
.set_cache(min(n
,2))
2690 self
.one
.set_cache( self
.zero() )
2692 self
.one
.set_cache( self
.monomial(0) )
2695 def _max_random_instance_dimension():
2697 There's no reason to go higher than five here. That's
2698 enough to get the point across.
2703 def _max_random_instance_size(max_dimension
):
2705 The maximum size (=dimension) of a random BilinearFormEJA.
2707 return max_dimension
2710 def random_instance(cls
, max_dimension
=None, **kwargs
):
2712 Return a random instance of this algebra.
2714 if max_dimension
is None:
2715 max_dimension
= cls
._max
_random
_instance
_dimension
()
2716 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
(max_dimension
) + 1)
2718 B
= matrix
.identity(ZZ
, n
)
2719 return cls(B
, **kwargs
)
2721 B11
= matrix
.identity(ZZ
, 1)
2722 M
= matrix
.random(ZZ
, n
-1)
2723 I
= matrix
.identity(ZZ
, n
-1)
2725 while alpha
.is_zero():
2726 alpha
= ZZ
.random_element().abs()
2727 B22
= M
.transpose()*M
+ alpha
*I
2729 from sage
.matrix
.special
import block_matrix
2730 B
= block_matrix(2,2, [ [B11
, ZZ(0) ],
2733 return cls(B
, **kwargs
)
2736 class JordanSpinEJA(BilinearFormEJA
):
2738 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
2739 with the usual inner product and jordan product ``x*y =
2740 (<x,y>, x0*y_bar + y0*x_bar)``. It has dimension `n` over
2745 sage: from mjo.eja.eja_algebra import JordanSpinEJA
2749 This multiplication table can be verified by hand::
2751 sage: J = JordanSpinEJA(4)
2752 sage: b0,b1,b2,b3 = J.gens()
2768 We can change the generator prefix::
2770 sage: JordanSpinEJA(2, prefix='B').gens()
2775 Ensure that we have the usual inner product on `R^n`::
2777 sage: set_random_seed()
2778 sage: J = JordanSpinEJA.random_instance()
2779 sage: x,y = J.random_elements(2)
2780 sage: actual = x.inner_product(y)
2781 sage: expected = x.to_vector().inner_product(y.to_vector())
2782 sage: actual == expected
2786 def __init__(self
, n
, *args
, **kwargs
):
2787 # This is a special case of the BilinearFormEJA with the
2788 # identity matrix as its bilinear form.
2789 B
= matrix
.identity(ZZ
, n
)
2791 # Don't orthonormalize because our basis is already
2792 # orthonormal with respect to our inner-product.
2793 if "orthonormalize" not in kwargs
: kwargs
["orthonormalize"] = False
2795 # But also don't pass check_field=False here, because the user
2796 # can pass in a field!
2797 super().__init
__(B
, *args
, **kwargs
)
2800 def random_instance(cls
, max_dimension
=None, **kwargs
):
2802 Return a random instance of this type of algebra.
2804 Needed here to override the implementation for ``BilinearFormEJA``.
2806 if max_dimension
is None:
2807 max_dimension
= cls
._max
_random
_instance
_dimension
()
2808 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
(max_dimension
) + 1)
2809 return cls(n
, **kwargs
)
2812 class TrivialEJA(RationalBasisEJA
, ConcreteEJA
):
2814 The trivial Euclidean Jordan algebra consisting of only a zero element.
2818 sage: from mjo.eja.eja_algebra import TrivialEJA
2822 sage: J = TrivialEJA()
2829 sage: 7*J.one()*12*J.one()
2831 sage: J.one().inner_product(J.one())
2833 sage: J.one().norm()
2835 sage: J.one().subalgebra_generated_by()
2836 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2841 def __init__(self
, field
=AA
, **kwargs
):
2842 jordan_product
= lambda x
,y
: x
2843 inner_product
= lambda x
,y
: field
.zero()
2845 MS
= MatrixSpace(field
,0)
2847 # New defaults for keyword arguments
2848 if "orthonormalize" not in kwargs
: kwargs
["orthonormalize"] = False
2849 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2851 super().__init
__(basis
,
2859 # The rank is zero using my definition, namely the dimension of the
2860 # largest subalgebra generated by any element.
2861 self
.rank
.set_cache(0)
2862 self
.one
.set_cache( self
.zero() )
2865 def random_instance(cls
, **kwargs
):
2866 # We don't take a "size" argument so the superclass method is
2867 # inappropriate for us.
2868 return cls(**kwargs
)
2871 class CartesianProductEJA(FiniteDimensionalEJA
):
2873 The external (orthogonal) direct sum of two or more Euclidean
2874 Jordan algebras. Every Euclidean Jordan algebra decomposes into an
2875 orthogonal direct sum of simple Euclidean Jordan algebras which is
2876 then isometric to a Cartesian product, so no generality is lost by
2877 providing only this construction.
2881 sage: from mjo.eja.eja_algebra import (random_eja,
2882 ....: CartesianProductEJA,
2884 ....: JordanSpinEJA,
2885 ....: RealSymmetricEJA)
2889 The Jordan product is inherited from our factors and implemented by
2890 our CombinatorialFreeModule Cartesian product superclass::
2892 sage: set_random_seed()
2893 sage: J1 = HadamardEJA(2)
2894 sage: J2 = RealSymmetricEJA(2)
2895 sage: J = cartesian_product([J1,J2])
2896 sage: x,y = J.random_elements(2)
2900 The ability to retrieve the original factors is implemented by our
2901 CombinatorialFreeModule Cartesian product superclass::
2903 sage: J1 = HadamardEJA(2, field=QQ)
2904 sage: J2 = JordanSpinEJA(3, field=QQ)
2905 sage: J = cartesian_product([J1,J2])
2906 sage: J.cartesian_factors()
2907 (Euclidean Jordan algebra of dimension 2 over Rational Field,
2908 Euclidean Jordan algebra of dimension 3 over Rational Field)
2910 You can provide more than two factors::
2912 sage: J1 = HadamardEJA(2)
2913 sage: J2 = JordanSpinEJA(3)
2914 sage: J3 = RealSymmetricEJA(3)
2915 sage: cartesian_product([J1,J2,J3])
2916 Euclidean Jordan algebra of dimension 2 over Algebraic Real
2917 Field (+) Euclidean Jordan algebra of dimension 3 over Algebraic
2918 Real Field (+) Euclidean Jordan algebra of dimension 6 over
2919 Algebraic Real Field
2921 Rank is additive on a Cartesian product::
2923 sage: J1 = HadamardEJA(1)
2924 sage: J2 = RealSymmetricEJA(2)
2925 sage: J = cartesian_product([J1,J2])
2926 sage: J1.rank.clear_cache()
2927 sage: J2.rank.clear_cache()
2928 sage: J.rank.clear_cache()
2931 sage: J.rank() == J1.rank() + J2.rank()
2934 The same rank computation works over the rationals, with whatever
2937 sage: J1 = HadamardEJA(1, field=QQ, orthonormalize=False)
2938 sage: J2 = RealSymmetricEJA(2, field=QQ, orthonormalize=False)
2939 sage: J = cartesian_product([J1,J2])
2940 sage: J1.rank.clear_cache()
2941 sage: J2.rank.clear_cache()
2942 sage: J.rank.clear_cache()
2945 sage: J.rank() == J1.rank() + J2.rank()
2948 The product algebra will be associative if and only if all of its
2949 components are associative::
2951 sage: J1 = HadamardEJA(2)
2952 sage: J1.is_associative()
2954 sage: J2 = HadamardEJA(3)
2955 sage: J2.is_associative()
2957 sage: J3 = RealSymmetricEJA(3)
2958 sage: J3.is_associative()
2960 sage: CP1 = cartesian_product([J1,J2])
2961 sage: CP1.is_associative()
2963 sage: CP2 = cartesian_product([J1,J3])
2964 sage: CP2.is_associative()
2967 Cartesian products of Cartesian products work::
2969 sage: J1 = JordanSpinEJA(1)
2970 sage: J2 = JordanSpinEJA(1)
2971 sage: J3 = JordanSpinEJA(1)
2972 sage: J = cartesian_product([J1,cartesian_product([J2,J3])])
2973 sage: J.multiplication_table()
2974 +----++----+----+----+
2975 | * || b0 | b1 | b2 |
2976 +====++====+====+====+
2977 | b0 || b0 | 0 | 0 |
2978 +----++----+----+----+
2979 | b1 || 0 | b1 | 0 |
2980 +----++----+----+----+
2981 | b2 || 0 | 0 | b2 |
2982 +----++----+----+----+
2983 sage: HadamardEJA(3).multiplication_table()
2984 +----++----+----+----+
2985 | * || b0 | b1 | b2 |
2986 +====++====+====+====+
2987 | b0 || b0 | 0 | 0 |
2988 +----++----+----+----+
2989 | b1 || 0 | b1 | 0 |
2990 +----++----+----+----+
2991 | b2 || 0 | 0 | b2 |
2992 +----++----+----+----+
2996 All factors must share the same base field::
2998 sage: J1 = HadamardEJA(2, field=QQ)
2999 sage: J2 = RealSymmetricEJA(2)
3000 sage: CartesianProductEJA((J1,J2))
3001 Traceback (most recent call last):
3003 ValueError: all factors must share the same base field
3005 The cached unit element is the same one that would be computed::
3007 sage: set_random_seed() # long time
3008 sage: J1 = random_eja() # long time
3009 sage: J2 = random_eja() # long time
3010 sage: J = cartesian_product([J1,J2]) # long time
3011 sage: actual = J.one() # long time
3012 sage: J.one.clear_cache() # long time
3013 sage: expected = J.one() # long time
3014 sage: actual == expected # long time
3018 Element
= FiniteDimensionalEJAElement
3021 def __init__(self
, factors
, **kwargs
):
3026 self
._sets
= factors
3028 field
= factors
[0].base_ring()
3029 if not all( J
.base_ring() == field
for J
in factors
):
3030 raise ValueError("all factors must share the same base field")
3032 associative
= all( f
.is_associative() for f
in factors
)
3034 # Compute my matrix space. This category isn't perfect, but
3035 # is good enough for what we need to do.
3036 MS_cat
= MagmaticAlgebras(field
).FiniteDimensional().WithBasis()
3037 MS_cat
= MS_cat
.Unital().CartesianProducts()
3038 MS_factors
= tuple( J
.matrix_space() for J
in factors
)
3039 from sage
.sets
.cartesian_product
import CartesianProduct
3040 MS
= CartesianProduct(MS_factors
, MS_cat
)
3045 for b
in factors
[i
].matrix_basis():
3050 basis
= tuple( MS(b
) for b
in basis
)
3052 # Define jordan/inner products that operate on that matrix_basis.
3053 def jordan_product(x
,y
):
3055 (factors
[i
](x
[i
])*factors
[i
](y
[i
])).to_matrix()
3059 def inner_product(x
, y
):
3061 factors
[i
](x
[i
]).inner_product(factors
[i
](y
[i
]))
3065 # There's no need to check the field since it already came
3066 # from an EJA. Likewise the axioms are guaranteed to be
3067 # satisfied, unless the guy writing this class sucks.
3069 # If you want the basis to be orthonormalized, orthonormalize
3071 FiniteDimensionalEJA
.__init
__(self
,
3077 orthonormalize
=False,
3078 associative
=associative
,
3079 cartesian_product
=True,
3083 # Since we don't (re)orthonormalize the basis, the FDEJA
3084 # constructor is going to set self._deortho_matrix to the
3085 # identity matrix. Here we set it to the correct value using
3086 # the deortho matrices from our factors.
3087 self
._deortho
_matrix
= matrix
.block_diagonal( [J
._deortho
_matrix
3090 self
.rank
.set_cache(sum(J
.rank() for J
in factors
))
3091 ones
= tuple(J
.one().to_matrix() for J
in factors
)
3092 self
.one
.set_cache(self(ones
))
3094 def cartesian_factors(self
):
3095 # Copy/pasted from CombinatorialFreeModule_CartesianProduct.
3098 def cartesian_factor(self
, i
):
3100 Return the ``i``th factor of this algebra.
3102 return self
._sets
[i
]
3105 # Copy/pasted from CombinatorialFreeModule_CartesianProduct.
3106 from sage
.categories
.cartesian_product
import cartesian_product
3107 return cartesian_product
.symbol
.join("%s" % factor
3108 for factor
in self
._sets
)
3110 def matrix_space(self
):
3112 Return the space that our matrix basis lives in as a Cartesian
3115 We don't simply use the ``cartesian_product()`` functor here
3116 because it acts differently on SageMath MatrixSpaces and our
3117 custom MatrixAlgebras, which are CombinatorialFreeModules. We
3118 always want the result to be represented (and indexed) as
3123 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
3125 ....: OctonionHermitianEJA,
3126 ....: RealSymmetricEJA)
3130 sage: J1 = HadamardEJA(1)
3131 sage: J2 = RealSymmetricEJA(2)
3132 sage: J = cartesian_product([J1,J2])
3133 sage: J.matrix_space()
3134 The Cartesian product of (Full MatrixSpace of 1 by 1 dense
3135 matrices over Algebraic Real Field, Full MatrixSpace of 2
3136 by 2 dense matrices over Algebraic Real Field)
3140 sage: J1 = ComplexHermitianEJA(1)
3141 sage: J2 = ComplexHermitianEJA(1)
3142 sage: J = cartesian_product([J1,J2])
3143 sage: J.one().to_matrix()[0]
3147 sage: J.one().to_matrix()[1]
3154 sage: J1 = OctonionHermitianEJA(1)
3155 sage: J2 = OctonionHermitianEJA(1)
3156 sage: J = cartesian_product([J1,J2])
3157 sage: J.one().to_matrix()[0]
3161 sage: J.one().to_matrix()[1]
3167 return super().matrix_space()
3171 def cartesian_projection(self
, i
):
3175 sage: from mjo.eja.eja_algebra import (random_eja,
3176 ....: JordanSpinEJA,
3178 ....: RealSymmetricEJA,
3179 ....: ComplexHermitianEJA)
3183 The projection morphisms are Euclidean Jordan algebra
3186 sage: J1 = HadamardEJA(2)
3187 sage: J2 = RealSymmetricEJA(2)
3188 sage: J = cartesian_product([J1,J2])
3189 sage: J.cartesian_projection(0)
3190 Linear operator between finite-dimensional Euclidean Jordan
3191 algebras represented by the matrix:
3194 Domain: Euclidean Jordan algebra of dimension 2 over Algebraic
3195 Real Field (+) Euclidean Jordan algebra of dimension 3 over
3196 Algebraic Real Field
3197 Codomain: Euclidean Jordan algebra of dimension 2 over Algebraic
3199 sage: J.cartesian_projection(1)
3200 Linear operator between finite-dimensional Euclidean Jordan
3201 algebras represented by the matrix:
3205 Domain: Euclidean Jordan algebra of dimension 2 over Algebraic
3206 Real Field (+) Euclidean Jordan algebra of dimension 3 over
3207 Algebraic Real Field
3208 Codomain: Euclidean Jordan algebra of dimension 3 over Algebraic
3211 The projections work the way you'd expect on the vector
3212 representation of an element::
3214 sage: J1 = JordanSpinEJA(2)
3215 sage: J2 = ComplexHermitianEJA(2)
3216 sage: J = cartesian_product([J1,J2])
3217 sage: pi_left = J.cartesian_projection(0)
3218 sage: pi_right = J.cartesian_projection(1)
3219 sage: pi_left(J.one()).to_vector()
3221 sage: pi_right(J.one()).to_vector()
3223 sage: J.one().to_vector()
3228 The answer never changes::
3230 sage: set_random_seed()
3231 sage: J1 = random_eja()
3232 sage: J2 = random_eja()
3233 sage: J = cartesian_product([J1,J2])
3234 sage: P0 = J.cartesian_projection(0)
3235 sage: P1 = J.cartesian_projection(0)
3240 offset
= sum( self
.cartesian_factor(k
).dimension()
3242 Ji
= self
.cartesian_factor(i
)
3243 Pi
= self
._module
_morphism
(lambda j
: Ji
.monomial(j
- offset
),
3246 return FiniteDimensionalEJAOperator(self
,Ji
,Pi
.matrix())
3249 def cartesian_embedding(self
, i
):
3253 sage: from mjo.eja.eja_algebra import (random_eja,
3254 ....: JordanSpinEJA,
3256 ....: RealSymmetricEJA)
3260 The embedding morphisms are Euclidean Jordan algebra
3263 sage: J1 = HadamardEJA(2)
3264 sage: J2 = RealSymmetricEJA(2)
3265 sage: J = cartesian_product([J1,J2])
3266 sage: J.cartesian_embedding(0)
3267 Linear operator between finite-dimensional Euclidean Jordan
3268 algebras represented by the matrix:
3274 Domain: Euclidean Jordan algebra of dimension 2 over
3275 Algebraic Real Field
3276 Codomain: Euclidean Jordan algebra of dimension 2 over
3277 Algebraic Real Field (+) Euclidean Jordan algebra of
3278 dimension 3 over Algebraic Real Field
3279 sage: J.cartesian_embedding(1)
3280 Linear operator between finite-dimensional Euclidean Jordan
3281 algebras represented by the matrix:
3287 Domain: Euclidean Jordan algebra of dimension 3 over
3288 Algebraic Real Field
3289 Codomain: Euclidean Jordan algebra of dimension 2 over
3290 Algebraic Real Field (+) Euclidean Jordan algebra of
3291 dimension 3 over Algebraic Real Field
3293 The embeddings work the way you'd expect on the vector
3294 representation of an element::
3296 sage: J1 = JordanSpinEJA(3)
3297 sage: J2 = RealSymmetricEJA(2)
3298 sage: J = cartesian_product([J1,J2])
3299 sage: iota_left = J.cartesian_embedding(0)
3300 sage: iota_right = J.cartesian_embedding(1)
3301 sage: iota_left(J1.zero()) == J.zero()
3303 sage: iota_right(J2.zero()) == J.zero()
3305 sage: J1.one().to_vector()
3307 sage: iota_left(J1.one()).to_vector()
3309 sage: J2.one().to_vector()
3311 sage: iota_right(J2.one()).to_vector()
3313 sage: J.one().to_vector()
3318 The answer never changes::
3320 sage: set_random_seed()
3321 sage: J1 = random_eja()
3322 sage: J2 = random_eja()
3323 sage: J = cartesian_product([J1,J2])
3324 sage: E0 = J.cartesian_embedding(0)
3325 sage: E1 = J.cartesian_embedding(0)
3329 Composing a projection with the corresponding inclusion should
3330 produce the identity map, and mismatching them should produce
3333 sage: set_random_seed()
3334 sage: J1 = random_eja()
3335 sage: J2 = random_eja()
3336 sage: J = cartesian_product([J1,J2])
3337 sage: iota_left = J.cartesian_embedding(0)
3338 sage: iota_right = J.cartesian_embedding(1)
3339 sage: pi_left = J.cartesian_projection(0)
3340 sage: pi_right = J.cartesian_projection(1)
3341 sage: pi_left*iota_left == J1.one().operator()
3343 sage: pi_right*iota_right == J2.one().operator()
3345 sage: (pi_left*iota_right).is_zero()
3347 sage: (pi_right*iota_left).is_zero()
3351 offset
= sum( self
.cartesian_factor(k
).dimension()
3353 Ji
= self
.cartesian_factor(i
)
3354 Ei
= Ji
._module
_morphism
(lambda j
: self
.monomial(j
+ offset
),
3356 return FiniteDimensionalEJAOperator(Ji
,self
,Ei
.matrix())
3360 FiniteDimensionalEJA
.CartesianProduct
= CartesianProductEJA
3362 class RationalBasisCartesianProductEJA(CartesianProductEJA
,
3365 A separate class for products of algebras for which we know a
3370 sage: from mjo.eja.eja_algebra import (HadamardEJA,
3371 ....: JordanSpinEJA,
3372 ....: OctonionHermitianEJA,
3373 ....: RealSymmetricEJA)
3377 This gives us fast characteristic polynomial computations in
3378 product algebras, too::
3381 sage: J1 = JordanSpinEJA(2)
3382 sage: J2 = RealSymmetricEJA(3)
3383 sage: J = cartesian_product([J1,J2])
3384 sage: J.characteristic_polynomial_of().degree()
3391 The ``cartesian_product()`` function only uses the first factor to
3392 decide where the result will live; thus we have to be careful to
3393 check that all factors do indeed have a `_rational_algebra` member
3394 before we try to access it::
3396 sage: J1 = OctonionHermitianEJA(1) # no rational basis
3397 sage: J2 = HadamardEJA(2)
3398 sage: cartesian_product([J1,J2])
3399 Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
3400 (+) Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
3401 sage: cartesian_product([J2,J1])
3402 Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
3403 (+) Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
3406 def __init__(self
, algebras
, **kwargs
):
3407 CartesianProductEJA
.__init
__(self
, algebras
, **kwargs
)
3409 self
._rational
_algebra
= None
3410 if self
.vector_space().base_field() is not QQ
:
3411 if all( hasattr(r
, "_rational_algebra") for r
in algebras
):
3412 self
._rational
_algebra
= cartesian_product([
3413 r
._rational
_algebra
for r
in algebras
3417 RationalBasisEJA
.CartesianProduct
= RationalBasisCartesianProductEJA
3419 def random_eja(max_dimension
=None, *args
, **kwargs
):
3420 # Use the ConcreteEJA default as the total upper bound (regardless
3421 # of any whether or not any individual factors set a lower limit).
3422 if max_dimension
is None:
3423 max_dimension
= ConcreteEJA
._max
_random
_instance
_dimension
()
3424 J1
= ConcreteEJA
.random_instance(max_dimension
, *args
, **kwargs
)
3427 # Roll the dice to see if we attempt a Cartesian product.
3428 dice_roll
= ZZ
.random_element(len(ConcreteEJA
.__subclasses
__()) + 1)
3429 new_max_dimension
= max_dimension
- J1
.dimension()
3430 if new_max_dimension
== 0 or dice_roll
!= 0:
3431 # If it's already as big as we're willing to tolerate, just
3432 # return it and don't worry about Cartesian products.
3435 # Use random_eja() again so we can get more than two factors
3436 # if the sub-call also Decides on a cartesian product.
3437 J2
= random_eja(new_max_dimension
, *args
, **kwargs
)
3438 return cartesian_product([J1
,J2
])