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 def EuclideanJordanAlgebras(field
):
175 The category of Euclidean Jordan algebras over ``field``, which
176 must be a subfield of the real numbers. For now this is just a
177 convenient wrapper around all of the other category axioms that
180 category
= MagmaticAlgebras(field
).FiniteDimensional()
181 category
= category
.WithBasis().Unital().Commutative()
184 class FiniteDimensionalEJA(CombinatorialFreeModule
):
186 A finite-dimensional Euclidean Jordan algebra.
190 - ``basis`` -- a tuple; a tuple of basis elements in "matrix
191 form," which must be the same form as the arguments to
192 ``jordan_product`` and ``inner_product``. In reality, "matrix
193 form" can be either vectors, matrices, or a Cartesian product
194 (ordered tuple) of vectors or matrices. All of these would
195 ideally be vector spaces in sage with no special-casing
196 needed; but in reality we turn vectors into column-matrices
197 and Cartesian products `(a,b)` into column matrices
198 `(a,b)^{T}` after converting `a` and `b` themselves.
200 - ``jordan_product`` -- a function; afunction of two ``basis``
201 elements (in matrix form) that returns their jordan product,
202 also in matrix form; this will be applied to ``basis`` to
203 compute a multiplication table for the algebra.
205 - ``inner_product`` -- a function; a function of two ``basis``
206 elements (in matrix form) that returns their inner
207 product. This will be applied to ``basis`` to compute an
208 inner-product table (basically a matrix) for this algebra.
210 - ``matrix_space`` -- the space that your matrix basis lives in,
211 or ``None`` (the default). So long as your basis does not have
212 length zero you can omit this. But in trivial algebras, it is
215 - ``field`` -- a subfield of the reals (default: ``AA``); the scalar
216 field for the algebra.
218 - ``orthonormalize`` -- boolean (default: ``True``); whether or
219 not to orthonormalize the basis. Doing so is expensive and
220 generally rules out using the rationals as your ``field``, but
221 is required for spectral decompositions.
225 sage: from mjo.eja.eja_algebra import random_eja
229 We should compute that an element subalgebra is associative even
230 if we circumvent the element method::
232 sage: set_random_seed()
233 sage: J = random_eja(field=QQ,orthonormalize=False)
234 sage: x = J.random_element()
235 sage: A = x.subalgebra_generated_by(orthonormalize=False)
236 sage: basis = tuple(b.superalgebra_element() for b in A.basis())
237 sage: J.subalgebra(basis, orthonormalize=False).is_associative()
240 Element
= FiniteDimensionalEJAElement
243 def _check_input_field(field
):
244 if not field
.is_subring(RR
):
245 # Note: this does return true for the real algebraic
246 # field, the rationals, and any quadratic field where
247 # we've specified a real embedding.
248 raise ValueError("scalar field is not real")
251 def _check_input_axioms(basis
, jordan_product
, inner_product
):
252 if not all( jordan_product(bi
,bj
) == jordan_product(bj
,bi
)
255 raise ValueError("Jordan product is not commutative")
257 if not all( inner_product(bi
,bj
) == inner_product(bj
,bi
)
260 raise ValueError("inner-product is not commutative")
270 cartesian_product
=False,
278 self
._check
_input
_field
(field
)
281 # Check commutativity of the Jordan and inner-products.
282 # This has to be done before we build the multiplication
283 # and inner-product tables/matrices, because we take
284 # advantage of symmetry in the process.
285 self
._check
_input
_axioms
(basis
, jordan_product
, inner_product
)
288 # All zero- and one-dimensional algebras are just the real
289 # numbers with (some positive multiples of) the usual
290 # multiplication as its Jordan and inner-product.
292 if associative
is None:
293 # We should figure it out. As with check_axioms, we have to do
294 # this without the help of the _jordan_product_is_associative()
295 # method because we need to know the category before we
296 # initialize the algebra.
297 associative
= all( jordan_product(jordan_product(bi
,bj
),bk
)
299 jordan_product(bi
,jordan_product(bj
,bk
))
304 category
= EuclideanJordanAlgebras(field
)
307 # Element subalgebras can take advantage of this.
308 category
= category
.Associative()
309 if cartesian_product
:
310 # Use join() here because otherwise we only get the
311 # "Cartesian product of..." and not the things themselves.
312 category
= category
.join([category
,
313 category
.CartesianProducts()])
315 # Call the superclass constructor so that we can use its from_vector()
316 # method to build our multiplication table.
317 CombinatorialFreeModule
.__init
__(self
,
324 # Now comes all of the hard work. We'll be constructing an
325 # ambient vector space V that our (vectorized) basis lives in,
326 # as well as a subspace W of V spanned by those (vectorized)
327 # basis elements. The W-coordinates are the coefficients that
328 # we see in things like x = 1*b1 + 2*b2.
332 degree
= len(_all2list(basis
[0]))
334 # Build an ambient space that fits our matrix basis when
335 # written out as "long vectors."
336 V
= VectorSpace(field
, degree
)
338 # The matrix that will hold the orthonormal -> unorthonormal
339 # coordinate transformation. Default to an identity matrix of
340 # the appropriate size to avoid special cases for None
342 self
._deortho
_matrix
= matrix
.identity(field
,n
)
345 # Save a copy of the un-orthonormalized basis for later.
346 # Convert it to ambient V (vector) coordinates while we're
347 # at it, because we'd have to do it later anyway.
348 deortho_vector_basis
= tuple( V(_all2list(b
)) for b
in basis
)
350 from mjo
.eja
.eja_utils
import gram_schmidt
351 basis
= tuple(gram_schmidt(basis
, inner_product
))
353 # Save the (possibly orthonormalized) matrix basis for
354 # later, as well as the space that its elements live in.
355 # In most cases we can deduce the matrix space, but when
356 # n == 0 (that is, there are no basis elements) we cannot.
357 self
._matrix
_basis
= basis
358 if matrix_space
is None:
359 self
._matrix
_space
= self
._matrix
_basis
[0].parent()
361 self
._matrix
_space
= matrix_space
363 # Now create the vector space for the algebra, which will have
364 # its own set of non-ambient coordinates (in terms of the
366 vector_basis
= tuple( V(_all2list(b
)) for b
in basis
)
368 # Save the span of our matrix basis (when written out as long
369 # vectors) because otherwise we'll have to reconstruct it
370 # every time we want to coerce a matrix into the algebra.
371 self
._matrix
_span
= V
.span_of_basis( vector_basis
, check
=check_axioms
)
374 # Now "self._matrix_span" is the vector space of our
375 # algebra coordinates. The variables "X1", "X2",... refer
376 # to the entries of vectors in self._matrix_span. Thus to
377 # convert back and forth between the orthonormal
378 # coordinates and the given ones, we need to stick the
379 # original basis in self._matrix_span.
380 U
= V
.span_of_basis( deortho_vector_basis
, check
=check_axioms
)
381 self
._deortho
_matrix
= matrix
.column( U
.coordinate_vector(q
)
382 for q
in vector_basis
)
385 # Now we actually compute the multiplication and inner-product
386 # tables/matrices using the possibly-orthonormalized basis.
387 self
._inner
_product
_matrix
= matrix
.identity(field
, n
)
388 self
._multiplication
_table
= [ [0 for j
in range(i
+1)]
391 # Note: the Jordan and inner-products are defined in terms
392 # of the ambient basis. It's important that their arguments
393 # are in ambient coordinates as well.
396 # ortho basis w.r.t. ambient coords
400 # The jordan product returns a matrixy answer, so we
401 # have to convert it to the algebra coordinates.
402 elt
= jordan_product(q_i
, q_j
)
403 elt
= self
._matrix
_span
.coordinate_vector(V(_all2list(elt
)))
404 self
._multiplication
_table
[i
][j
] = self
.from_vector(elt
)
406 if not orthonormalize
:
407 # If we're orthonormalizing the basis with respect
408 # to an inner-product, then the inner-product
409 # matrix with respect to the resulting basis is
410 # just going to be the identity.
411 ip
= inner_product(q_i
, q_j
)
412 self
._inner
_product
_matrix
[i
,j
] = ip
413 self
._inner
_product
_matrix
[j
,i
] = ip
415 self
._inner
_product
_matrix
._cache
= {'hermitian': True}
416 self
._inner
_product
_matrix
.set_immutable()
419 if not self
._is
_jordanian
():
420 raise ValueError("Jordan identity does not hold")
421 if not self
._inner
_product
_is
_associative
():
422 raise ValueError("inner product is not associative")
425 def _coerce_map_from_base_ring(self
):
427 Disable the map from the base ring into the algebra.
429 Performing a nonsense conversion like this automatically
430 is counterpedagogical. The fallback is to try the usual
431 element constructor, which should also fail.
435 sage: from mjo.eja.eja_algebra import random_eja
439 sage: set_random_seed()
440 sage: J = random_eja()
442 Traceback (most recent call last):
444 ValueError: not an element of this algebra
450 def product_on_basis(self
, i
, j
):
452 Returns the Jordan product of the `i` and `j`th basis elements.
454 This completely defines the Jordan product on the algebra, and
455 is used direclty by our superclass machinery to implement
460 sage: from mjo.eja.eja_algebra import random_eja
464 sage: set_random_seed()
465 sage: J = random_eja()
466 sage: n = J.dimension()
469 sage: bi_bj = J.zero()*J.zero()
471 ....: i = ZZ.random_element(n)
472 ....: j = ZZ.random_element(n)
473 ....: bi = J.monomial(i)
474 ....: bj = J.monomial(j)
475 ....: bi_bj = J.product_on_basis(i,j)
480 # We only stored the lower-triangular portion of the
481 # multiplication table.
483 return self
._multiplication
_table
[i
][j
]
485 return self
._multiplication
_table
[j
][i
]
487 def inner_product(self
, x
, y
):
489 The inner product associated with this Euclidean Jordan algebra.
491 Defaults to the trace inner product, but can be overridden by
492 subclasses if they are sure that the necessary properties are
497 sage: from mjo.eja.eja_algebra import (random_eja,
499 ....: BilinearFormEJA)
503 Our inner product is "associative," which means the following for
504 a symmetric bilinear form::
506 sage: set_random_seed()
507 sage: J = random_eja()
508 sage: x,y,z = J.random_elements(3)
509 sage: (x*y).inner_product(z) == y.inner_product(x*z)
514 Ensure that this is the usual inner product for the algebras
517 sage: set_random_seed()
518 sage: J = HadamardEJA.random_instance()
519 sage: x,y = J.random_elements(2)
520 sage: actual = x.inner_product(y)
521 sage: expected = x.to_vector().inner_product(y.to_vector())
522 sage: actual == expected
525 Ensure that this is one-half of the trace inner-product in a
526 BilinearFormEJA that isn't just the reals (when ``n`` isn't
527 one). This is in Faraut and Koranyi, and also my "On the
530 sage: set_random_seed()
531 sage: J = BilinearFormEJA.random_instance()
532 sage: n = J.dimension()
533 sage: x = J.random_element()
534 sage: y = J.random_element()
535 sage: (n == 1) or (x.inner_product(y) == (x*y).trace()/2)
539 B
= self
._inner
_product
_matrix
540 return (B
*x
.to_vector()).inner_product(y
.to_vector())
543 def is_associative(self
):
545 Return whether or not this algebra's Jordan product is associative.
549 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
553 sage: J = ComplexHermitianEJA(3, field=QQ, orthonormalize=False)
554 sage: J.is_associative()
556 sage: x = sum(J.gens())
557 sage: A = x.subalgebra_generated_by(orthonormalize=False)
558 sage: A.is_associative()
562 return "Associative" in self
.category().axioms()
564 def _is_commutative(self
):
566 Whether or not this algebra's multiplication table is commutative.
568 This method should of course always return ``True``, unless
569 this algebra was constructed with ``check_axioms=False`` and
570 passed an invalid multiplication table.
572 return all( x
*y
== y
*x
for x
in self
.gens() for y
in self
.gens() )
574 def _is_jordanian(self
):
576 Whether or not this algebra's multiplication table respects the
577 Jordan identity `(x^{2})(xy) = x(x^{2}y)`.
579 We only check one arrangement of `x` and `y`, so for a
580 ``True`` result to be truly true, you should also check
581 :meth:`_is_commutative`. This method should of course always
582 return ``True``, unless this algebra was constructed with
583 ``check_axioms=False`` and passed an invalid multiplication table.
585 return all( (self
.monomial(i
)**2)*(self
.monomial(i
)*self
.monomial(j
))
587 (self
.monomial(i
))*((self
.monomial(i
)**2)*self
.monomial(j
))
588 for i
in range(self
.dimension())
589 for j
in range(self
.dimension()) )
591 def _jordan_product_is_associative(self
):
593 Return whether or not this algebra's Jordan product is
594 associative; that is, whether or not `x*(y*z) = (x*y)*z`
597 This method should agree with :meth:`is_associative` unless
598 you lied about the value of the ``associative`` parameter
599 when you constructed the algebra.
603 sage: from mjo.eja.eja_algebra import (random_eja,
604 ....: RealSymmetricEJA,
605 ....: ComplexHermitianEJA,
606 ....: QuaternionHermitianEJA)
610 sage: J = RealSymmetricEJA(4, orthonormalize=False)
611 sage: J._jordan_product_is_associative()
613 sage: x = sum(J.gens())
614 sage: A = x.subalgebra_generated_by()
615 sage: A._jordan_product_is_associative()
620 sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
621 sage: J._jordan_product_is_associative()
623 sage: x = sum(J.gens())
624 sage: A = x.subalgebra_generated_by(orthonormalize=False)
625 sage: A._jordan_product_is_associative()
630 sage: J = QuaternionHermitianEJA(2)
631 sage: J._jordan_product_is_associative()
633 sage: x = sum(J.gens())
634 sage: A = x.subalgebra_generated_by()
635 sage: A._jordan_product_is_associative()
640 The values we've presupplied to the constructors agree with
643 sage: set_random_seed()
644 sage: J = random_eja()
645 sage: J.is_associative() == J._jordan_product_is_associative()
651 # Used to check whether or not something is zero.
654 # I don't know of any examples that make this magnitude
655 # necessary because I don't know how to make an
656 # associative algebra when the element subalgebra
657 # construction is unreliable (as it is over RDF; we can't
658 # find the degree of an element because we can't compute
659 # the rank of a matrix). But even multiplication of floats
660 # is non-associative, so *some* epsilon is needed... let's
661 # just take the one from _inner_product_is_associative?
664 for i
in range(self
.dimension()):
665 for j
in range(self
.dimension()):
666 for k
in range(self
.dimension()):
670 diff
= (x
*y
)*z
- x
*(y
*z
)
672 if diff
.norm() > epsilon
:
677 def _inner_product_is_associative(self
):
679 Return whether or not this algebra's inner product `B` is
680 associative; that is, whether or not `B(xy,z) = B(x,yz)`.
682 This method should of course always return ``True``, unless
683 this algebra was constructed with ``check_axioms=False`` and
684 passed an invalid Jordan or inner-product.
688 # Used to check whether or not something is zero.
691 # This choice is sufficient to allow the construction of
692 # QuaternionHermitianEJA(2, field=RDF) with check_axioms=True.
695 for i
in range(self
.dimension()):
696 for j
in range(self
.dimension()):
697 for k
in range(self
.dimension()):
701 diff
= (x
*y
).inner_product(z
) - x
.inner_product(y
*z
)
703 if diff
.abs() > epsilon
:
708 def _element_constructor_(self
, elt
):
710 Construct an element of this algebra or a subalgebra from its
711 EJA element, vector, or matrix representation.
713 This gets called only after the parent element _call_ method
714 fails to find a coercion for the argument.
718 sage: from mjo.eja.eja_algebra import (random_eja,
721 ....: RealSymmetricEJA)
725 The identity in `S^n` is converted to the identity in the EJA::
727 sage: J = RealSymmetricEJA(3)
728 sage: I = matrix.identity(QQ,3)
729 sage: J(I) == J.one()
732 This skew-symmetric matrix can't be represented in the EJA::
734 sage: J = RealSymmetricEJA(3)
735 sage: A = matrix(QQ,3, lambda i,j: i-j)
737 Traceback (most recent call last):
739 ValueError: not an element of this algebra
741 Tuples work as well, provided that the matrix basis for the
742 algebra consists of them::
744 sage: J1 = HadamardEJA(3)
745 sage: J2 = RealSymmetricEJA(2)
746 sage: J = cartesian_product([J1,J2])
747 sage: J( (J1.matrix_basis()[1], J2.matrix_basis()[2]) )
750 Subalgebra elements are embedded into the superalgebra::
752 sage: J = JordanSpinEJA(3)
755 sage: x = sum(J.gens())
756 sage: A = x.subalgebra_generated_by()
762 Ensure that we can convert any element back and forth
763 faithfully between its matrix and algebra representations::
765 sage: set_random_seed()
766 sage: J = random_eja()
767 sage: x = J.random_element()
768 sage: J(x.to_matrix()) == x
771 We cannot coerce elements between algebras just because their
772 matrix representations are compatible::
774 sage: J1 = HadamardEJA(3)
775 sage: J2 = JordanSpinEJA(3)
777 Traceback (most recent call last):
779 ValueError: not an element of this algebra
781 Traceback (most recent call last):
783 ValueError: not an element of this algebra
786 msg
= "not an element of this algebra"
787 if elt
in self
.base_ring():
788 # Ensure that no base ring -> algebra coercion is performed
789 # by this method. There's some stupidity in sage that would
790 # otherwise propagate to this method; for example, sage thinks
791 # that the integer 3 belongs to the space of 2-by-2 matrices.
792 raise ValueError(msg
)
794 if hasattr(elt
, 'superalgebra_element'):
795 # Handle subalgebra elements
796 if elt
.parent().superalgebra() == self
:
797 return elt
.superalgebra_element()
799 if hasattr(elt
, 'sparse_vector'):
800 # Convert a vector into a column-matrix. We check for
801 # "sparse_vector" and not "column" because matrices also
802 # have a "column" method.
805 if elt
not in self
.matrix_space():
806 raise ValueError(msg
)
808 # Thanks for nothing! Matrix spaces aren't vector spaces in
809 # Sage, so we have to figure out its matrix-basis coordinates
810 # ourselves. We use the basis space's ring instead of the
811 # element's ring because the basis space might be an algebraic
812 # closure whereas the base ring of the 3-by-3 identity matrix
813 # could be QQ instead of QQbar.
815 # And, we also have to handle Cartesian product bases (when
816 # the matrix basis consists of tuples) here. The "good news"
817 # is that we're already converting everything to long vectors,
818 # and that strategy works for tuples as well.
820 elt
= self
._matrix
_span
.ambient_vector_space()(_all2list(elt
))
823 coords
= self
._matrix
_span
.coordinate_vector(elt
)
824 except ArithmeticError: # vector is not in free module
825 raise ValueError(msg
)
827 return self
.from_vector(coords
)
831 Return a string representation of ``self``.
835 sage: from mjo.eja.eja_algebra import JordanSpinEJA
839 Ensure that it says what we think it says::
841 sage: JordanSpinEJA(2, field=AA)
842 Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
843 sage: JordanSpinEJA(3, field=RDF)
844 Euclidean Jordan algebra of dimension 3 over Real Double Field
847 fmt
= "Euclidean Jordan algebra of dimension {} over {}"
848 return fmt
.format(self
.dimension(), self
.base_ring())
852 def characteristic_polynomial_of(self
):
854 Return the algebra's "characteristic polynomial of" function,
855 which is itself a multivariate polynomial that, when evaluated
856 at the coordinates of some algebra element, returns that
857 element's characteristic polynomial.
859 The resulting polynomial has `n+1` variables, where `n` is the
860 dimension of this algebra. The first `n` variables correspond to
861 the coordinates of an algebra element: when evaluated at the
862 coordinates of an algebra element with respect to a certain
863 basis, the result is a univariate polynomial (in the one
864 remaining variable ``t``), namely the characteristic polynomial
869 sage: from mjo.eja.eja_algebra import JordanSpinEJA, TrivialEJA
873 The characteristic polynomial in the spin algebra is given in
874 Alizadeh, Example 11.11::
876 sage: J = JordanSpinEJA(3)
877 sage: p = J.characteristic_polynomial_of(); p
878 X1^2 - X2^2 - X3^2 + (-2*t)*X1 + t^2
879 sage: xvec = J.one().to_vector()
883 By definition, the characteristic polynomial is a monic
884 degree-zero polynomial in a rank-zero algebra. Note that
885 Cayley-Hamilton is indeed satisfied since the polynomial
886 ``1`` evaluates to the identity element of the algebra on
889 sage: J = TrivialEJA()
890 sage: J.characteristic_polynomial_of()
897 # The list of coefficient polynomials a_0, a_1, a_2, ..., a_(r-1).
898 a
= self
._charpoly
_coefficients
()
900 # We go to a bit of trouble here to reorder the
901 # indeterminates, so that it's easier to evaluate the
902 # characteristic polynomial at x's coordinates and get back
903 # something in terms of t, which is what we want.
904 S
= PolynomialRing(self
.base_ring(),'t')
908 S
= PolynomialRing(S
, R
.variable_names())
911 return (t
**r
+ sum( a
[k
]*(t
**k
) for k
in range(r
) ))
913 def coordinate_polynomial_ring(self
):
915 The multivariate polynomial ring in which this algebra's
916 :meth:`characteristic_polynomial_of` lives.
920 sage: from mjo.eja.eja_algebra import (HadamardEJA,
921 ....: RealSymmetricEJA)
925 sage: J = HadamardEJA(2)
926 sage: J.coordinate_polynomial_ring()
927 Multivariate Polynomial Ring in X1, X2...
928 sage: J = RealSymmetricEJA(3,field=QQ,orthonormalize=False)
929 sage: J.coordinate_polynomial_ring()
930 Multivariate Polynomial Ring in X1, X2, X3, X4, X5, X6...
933 var_names
= tuple( "X%d" % z
for z
in range(1, self
.dimension()+1) )
934 return PolynomialRing(self
.base_ring(), var_names
)
936 def inner_product(self
, x
, y
):
938 The inner product associated with this Euclidean Jordan algebra.
940 Defaults to the trace inner product, but can be overridden by
941 subclasses if they are sure that the necessary properties are
946 sage: from mjo.eja.eja_algebra import (random_eja,
948 ....: BilinearFormEJA)
952 Our inner product is "associative," which means the following for
953 a symmetric bilinear form::
955 sage: set_random_seed()
956 sage: J = random_eja()
957 sage: x,y,z = J.random_elements(3)
958 sage: (x*y).inner_product(z) == y.inner_product(x*z)
963 Ensure that this is the usual inner product for the algebras
966 sage: set_random_seed()
967 sage: J = HadamardEJA.random_instance()
968 sage: x,y = J.random_elements(2)
969 sage: actual = x.inner_product(y)
970 sage: expected = x.to_vector().inner_product(y.to_vector())
971 sage: actual == expected
974 Ensure that this is one-half of the trace inner-product in a
975 BilinearFormEJA that isn't just the reals (when ``n`` isn't
976 one). This is in Faraut and Koranyi, and also my "On the
979 sage: set_random_seed()
980 sage: J = BilinearFormEJA.random_instance()
981 sage: n = J.dimension()
982 sage: x = J.random_element()
983 sage: y = J.random_element()
984 sage: (n == 1) or (x.inner_product(y) == (x*y).trace()/2)
987 B
= self
._inner
_product
_matrix
988 return (B
*x
.to_vector()).inner_product(y
.to_vector())
991 def is_trivial(self
):
993 Return whether or not this algebra is trivial.
995 A trivial algebra contains only the zero element.
999 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
1004 sage: J = ComplexHermitianEJA(3)
1005 sage: J.is_trivial()
1010 sage: J = TrivialEJA()
1011 sage: J.is_trivial()
1015 return self
.dimension() == 0
1018 def multiplication_table(self
):
1020 Return a visual representation of this algebra's multiplication
1021 table (on basis elements).
1025 sage: from mjo.eja.eja_algebra import JordanSpinEJA
1029 sage: J = JordanSpinEJA(4)
1030 sage: J.multiplication_table()
1031 +----++----+----+----+----+
1032 | * || b0 | b1 | b2 | b3 |
1033 +====++====+====+====+====+
1034 | b0 || b0 | b1 | b2 | b3 |
1035 +----++----+----+----+----+
1036 | b1 || b1 | b0 | 0 | 0 |
1037 +----++----+----+----+----+
1038 | b2 || b2 | 0 | b0 | 0 |
1039 +----++----+----+----+----+
1040 | b3 || b3 | 0 | 0 | b0 |
1041 +----++----+----+----+----+
1044 n
= self
.dimension()
1045 # Prepend the header row.
1046 M
= [["*"] + list(self
.gens())]
1048 # And to each subsequent row, prepend an entry that belongs to
1049 # the left-side "header column."
1050 M
+= [ [self
.monomial(i
)] + [ self
.monomial(i
)*self
.monomial(j
)
1054 return table(M
, header_row
=True, header_column
=True, frame
=True)
1057 def matrix_basis(self
):
1059 Return an (often more natural) representation of this algebras
1060 basis as an ordered tuple of matrices.
1062 Every finite-dimensional Euclidean Jordan Algebra is a, up to
1063 Jordan isomorphism, a direct sum of five simple
1064 algebras---four of which comprise Hermitian matrices. And the
1065 last type of algebra can of course be thought of as `n`-by-`1`
1066 column matrices (ambiguusly called column vectors) to avoid
1067 special cases. As a result, matrices (and column vectors) are
1068 a natural representation format for Euclidean Jordan algebra
1071 But, when we construct an algebra from a basis of matrices,
1072 those matrix representations are lost in favor of coordinate
1073 vectors *with respect to* that basis. We could eventually
1074 convert back if we tried hard enough, but having the original
1075 representations handy is valuable enough that we simply store
1076 them and return them from this method.
1078 Why implement this for non-matrix algebras? Avoiding special
1079 cases for the :class:`BilinearFormEJA` pays with simplicity in
1080 its own right. But mainly, we would like to be able to assume
1081 that elements of a :class:`CartesianProductEJA` can be displayed
1082 nicely, without having to have special classes for direct sums
1083 one of whose components was a matrix algebra.
1087 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
1088 ....: RealSymmetricEJA)
1092 sage: J = RealSymmetricEJA(2)
1094 Finite family {0: b0, 1: b1, 2: b2}
1095 sage: J.matrix_basis()
1097 [1 0] [ 0 0.7071067811865475?] [0 0]
1098 [0 0], [0.7071067811865475? 0], [0 1]
1103 sage: J = JordanSpinEJA(2)
1105 Finite family {0: b0, 1: b1}
1106 sage: J.matrix_basis()
1112 return self
._matrix
_basis
1115 def matrix_space(self
):
1117 Return the matrix space in which this algebra's elements live, if
1118 we think of them as matrices (including column vectors of the
1121 "By default" this will be an `n`-by-`1` column-matrix space,
1122 except when the algebra is trivial. There it's `n`-by-`n`
1123 (where `n` is zero), to ensure that two elements of the matrix
1124 space (empty matrices) can be multiplied. For algebras of
1125 matrices, this returns the space in which their
1126 real embeddings live.
1130 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
1131 ....: JordanSpinEJA,
1132 ....: QuaternionHermitianEJA,
1137 By default, the matrix representation is just a column-matrix
1138 equivalent to the vector representation::
1140 sage: J = JordanSpinEJA(3)
1141 sage: J.matrix_space()
1142 Full MatrixSpace of 3 by 1 dense matrices over Algebraic
1145 The matrix representation in the trivial algebra is
1146 zero-by-zero instead of the usual `n`-by-one::
1148 sage: J = TrivialEJA()
1149 sage: J.matrix_space()
1150 Full MatrixSpace of 0 by 0 dense matrices over Algebraic
1153 The matrix space for complex/quaternion Hermitian matrix EJA
1154 is the space in which their real-embeddings live, not the
1155 original complex/quaternion matrix space::
1157 sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
1158 sage: J.matrix_space()
1159 Module of 2 by 2 matrices with entries in Algebraic Field over
1160 the scalar ring Rational Field
1161 sage: J = QuaternionHermitianEJA(1,field=QQ,orthonormalize=False)
1162 sage: J.matrix_space()
1163 Module of 1 by 1 matrices with entries in Quaternion
1164 Algebra (-1, -1) with base ring Rational Field over
1165 the scalar ring Rational Field
1168 return self
._matrix
_space
1174 Return the unit element of this algebra.
1178 sage: from mjo.eja.eja_algebra import (HadamardEJA,
1183 We can compute unit element in the Hadamard EJA::
1185 sage: J = HadamardEJA(5)
1187 b0 + b1 + b2 + b3 + b4
1189 The unit element in the Hadamard EJA is inherited in the
1190 subalgebras generated by its elements::
1192 sage: J = HadamardEJA(5)
1194 b0 + b1 + b2 + b3 + b4
1195 sage: x = sum(J.gens())
1196 sage: A = x.subalgebra_generated_by(orthonormalize=False)
1199 sage: A.one().superalgebra_element()
1200 b0 + b1 + b2 + b3 + b4
1204 The identity element acts like the identity, regardless of
1205 whether or not we orthonormalize::
1207 sage: set_random_seed()
1208 sage: J = random_eja()
1209 sage: x = J.random_element()
1210 sage: J.one()*x == x and x*J.one() == x
1212 sage: A = x.subalgebra_generated_by()
1213 sage: y = A.random_element()
1214 sage: A.one()*y == y and y*A.one() == y
1219 sage: set_random_seed()
1220 sage: J = random_eja(field=QQ, orthonormalize=False)
1221 sage: x = J.random_element()
1222 sage: J.one()*x == x and x*J.one() == x
1224 sage: A = x.subalgebra_generated_by(orthonormalize=False)
1225 sage: y = A.random_element()
1226 sage: A.one()*y == y and y*A.one() == y
1229 The matrix of the unit element's operator is the identity,
1230 regardless of the base field and whether or not we
1233 sage: set_random_seed()
1234 sage: J = random_eja()
1235 sage: actual = J.one().operator().matrix()
1236 sage: expected = matrix.identity(J.base_ring(), J.dimension())
1237 sage: actual == expected
1239 sage: x = J.random_element()
1240 sage: A = x.subalgebra_generated_by()
1241 sage: actual = A.one().operator().matrix()
1242 sage: expected = matrix.identity(A.base_ring(), A.dimension())
1243 sage: actual == expected
1248 sage: set_random_seed()
1249 sage: J = random_eja(field=QQ, orthonormalize=False)
1250 sage: actual = J.one().operator().matrix()
1251 sage: expected = matrix.identity(J.base_ring(), J.dimension())
1252 sage: actual == expected
1254 sage: x = J.random_element()
1255 sage: A = x.subalgebra_generated_by(orthonormalize=False)
1256 sage: actual = A.one().operator().matrix()
1257 sage: expected = matrix.identity(A.base_ring(), A.dimension())
1258 sage: actual == expected
1261 Ensure that the cached unit element (often precomputed by
1262 hand) agrees with the computed one::
1264 sage: set_random_seed()
1265 sage: J = random_eja()
1266 sage: cached = J.one()
1267 sage: J.one.clear_cache()
1268 sage: J.one() == cached
1273 sage: set_random_seed()
1274 sage: J = random_eja(field=QQ, orthonormalize=False)
1275 sage: cached = J.one()
1276 sage: J.one.clear_cache()
1277 sage: J.one() == cached
1281 # We can brute-force compute the matrices of the operators
1282 # that correspond to the basis elements of this algebra.
1283 # If some linear combination of those basis elements is the
1284 # algebra identity, then the same linear combination of
1285 # their matrices has to be the identity matrix.
1287 # Of course, matrices aren't vectors in sage, so we have to
1288 # appeal to the "long vectors" isometry.
1289 oper_vecs
= [ _mat2vec(g
.operator().matrix()) for g
in self
.gens() ]
1291 # Now we use basic linear algebra to find the coefficients,
1292 # of the matrices-as-vectors-linear-combination, which should
1293 # work for the original algebra basis too.
1294 A
= matrix(self
.base_ring(), oper_vecs
)
1296 # We used the isometry on the left-hand side already, but we
1297 # still need to do it for the right-hand side. Recall that we
1298 # wanted something that summed to the identity matrix.
1299 b
= _mat2vec( matrix
.identity(self
.base_ring(), self
.dimension()) )
1301 # Now if there's an identity element in the algebra, this
1302 # should work. We solve on the left to avoid having to
1303 # transpose the matrix "A".
1304 return self
.from_vector(A
.solve_left(b
))
1307 def peirce_decomposition(self
, c
):
1309 The Peirce decomposition of this algebra relative to the
1312 In the future, this can be extended to a complete system of
1313 orthogonal idempotents.
1317 - ``c`` -- an idempotent of this algebra.
1321 A triple (J0, J5, J1) containing two subalgebras and one subspace
1324 - ``J0`` -- the algebra on the eigenspace of ``c.operator()``
1325 corresponding to the eigenvalue zero.
1327 - ``J5`` -- the eigenspace (NOT a subalgebra) of ``c.operator()``
1328 corresponding to the eigenvalue one-half.
1330 - ``J1`` -- the algebra on the eigenspace of ``c.operator()``
1331 corresponding to the eigenvalue one.
1333 These are the only possible eigenspaces for that operator, and this
1334 algebra is a direct sum of them. The spaces ``J0`` and ``J1`` are
1335 orthogonal, and are subalgebras of this algebra with the appropriate
1340 sage: from mjo.eja.eja_algebra import random_eja, RealSymmetricEJA
1344 The canonical example comes from the symmetric matrices, which
1345 decompose into diagonal and off-diagonal parts::
1347 sage: J = RealSymmetricEJA(3)
1348 sage: C = matrix(QQ, [ [1,0,0],
1352 sage: J0,J5,J1 = J.peirce_decomposition(c)
1354 Euclidean Jordan algebra of dimension 1...
1356 Vector space of degree 6 and dimension 2...
1358 Euclidean Jordan algebra of dimension 3...
1359 sage: J0.one().to_matrix()
1363 sage: orig_df = AA.options.display_format
1364 sage: AA.options.display_format = 'radical'
1365 sage: J.from_vector(J5.basis()[0]).to_matrix()
1369 sage: J.from_vector(J5.basis()[1]).to_matrix()
1373 sage: AA.options.display_format = orig_df
1374 sage: J1.one().to_matrix()
1381 Every algebra decomposes trivially with respect to its identity
1384 sage: set_random_seed()
1385 sage: J = random_eja()
1386 sage: J0,J5,J1 = J.peirce_decomposition(J.one())
1387 sage: J0.dimension() == 0 and J5.dimension() == 0
1389 sage: J1.superalgebra() == J and J1.dimension() == J.dimension()
1392 The decomposition is into eigenspaces, and its components are
1393 therefore necessarily orthogonal. Moreover, the identity
1394 elements in the two subalgebras are the projections onto their
1395 respective subspaces of the superalgebra's identity element::
1397 sage: set_random_seed()
1398 sage: J = random_eja()
1399 sage: x = J.random_element()
1400 sage: if not J.is_trivial():
1401 ....: while x.is_nilpotent():
1402 ....: x = J.random_element()
1403 sage: c = x.subalgebra_idempotent()
1404 sage: J0,J5,J1 = J.peirce_decomposition(c)
1406 sage: for (w,y,z) in zip(J0.basis(), J5.basis(), J1.basis()):
1407 ....: w = w.superalgebra_element()
1408 ....: y = J.from_vector(y)
1409 ....: z = z.superalgebra_element()
1410 ....: ipsum += w.inner_product(y).abs()
1411 ....: ipsum += w.inner_product(z).abs()
1412 ....: ipsum += y.inner_product(z).abs()
1415 sage: J1(c) == J1.one()
1417 sage: J0(J.one() - c) == J0.one()
1421 if not c
.is_idempotent():
1422 raise ValueError("element is not idempotent: %s" % c
)
1424 # Default these to what they should be if they turn out to be
1425 # trivial, because eigenspaces_left() won't return eigenvalues
1426 # corresponding to trivial spaces (e.g. it returns only the
1427 # eigenspace corresponding to lambda=1 if you take the
1428 # decomposition relative to the identity element).
1429 trivial
= self
.subalgebra((), check_axioms
=False)
1430 J0
= trivial
# eigenvalue zero
1431 J5
= VectorSpace(self
.base_ring(), 0) # eigenvalue one-half
1432 J1
= trivial
# eigenvalue one
1434 for (eigval
, eigspace
) in c
.operator().matrix().right_eigenspaces():
1435 if eigval
== ~
(self
.base_ring()(2)):
1438 gens
= tuple( self
.from_vector(b
) for b
in eigspace
.basis() )
1439 subalg
= self
.subalgebra(gens
, check_axioms
=False)
1445 raise ValueError("unexpected eigenvalue: %s" % eigval
)
1450 def random_element(self
, thorough
=False):
1452 Return a random element of this algebra.
1454 Our algebra superclass method only returns a linear
1455 combination of at most two basis elements. We instead
1456 want the vector space "random element" method that
1457 returns a more diverse selection.
1461 - ``thorough`` -- (boolean; default False) whether or not we
1462 should generate irrational coefficients for the random
1463 element when our base ring is irrational; this slows the
1464 algebra operations to a crawl, but any truly random method
1468 # For a general base ring... maybe we can trust this to do the
1469 # right thing? Unlikely, but.
1470 V
= self
.vector_space()
1471 v
= V
.random_element()
1473 if self
.base_ring() is AA
:
1474 # The "random element" method of the algebraic reals is
1475 # stupid at the moment, and only returns integers between
1476 # -2 and 2, inclusive:
1478 # https://trac.sagemath.org/ticket/30875
1480 # Instead, we implement our own "random vector" method,
1481 # and then coerce that into the algebra. We use the vector
1482 # space degree here instead of the dimension because a
1483 # subalgebra could (for example) be spanned by only two
1484 # vectors, each with five coordinates. We need to
1485 # generate all five coordinates.
1487 v
*= QQbar
.random_element().real()
1489 v
*= QQ
.random_element()
1491 return self
.from_vector(V
.coordinate_vector(v
))
1493 def random_elements(self
, count
, thorough
=False):
1495 Return ``count`` random elements as a tuple.
1499 - ``thorough`` -- (boolean; default False) whether or not we
1500 should generate irrational coefficients for the random
1501 elements when our base ring is irrational; this slows the
1502 algebra operations to a crawl, but any truly random method
1507 sage: from mjo.eja.eja_algebra import JordanSpinEJA
1511 sage: J = JordanSpinEJA(3)
1512 sage: x,y,z = J.random_elements(3)
1513 sage: all( [ x in J, y in J, z in J ])
1515 sage: len( J.random_elements(10) ) == 10
1519 return tuple( self
.random_element(thorough
)
1520 for idx
in range(count
) )
1524 def _charpoly_coefficients(self
):
1526 The `r` polynomial coefficients of the "characteristic polynomial
1531 sage: from mjo.eja.eja_algebra import random_eja
1535 The theory shows that these are all homogeneous polynomials of
1538 sage: set_random_seed()
1539 sage: J = random_eja()
1540 sage: all(p.is_homogeneous() for p in J._charpoly_coefficients())
1544 n
= self
.dimension()
1545 R
= self
.coordinate_polynomial_ring()
1547 F
= R
.fraction_field()
1550 # From a result in my book, these are the entries of the
1551 # basis representation of L_x.
1552 return sum( vars[k
]*self
.monomial(k
).operator().matrix()[i
,j
]
1555 L_x
= matrix(F
, n
, n
, L_x_i_j
)
1558 if self
.rank
.is_in_cache():
1560 # There's no need to pad the system with redundant
1561 # columns if we *know* they'll be redundant.
1564 # Compute an extra power in case the rank is equal to
1565 # the dimension (otherwise, we would stop at x^(r-1)).
1566 x_powers
= [ (L_x
**k
)*self
.one().to_vector()
1567 for k
in range(n
+1) ]
1568 A
= matrix
.column(F
, x_powers
[:n
])
1569 AE
= A
.extended_echelon_form()
1576 # The theory says that only the first "r" coefficients are
1577 # nonzero, and they actually live in the original polynomial
1578 # ring and not the fraction field. We negate them because in
1579 # the actual characteristic polynomial, they get moved to the
1580 # other side where x^r lives. We don't bother to trim A_rref
1581 # down to a square matrix and solve the resulting system,
1582 # because the upper-left r-by-r portion of A_rref is
1583 # guaranteed to be the identity matrix, so e.g.
1585 # A_rref.solve_right(Y)
1587 # would just be returning Y.
1588 return (-E
*b
)[:r
].change_ring(R
)
1593 Return the rank of this EJA.
1595 This is a cached method because we know the rank a priori for
1596 all of the algebras we can construct. Thus we can avoid the
1597 expensive ``_charpoly_coefficients()`` call unless we truly
1598 need to compute the whole characteristic polynomial.
1602 sage: from mjo.eja.eja_algebra import (HadamardEJA,
1603 ....: JordanSpinEJA,
1604 ....: RealSymmetricEJA,
1605 ....: ComplexHermitianEJA,
1606 ....: QuaternionHermitianEJA,
1611 The rank of the Jordan spin algebra is always two::
1613 sage: JordanSpinEJA(2).rank()
1615 sage: JordanSpinEJA(3).rank()
1617 sage: JordanSpinEJA(4).rank()
1620 The rank of the `n`-by-`n` Hermitian real, complex, or
1621 quaternion matrices is `n`::
1623 sage: RealSymmetricEJA(4).rank()
1625 sage: ComplexHermitianEJA(3).rank()
1627 sage: QuaternionHermitianEJA(2).rank()
1632 Ensure that every EJA that we know how to construct has a
1633 positive integer rank, unless the algebra is trivial in
1634 which case its rank will be zero::
1636 sage: set_random_seed()
1637 sage: J = random_eja()
1641 sage: r > 0 or (r == 0 and J.is_trivial())
1644 Ensure that computing the rank actually works, since the ranks
1645 of all simple algebras are known and will be cached by default::
1647 sage: set_random_seed() # long time
1648 sage: J = random_eja() # long time
1649 sage: cached = J.rank() # long time
1650 sage: J.rank.clear_cache() # long time
1651 sage: J.rank() == cached # long time
1655 return len(self
._charpoly
_coefficients
())
1658 def subalgebra(self
, basis
, **kwargs
):
1660 Create a subalgebra of this algebra from the given basis.
1662 from mjo
.eja
.eja_subalgebra
import FiniteDimensionalEJASubalgebra
1663 return FiniteDimensionalEJASubalgebra(self
, basis
, **kwargs
)
1666 def vector_space(self
):
1668 Return the vector space that underlies this algebra.
1672 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1676 sage: J = RealSymmetricEJA(2)
1677 sage: J.vector_space()
1678 Vector space of dimension 3 over...
1681 return self
.zero().to_vector().parent().ambient_vector_space()
1685 class RationalBasisEJA(FiniteDimensionalEJA
):
1687 Algebras whose supplied basis elements have all rational entries.
1691 sage: from mjo.eja.eja_algebra import BilinearFormEJA
1695 The supplied basis is orthonormalized by default::
1697 sage: B = matrix(QQ, [[1, 0, 0], [0, 25, -32], [0, -32, 41]])
1698 sage: J = BilinearFormEJA(B)
1699 sage: J.matrix_basis()
1716 # Abuse the check_field parameter to check that the entries of
1717 # out basis (in ambient coordinates) are in the field QQ.
1718 # Use _all2list to get the vector coordinates of octonion
1719 # entries and not the octonions themselves (which are not
1721 if not all( all(b_i
in QQ
for b_i
in _all2list(b
))
1723 raise TypeError("basis not rational")
1725 super().__init
__(basis
,
1729 check_field
=check_field
,
1732 self
._rational
_algebra
= None
1734 # There's no point in constructing the extra algebra if this
1735 # one is already rational.
1737 # Note: the same Jordan and inner-products work here,
1738 # because they are necessarily defined with respect to
1739 # ambient coordinates and not any particular basis.
1740 self
._rational
_algebra
= FiniteDimensionalEJA(
1745 matrix_space
=self
.matrix_space(),
1746 associative
=self
.is_associative(),
1747 orthonormalize
=False,
1752 def _charpoly_coefficients(self
):
1756 sage: from mjo.eja.eja_algebra import (BilinearFormEJA,
1757 ....: JordanSpinEJA)
1761 The base ring of the resulting polynomial coefficients is what
1762 it should be, and not the rationals (unless the algebra was
1763 already over the rationals)::
1765 sage: J = JordanSpinEJA(3)
1766 sage: J._charpoly_coefficients()
1767 (X1^2 - X2^2 - X3^2, -2*X1)
1768 sage: a0 = J._charpoly_coefficients()[0]
1770 Algebraic Real Field
1771 sage: a0.base_ring()
1772 Algebraic Real Field
1775 if self
._rational
_algebra
is None:
1776 # There's no need to construct *another* algebra over the
1777 # rationals if this one is already over the
1778 # rationals. Likewise, if we never orthonormalized our
1779 # basis, we might as well just use the given one.
1780 return super()._charpoly
_coefficients
()
1782 # Do the computation over the rationals. The answer will be
1783 # the same, because all we've done is a change of basis.
1784 # Then, change back from QQ to our real base ring
1785 a
= ( a_i
.change_ring(self
.base_ring())
1786 for a_i
in self
._rational
_algebra
._charpoly
_coefficients
() )
1788 # Otherwise, convert the coordinate variables back to the
1789 # deorthonormalized ones.
1790 R
= self
.coordinate_polynomial_ring()
1791 from sage
.modules
.free_module_element
import vector
1792 X
= vector(R
, R
.gens())
1793 BX
= self
._deortho
_matrix
*X
1795 subs_dict
= { X[i]: BX[i] for i in range(len(X)) }
1796 return tuple( a_i
.subs(subs_dict
) for a_i
in a
)
1798 class ConcreteEJA(FiniteDimensionalEJA
):
1800 A class for the Euclidean Jordan algebras that we know by name.
1802 These are the Jordan algebras whose basis, multiplication table,
1803 rank, and so on are known a priori. More to the point, they are
1804 the Euclidean Jordan algebras for which we are able to conjure up
1805 a "random instance."
1809 sage: from mjo.eja.eja_algebra import ConcreteEJA
1813 Our basis is normalized with respect to the algebra's inner
1814 product, unless we specify otherwise::
1816 sage: set_random_seed()
1817 sage: J = ConcreteEJA.random_instance()
1818 sage: all( b.norm() == 1 for b in J.gens() )
1821 Since our basis is orthonormal with respect to the algebra's inner
1822 product, and since we know that this algebra is an EJA, any
1823 left-multiplication operator's matrix will be symmetric because
1824 natural->EJA basis representation is an isometry and within the
1825 EJA the operator is self-adjoint by the Jordan axiom::
1827 sage: set_random_seed()
1828 sage: J = ConcreteEJA.random_instance()
1829 sage: x = J.random_element()
1830 sage: x.operator().is_self_adjoint()
1835 def _max_random_instance_dimension():
1837 The maximum dimension of any random instance. Ten dimensions seems
1838 to be about the point where everything takes a turn for the
1839 worse. And dimension ten (but not nine) allows the 4-by-4 real
1840 Hermitian matrices, the 2-by-2 quaternion Hermitian matrices,
1841 and the 2-by-2 octonion Hermitian matrices.
1846 def _max_random_instance_size(max_dimension
):
1848 Return an integer "size" that is an upper bound on the size of
1849 this algebra when it is used in a random test case. This size
1850 (which can be passed to the algebra's constructor) is itself
1851 based on the ``max_dimension`` parameter.
1853 This method must be implemented in each subclass.
1855 raise NotImplementedError
1858 def random_instance(cls
, max_dimension
=None, *args
, **kwargs
):
1860 Return a random instance of this type of algebra whose dimension
1861 is less than or equal to the lesser of ``max_dimension`` and
1862 the value returned by ``_max_random_instance_dimension()``. If
1863 the dimension bound is omitted, then only the
1864 ``_max_random_instance_dimension()`` is used as a bound.
1866 This method should be implemented in each subclass.
1870 sage: from mjo.eja.eja_algebra import ConcreteEJA
1874 Both the class bound and the ``max_dimension`` argument are upper
1875 bounds on the dimension of the algebra returned::
1877 sage: from sage.misc.prandom import choice
1878 sage: eja_class = choice(ConcreteEJA.__subclasses__())
1879 sage: class_max_d = eja_class._max_random_instance_dimension()
1880 sage: J = eja_class.random_instance(max_dimension=20,
1882 ....: orthonormalize=False)
1883 sage: J.dimension() <= class_max_d
1885 sage: J = eja_class.random_instance(max_dimension=2,
1887 ....: orthonormalize=False)
1888 sage: J.dimension() <= 2
1892 from sage
.misc
.prandom
import choice
1893 eja_class
= choice(cls
.__subclasses
__())
1895 # These all bubble up to the RationalBasisEJA superclass
1896 # constructor, so any (kw)args valid there are also valid
1898 return eja_class
.random_instance(max_dimension
, *args
, **kwargs
)
1901 class MatrixEJA(FiniteDimensionalEJA
):
1903 def _denormalized_basis(A
):
1905 Returns a basis for the space of complex Hermitian n-by-n matrices.
1907 Why do we embed these? Basically, because all of numerical linear
1908 algebra assumes that you're working with vectors consisting of `n`
1909 entries from a field and scalars from the same field. There's no way
1910 to tell SageMath that (for example) the vectors contain complex
1911 numbers, while the scalar field is real.
1915 sage: from mjo.hurwitz import (ComplexMatrixAlgebra,
1916 ....: QuaternionMatrixAlgebra,
1917 ....: OctonionMatrixAlgebra)
1918 sage: from mjo.eja.eja_algebra import MatrixEJA
1922 sage: set_random_seed()
1923 sage: n = ZZ.random_element(1,5)
1924 sage: A = MatrixSpace(QQ, n)
1925 sage: B = MatrixEJA._denormalized_basis(A)
1926 sage: all( M.is_hermitian() for M in B)
1931 sage: set_random_seed()
1932 sage: n = ZZ.random_element(1,5)
1933 sage: A = ComplexMatrixAlgebra(n, scalars=QQ)
1934 sage: B = MatrixEJA._denormalized_basis(A)
1935 sage: all( M.is_hermitian() for M in B)
1940 sage: set_random_seed()
1941 sage: n = ZZ.random_element(1,5)
1942 sage: A = QuaternionMatrixAlgebra(n, scalars=QQ)
1943 sage: B = MatrixEJA._denormalized_basis(A)
1944 sage: all( M.is_hermitian() for M in B )
1949 sage: set_random_seed()
1950 sage: n = ZZ.random_element(1,5)
1951 sage: A = OctonionMatrixAlgebra(n, scalars=QQ)
1952 sage: B = MatrixEJA._denormalized_basis(A)
1953 sage: all( M.is_hermitian() for M in B )
1957 # These work for real MatrixSpace, whose monomials only have
1958 # two coordinates (because the last one would always be "1").
1959 es
= A
.base_ring().gens()
1960 gen
= lambda A
,m
: A
.monomial(m
[:2])
1962 if hasattr(A
, 'entry_algebra_gens'):
1963 # We've got a MatrixAlgebra, and its monomials will have
1964 # three coordinates.
1965 es
= A
.entry_algebra_gens()
1966 gen
= lambda A
,m
: A
.monomial(m
)
1969 for i
in range(A
.nrows()):
1970 for j
in range(i
+1):
1972 E_ii
= gen(A
, (i
,j
,es
[0]))
1976 E_ij
= gen(A
, (i
,j
,e
))
1977 E_ij
+= E_ij
.conjugate_transpose()
1980 return tuple( basis
)
1983 def jordan_product(X
,Y
):
1984 return (X
*Y
+ Y
*X
)/2
1987 def trace_inner_product(X
,Y
):
1989 A trace inner-product for matrices that aren't embedded in the
1990 reals. It takes MATRICES as arguments, not EJA elements.
1994 sage: from mjo.eja.eja_algebra import (RealSymmetricEJA,
1995 ....: ComplexHermitianEJA,
1996 ....: QuaternionHermitianEJA,
1997 ....: OctonionHermitianEJA)
2001 sage: J = RealSymmetricEJA(2,field=QQ,orthonormalize=False)
2002 sage: I = J.one().to_matrix()
2003 sage: J.trace_inner_product(I, -I)
2008 sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
2009 sage: I = J.one().to_matrix()
2010 sage: J.trace_inner_product(I, -I)
2015 sage: J = QuaternionHermitianEJA(2,field=QQ,orthonormalize=False)
2016 sage: I = J.one().to_matrix()
2017 sage: J.trace_inner_product(I, -I)
2022 sage: J = OctonionHermitianEJA(2,field=QQ,orthonormalize=False)
2023 sage: I = J.one().to_matrix()
2024 sage: J.trace_inner_product(I, -I)
2029 if hasattr(tr
, 'coefficient'):
2030 # Works for octonions, and has to come first because they
2031 # also have a "real()" method that doesn't return an
2032 # element of the scalar ring.
2033 return tr
.coefficient(0)
2034 elif hasattr(tr
, 'coefficient_tuple'):
2035 # Works for quaternions.
2036 return tr
.coefficient_tuple()[0]
2038 # Works for real and complex numbers.
2042 def __init__(self
, matrix_space
, **kwargs
):
2043 # We know this is a valid EJA, but will double-check
2044 # if the user passes check_axioms=True.
2045 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2048 super().__init
__(self
._denormalized
_basis
(matrix_space
),
2049 self
.jordan_product
,
2050 self
.trace_inner_product
,
2051 field
=matrix_space
.base_ring(),
2052 matrix_space
=matrix_space
,
2055 self
.rank
.set_cache(matrix_space
.nrows())
2056 self
.one
.set_cache( self(matrix_space
.one()) )
2058 class RealSymmetricEJA(MatrixEJA
, RationalBasisEJA
, ConcreteEJA
):
2060 The rank-n simple EJA consisting of real symmetric n-by-n
2061 matrices, the usual symmetric Jordan product, and the trace inner
2062 product. It has dimension `(n^2 + n)/2` over the reals.
2066 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
2070 sage: J = RealSymmetricEJA(2)
2071 sage: b0, b1, b2 = J.gens()
2079 In theory, our "field" can be any subfield of the reals::
2081 sage: RealSymmetricEJA(2, field=RDF, check_axioms=True)
2082 Euclidean Jordan algebra of dimension 3 over Real Double Field
2083 sage: RealSymmetricEJA(2, field=RR, check_axioms=True)
2084 Euclidean Jordan algebra of dimension 3 over Real Field with
2085 53 bits of precision
2089 The dimension of this algebra is `(n^2 + n) / 2`::
2091 sage: set_random_seed()
2092 sage: d = RealSymmetricEJA._max_random_instance_dimension()
2093 sage: n = RealSymmetricEJA._max_random_instance_size(d)
2094 sage: J = RealSymmetricEJA(n)
2095 sage: J.dimension() == (n^2 + n)/2
2098 The Jordan multiplication is what we think it is::
2100 sage: set_random_seed()
2101 sage: J = RealSymmetricEJA.random_instance()
2102 sage: x,y = J.random_elements(2)
2103 sage: actual = (x*y).to_matrix()
2104 sage: X = x.to_matrix()
2105 sage: Y = y.to_matrix()
2106 sage: expected = (X*Y + Y*X)/2
2107 sage: actual == expected
2109 sage: J(expected) == x*y
2112 We can change the generator prefix::
2114 sage: RealSymmetricEJA(3, prefix='q').gens()
2115 (q0, q1, q2, q3, q4, q5)
2117 We can construct the (trivial) algebra of rank zero::
2119 sage: RealSymmetricEJA(0)
2120 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2124 def _max_random_instance_size(max_dimension
):
2125 # Obtained by solving d = (n^2 + n)/2.
2126 # The ZZ-int-ZZ thing is just "floor."
2127 return ZZ(int(ZZ(8*max_dimension
+ 1).sqrt()/2 - 1/2))
2130 def random_instance(cls
, max_dimension
=None, *args
, **kwargs
):
2132 Return a random instance of this type of algebra.
2134 class_max_d
= cls
._max
_random
_instance
_dimension
()
2135 if (max_dimension
is None or max_dimension
> class_max_d
):
2136 max_dimension
= class_max_d
2137 max_size
= cls
._max
_random
_instance
_size
(max_dimension
)
2138 n
= ZZ
.random_element(max_size
+ 1)
2139 return cls(n
, **kwargs
)
2141 def __init__(self
, n
, field
=AA
, **kwargs
):
2142 # We know this is a valid EJA, but will double-check
2143 # if the user passes check_axioms=True.
2144 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2146 A
= MatrixSpace(field
, n
)
2147 super().__init
__(A
, **kwargs
)
2149 from mjo
.eja
.eja_cache
import real_symmetric_eja_coeffs
2150 a
= real_symmetric_eja_coeffs(self
)
2152 if self
._rational
_algebra
is None:
2153 self
._charpoly
_coefficients
.set_cache(a
)
2155 self
._rational
_algebra
._charpoly
_coefficients
.set_cache(a
)
2159 class ComplexHermitianEJA(MatrixEJA
, RationalBasisEJA
, ConcreteEJA
):
2161 The rank-n simple EJA consisting of complex Hermitian n-by-n
2162 matrices over the real numbers, the usual symmetric Jordan product,
2163 and the real-part-of-trace inner product. It has dimension `n^2` over
2168 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
2172 In theory, our "field" can be any subfield of the reals, but we
2173 can't use inexact real fields at the moment because SageMath
2174 doesn't know how to convert their elements into complex numbers,
2175 or even into algebraic reals::
2178 Traceback (most recent call last):
2180 TypeError: Illegal initializer for algebraic number
2182 Traceback (most recent call last):
2184 TypeError: Illegal initializer for algebraic number
2186 This causes the following error when we try to scale a matrix of
2187 complex numbers by an inexact real number::
2189 sage: ComplexHermitianEJA(2,field=RR)
2190 Traceback (most recent call last):
2192 TypeError: Unable to coerce entries (=(1.00000000000000,
2193 -0.000000000000000)) to coefficients in Algebraic Real Field
2197 The dimension of this algebra is `n^2`::
2199 sage: set_random_seed()
2200 sage: d = ComplexHermitianEJA._max_random_instance_dimension()
2201 sage: n = ComplexHermitianEJA._max_random_instance_size(d)
2202 sage: J = ComplexHermitianEJA(n)
2203 sage: J.dimension() == n^2
2206 The Jordan multiplication is what we think it is::
2208 sage: set_random_seed()
2209 sage: J = ComplexHermitianEJA.random_instance()
2210 sage: x,y = J.random_elements(2)
2211 sage: actual = (x*y).to_matrix()
2212 sage: X = x.to_matrix()
2213 sage: Y = y.to_matrix()
2214 sage: expected = (X*Y + Y*X)/2
2215 sage: actual == expected
2217 sage: J(expected) == x*y
2220 We can change the generator prefix::
2222 sage: ComplexHermitianEJA(2, prefix='z').gens()
2225 We can construct the (trivial) algebra of rank zero::
2227 sage: ComplexHermitianEJA(0)
2228 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2231 def __init__(self
, n
, field
=AA
, **kwargs
):
2232 # We know this is a valid EJA, but will double-check
2233 # if the user passes check_axioms=True.
2234 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2236 from mjo
.hurwitz
import ComplexMatrixAlgebra
2237 A
= ComplexMatrixAlgebra(n
, scalars
=field
)
2238 super().__init
__(A
, **kwargs
)
2240 from mjo
.eja
.eja_cache
import complex_hermitian_eja_coeffs
2241 a
= complex_hermitian_eja_coeffs(self
)
2243 if self
._rational
_algebra
is None:
2244 self
._charpoly
_coefficients
.set_cache(a
)
2246 self
._rational
_algebra
._charpoly
_coefficients
.set_cache(a
)
2249 def _max_random_instance_size(max_dimension
):
2250 # Obtained by solving d = n^2.
2251 # The ZZ-int-ZZ thing is just "floor."
2252 return ZZ(int(ZZ(max_dimension
).sqrt()))
2255 def random_instance(cls
, max_dimension
=None, *args
, **kwargs
):
2257 Return a random instance of this type of algebra.
2259 class_max_d
= cls
._max
_random
_instance
_dimension
()
2260 if (max_dimension
is None or max_dimension
> class_max_d
):
2261 max_dimension
= class_max_d
2262 max_size
= cls
._max
_random
_instance
_size
(max_dimension
)
2263 n
= ZZ
.random_element(max_size
+ 1)
2264 return cls(n
, **kwargs
)
2267 class QuaternionHermitianEJA(MatrixEJA
, RationalBasisEJA
, ConcreteEJA
):
2269 The rank-n simple EJA consisting of self-adjoint n-by-n quaternion
2270 matrices, the usual symmetric Jordan product, and the
2271 real-part-of-trace inner product. It has dimension `2n^2 - n` over
2276 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
2280 In theory, our "field" can be any subfield of the reals::
2282 sage: QuaternionHermitianEJA(2, field=RDF, check_axioms=True)
2283 Euclidean Jordan algebra of dimension 6 over Real Double Field
2284 sage: QuaternionHermitianEJA(2, field=RR, check_axioms=True)
2285 Euclidean Jordan algebra of dimension 6 over Real Field with
2286 53 bits of precision
2290 The dimension of this algebra is `2*n^2 - n`::
2292 sage: set_random_seed()
2293 sage: d = QuaternionHermitianEJA._max_random_instance_dimension()
2294 sage: n = QuaternionHermitianEJA._max_random_instance_size(d)
2295 sage: J = QuaternionHermitianEJA(n)
2296 sage: J.dimension() == 2*(n^2) - n
2299 The Jordan multiplication is what we think it is::
2301 sage: set_random_seed()
2302 sage: J = QuaternionHermitianEJA.random_instance()
2303 sage: x,y = J.random_elements(2)
2304 sage: actual = (x*y).to_matrix()
2305 sage: X = x.to_matrix()
2306 sage: Y = y.to_matrix()
2307 sage: expected = (X*Y + Y*X)/2
2308 sage: actual == expected
2310 sage: J(expected) == x*y
2313 We can change the generator prefix::
2315 sage: QuaternionHermitianEJA(2, prefix='a').gens()
2316 (a0, a1, a2, a3, a4, a5)
2318 We can construct the (trivial) algebra of rank zero::
2320 sage: QuaternionHermitianEJA(0)
2321 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2324 def __init__(self
, n
, field
=AA
, **kwargs
):
2325 # We know this is a valid EJA, but will double-check
2326 # if the user passes check_axioms=True.
2327 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2329 from mjo
.hurwitz
import QuaternionMatrixAlgebra
2330 A
= QuaternionMatrixAlgebra(n
, scalars
=field
)
2331 super().__init
__(A
, **kwargs
)
2333 from mjo
.eja
.eja_cache
import quaternion_hermitian_eja_coeffs
2334 a
= quaternion_hermitian_eja_coeffs(self
)
2336 if self
._rational
_algebra
is None:
2337 self
._charpoly
_coefficients
.set_cache(a
)
2339 self
._rational
_algebra
._charpoly
_coefficients
.set_cache(a
)
2344 def _max_random_instance_size(max_dimension
):
2346 The maximum rank of a random QuaternionHermitianEJA.
2348 # Obtained by solving d = 2n^2 - n.
2349 # The ZZ-int-ZZ thing is just "floor."
2350 return ZZ(int(ZZ(8*max_dimension
+ 1).sqrt()/4 + 1/4))
2353 def random_instance(cls
, max_dimension
=None, *args
, **kwargs
):
2355 Return a random instance of this type of algebra.
2357 class_max_d
= cls
._max
_random
_instance
_dimension
()
2358 if (max_dimension
is None or max_dimension
> class_max_d
):
2359 max_dimension
= class_max_d
2360 max_size
= cls
._max
_random
_instance
_size
(max_dimension
)
2361 n
= ZZ
.random_element(max_size
+ 1)
2362 return cls(n
, **kwargs
)
2364 class OctonionHermitianEJA(MatrixEJA
, RationalBasisEJA
, ConcreteEJA
):
2368 sage: from mjo.eja.eja_algebra import (FiniteDimensionalEJA,
2369 ....: OctonionHermitianEJA)
2370 sage: from mjo.hurwitz import Octonions, OctonionMatrixAlgebra
2374 The 3-by-3 algebra satisfies the axioms of an EJA::
2376 sage: OctonionHermitianEJA(3, # long time
2377 ....: field=QQ, # long time
2378 ....: orthonormalize=False, # long time
2379 ....: check_axioms=True) # long time
2380 Euclidean Jordan algebra of dimension 27 over Rational Field
2382 After a change-of-basis, the 2-by-2 algebra has the same
2383 multiplication table as the ten-dimensional Jordan spin algebra::
2385 sage: A = OctonionMatrixAlgebra(2,Octonions(QQ),QQ)
2386 sage: b = OctonionHermitianEJA._denormalized_basis(A)
2387 sage: basis = (b[0] + b[9],) + b[1:9] + (b[0] - b[9],)
2388 sage: jp = OctonionHermitianEJA.jordan_product
2389 sage: ip = OctonionHermitianEJA.trace_inner_product
2390 sage: J = FiniteDimensionalEJA(basis,
2394 ....: orthonormalize=False)
2395 sage: J.multiplication_table()
2396 +----++----+----+----+----+----+----+----+----+----+----+
2397 | * || b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7 | b8 | b9 |
2398 +====++====+====+====+====+====+====+====+====+====+====+
2399 | b0 || b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7 | b8 | b9 |
2400 +----++----+----+----+----+----+----+----+----+----+----+
2401 | b1 || b1 | b0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2402 +----++----+----+----+----+----+----+----+----+----+----+
2403 | b2 || b2 | 0 | b0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2404 +----++----+----+----+----+----+----+----+----+----+----+
2405 | b3 || b3 | 0 | 0 | b0 | 0 | 0 | 0 | 0 | 0 | 0 |
2406 +----++----+----+----+----+----+----+----+----+----+----+
2407 | b4 || b4 | 0 | 0 | 0 | b0 | 0 | 0 | 0 | 0 | 0 |
2408 +----++----+----+----+----+----+----+----+----+----+----+
2409 | b5 || b5 | 0 | 0 | 0 | 0 | b0 | 0 | 0 | 0 | 0 |
2410 +----++----+----+----+----+----+----+----+----+----+----+
2411 | b6 || b6 | 0 | 0 | 0 | 0 | 0 | b0 | 0 | 0 | 0 |
2412 +----++----+----+----+----+----+----+----+----+----+----+
2413 | b7 || b7 | 0 | 0 | 0 | 0 | 0 | 0 | b0 | 0 | 0 |
2414 +----++----+----+----+----+----+----+----+----+----+----+
2415 | b8 || b8 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | b0 | 0 |
2416 +----++----+----+----+----+----+----+----+----+----+----+
2417 | b9 || b9 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | b0 |
2418 +----++----+----+----+----+----+----+----+----+----+----+
2422 We can actually construct the 27-dimensional Albert algebra,
2423 and we get the right unit element if we recompute it::
2425 sage: J = OctonionHermitianEJA(3, # long time
2426 ....: field=QQ, # long time
2427 ....: orthonormalize=False) # long time
2428 sage: J.one.clear_cache() # long time
2429 sage: J.one() # long time
2431 sage: J.one().to_matrix() # long time
2440 The 2-by-2 algebra is isomorphic to the ten-dimensional Jordan
2441 spin algebra, but just to be sure, we recompute its rank::
2443 sage: J = OctonionHermitianEJA(2, # long time
2444 ....: field=QQ, # long time
2445 ....: orthonormalize=False) # long time
2446 sage: J.rank.clear_cache() # long time
2447 sage: J.rank() # long time
2452 def _max_random_instance_size(max_dimension
):
2454 The maximum rank of a random QuaternionHermitianEJA.
2456 # There's certainly a formula for this, but with only four
2457 # cases to worry about, I'm not that motivated to derive it.
2458 if max_dimension
>= 27:
2460 elif max_dimension
>= 10:
2462 elif max_dimension
>= 1:
2468 def random_instance(cls
, max_dimension
=None, *args
, **kwargs
):
2470 Return a random instance of this type of algebra.
2472 class_max_d
= cls
._max
_random
_instance
_dimension
()
2473 if (max_dimension
is None or max_dimension
> class_max_d
):
2474 max_dimension
= class_max_d
2475 max_size
= cls
._max
_random
_instance
_size
(max_dimension
)
2476 n
= ZZ
.random_element(max_size
+ 1)
2477 return cls(n
, **kwargs
)
2479 def __init__(self
, n
, field
=AA
, **kwargs
):
2481 # Otherwise we don't get an EJA.
2482 raise ValueError("n cannot exceed 3")
2484 # We know this is a valid EJA, but will double-check
2485 # if the user passes check_axioms=True.
2486 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2488 from mjo
.hurwitz
import OctonionMatrixAlgebra
2489 A
= OctonionMatrixAlgebra(n
, scalars
=field
)
2490 super().__init
__(A
, **kwargs
)
2492 from mjo
.eja
.eja_cache
import octonion_hermitian_eja_coeffs
2493 a
= octonion_hermitian_eja_coeffs(self
)
2495 if self
._rational
_algebra
is None:
2496 self
._charpoly
_coefficients
.set_cache(a
)
2498 self
._rational
_algebra
._charpoly
_coefficients
.set_cache(a
)
2501 class AlbertEJA(OctonionHermitianEJA
):
2503 The Albert algebra is the algebra of three-by-three Hermitian
2504 matrices whose entries are octonions.
2508 sage: from mjo.eja.eja_algebra import AlbertEJA
2512 sage: AlbertEJA(field=QQ, orthonormalize=False)
2513 Euclidean Jordan algebra of dimension 27 over Rational Field
2514 sage: AlbertEJA() # long time
2515 Euclidean Jordan algebra of dimension 27 over Algebraic Real Field
2518 def __init__(self
, *args
, **kwargs
):
2519 super().__init
__(3, *args
, **kwargs
)
2522 class HadamardEJA(RationalBasisEJA
, ConcreteEJA
):
2524 Return the Euclidean Jordan algebra on `R^n` with the Hadamard
2525 (pointwise real-number multiplication) Jordan product and the
2526 usual inner-product.
2528 This is nothing more than the Cartesian product of ``n`` copies of
2529 the one-dimensional Jordan spin algebra, and is the most common
2530 example of a non-simple Euclidean Jordan algebra.
2534 sage: from mjo.eja.eja_algebra import HadamardEJA
2538 This multiplication table can be verified by hand::
2540 sage: J = HadamardEJA(3)
2541 sage: b0,b1,b2 = J.gens()
2557 We can change the generator prefix::
2559 sage: HadamardEJA(3, prefix='r').gens()
2562 def __init__(self
, n
, field
=AA
, **kwargs
):
2563 MS
= MatrixSpace(field
, n
, 1)
2566 jordan_product
= lambda x
,y
: x
2567 inner_product
= lambda x
,y
: x
2569 def jordan_product(x
,y
):
2570 return MS( xi
*yi
for (xi
,yi
) in zip(x
,y
) )
2572 def inner_product(x
,y
):
2575 # New defaults for keyword arguments. Don't orthonormalize
2576 # because our basis is already orthonormal with respect to our
2577 # inner-product. Don't check the axioms, because we know this
2578 # is a valid EJA... but do double-check if the user passes
2579 # check_axioms=True. Note: we DON'T override the "check_field"
2580 # default here, because the user can pass in a field!
2581 if "orthonormalize" not in kwargs
: kwargs
["orthonormalize"] = False
2582 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2584 column_basis
= tuple( MS(b
) for b
in FreeModule(field
, n
).basis() )
2585 super().__init
__(column_basis
,
2592 self
.rank
.set_cache(n
)
2594 self
.one
.set_cache( self
.sum(self
.gens()) )
2597 def _max_random_instance_dimension():
2599 There's no reason to go higher than five here. That's
2600 enough to get the point across.
2605 def _max_random_instance_size(max_dimension
):
2607 The maximum size (=dimension) of a random HadamardEJA.
2609 return max_dimension
2612 def random_instance(cls
, max_dimension
=None, *args
, **kwargs
):
2614 Return a random instance of this type of algebra.
2616 class_max_d
= cls
._max
_random
_instance
_dimension
()
2617 if (max_dimension
is None or max_dimension
> class_max_d
):
2618 max_dimension
= class_max_d
2619 max_size
= cls
._max
_random
_instance
_size
(max_dimension
)
2620 n
= ZZ
.random_element(max_size
+ 1)
2621 return cls(n
, **kwargs
)
2624 class BilinearFormEJA(RationalBasisEJA
, ConcreteEJA
):
2626 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
2627 with the half-trace inner product and jordan product ``x*y =
2628 (<Bx,y>,y_bar>, x0*y_bar + y0*x_bar)`` where `B = 1 \times B22` is
2629 a symmetric positive-definite "bilinear form" matrix. Its
2630 dimension is the size of `B`, and it has rank two in dimensions
2631 larger than two. It reduces to the ``JordanSpinEJA`` when `B` is
2632 the identity matrix of order ``n``.
2634 We insist that the one-by-one upper-left identity block of `B` be
2635 passed in as well so that we can be passed a matrix of size zero
2636 to construct a trivial algebra.
2640 sage: from mjo.eja.eja_algebra import (BilinearFormEJA,
2641 ....: JordanSpinEJA)
2645 When no bilinear form is specified, the identity matrix is used,
2646 and the resulting algebra is the Jordan spin algebra::
2648 sage: B = matrix.identity(AA,3)
2649 sage: J0 = BilinearFormEJA(B)
2650 sage: J1 = JordanSpinEJA(3)
2651 sage: J0.multiplication_table() == J0.multiplication_table()
2654 An error is raised if the matrix `B` does not correspond to a
2655 positive-definite bilinear form::
2657 sage: B = matrix.random(QQ,2,3)
2658 sage: J = BilinearFormEJA(B)
2659 Traceback (most recent call last):
2661 ValueError: bilinear form is not positive-definite
2662 sage: B = matrix.zero(QQ,3)
2663 sage: J = BilinearFormEJA(B)
2664 Traceback (most recent call last):
2666 ValueError: bilinear form is not positive-definite
2670 We can create a zero-dimensional algebra::
2672 sage: B = matrix.identity(AA,0)
2673 sage: J = BilinearFormEJA(B)
2677 We can check the multiplication condition given in the Jordan, von
2678 Neumann, and Wigner paper (and also discussed on my "On the
2679 symmetry..." paper). Note that this relies heavily on the standard
2680 choice of basis, as does anything utilizing the bilinear form
2681 matrix. We opt not to orthonormalize the basis, because if we
2682 did, we would have to normalize the `s_{i}` in a similar manner::
2684 sage: set_random_seed()
2685 sage: n = ZZ.random_element(5)
2686 sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular')
2687 sage: B11 = matrix.identity(QQ,1)
2688 sage: B22 = M.transpose()*M
2689 sage: B = block_matrix(2,2,[ [B11,0 ],
2691 sage: J = BilinearFormEJA(B, orthonormalize=False)
2692 sage: eis = VectorSpace(M.base_ring(), M.ncols()).basis()
2693 sage: V = J.vector_space()
2694 sage: sis = [ J( V([0] + (M.inverse()*ei).list()).column() )
2695 ....: for ei in eis ]
2696 sage: actual = [ sis[i]*sis[j]
2697 ....: for i in range(n-1)
2698 ....: for j in range(n-1) ]
2699 sage: expected = [ J.one() if i == j else J.zero()
2700 ....: for i in range(n-1)
2701 ....: for j in range(n-1) ]
2702 sage: actual == expected
2706 def __init__(self
, B
, field
=AA
, **kwargs
):
2707 # The matrix "B" is supplied by the user in most cases,
2708 # so it makes sense to check whether or not its positive-
2709 # definite unless we are specifically asked not to...
2710 if ("check_axioms" not in kwargs
) or kwargs
["check_axioms"]:
2711 if not B
.is_positive_definite():
2712 raise ValueError("bilinear form is not positive-definite")
2714 # However, all of the other data for this EJA is computed
2715 # by us in manner that guarantees the axioms are
2716 # satisfied. So, again, unless we are specifically asked to
2717 # verify things, we'll skip the rest of the checks.
2718 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2721 MS
= MatrixSpace(field
, n
, 1)
2723 def inner_product(x
,y
):
2724 return (y
.T
*B
*x
)[0,0]
2726 def jordan_product(x
,y
):
2731 z0
= inner_product(y
,x
)
2732 zbar
= y0
*xbar
+ x0
*ybar
2733 return MS([z0
] + zbar
.list())
2735 column_basis
= tuple( MS(b
) for b
in FreeModule(field
, n
).basis() )
2737 # TODO: I haven't actually checked this, but it seems legit.
2742 super().__init
__(column_basis
,
2747 associative
=associative
,
2750 # The rank of this algebra is two, unless we're in a
2751 # one-dimensional ambient space (because the rank is bounded
2752 # by the ambient dimension).
2753 self
.rank
.set_cache(min(n
,2))
2755 self
.one
.set_cache( self
.zero() )
2757 self
.one
.set_cache( self
.monomial(0) )
2760 def _max_random_instance_dimension():
2762 There's no reason to go higher than five here. That's
2763 enough to get the point across.
2768 def _max_random_instance_size(max_dimension
):
2770 The maximum size (=dimension) of a random BilinearFormEJA.
2772 return max_dimension
2775 def random_instance(cls
, max_dimension
=None, *args
, **kwargs
):
2777 Return a random instance of this algebra.
2779 class_max_d
= cls
._max
_random
_instance
_dimension
()
2780 if (max_dimension
is None or max_dimension
> class_max_d
):
2781 max_dimension
= class_max_d
2782 max_size
= cls
._max
_random
_instance
_size
(max_dimension
)
2783 n
= ZZ
.random_element(max_size
+ 1)
2786 B
= matrix
.identity(ZZ
, n
)
2787 return cls(B
, **kwargs
)
2789 B11
= matrix
.identity(ZZ
, 1)
2790 M
= matrix
.random(ZZ
, n
-1)
2791 I
= matrix
.identity(ZZ
, n
-1)
2793 while alpha
.is_zero():
2794 alpha
= ZZ
.random_element().abs()
2796 B22
= M
.transpose()*M
+ alpha
*I
2798 from sage
.matrix
.special
import block_matrix
2799 B
= block_matrix(2,2, [ [B11
, ZZ(0) ],
2802 return cls(B
, **kwargs
)
2805 class JordanSpinEJA(BilinearFormEJA
):
2807 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
2808 with the usual inner product and jordan product ``x*y =
2809 (<x,y>, x0*y_bar + y0*x_bar)``. It has dimension `n` over
2814 sage: from mjo.eja.eja_algebra import JordanSpinEJA
2818 This multiplication table can be verified by hand::
2820 sage: J = JordanSpinEJA(4)
2821 sage: b0,b1,b2,b3 = J.gens()
2837 We can change the generator prefix::
2839 sage: JordanSpinEJA(2, prefix='B').gens()
2844 Ensure that we have the usual inner product on `R^n`::
2846 sage: set_random_seed()
2847 sage: J = JordanSpinEJA.random_instance()
2848 sage: x,y = J.random_elements(2)
2849 sage: actual = x.inner_product(y)
2850 sage: expected = x.to_vector().inner_product(y.to_vector())
2851 sage: actual == expected
2855 def __init__(self
, n
, *args
, **kwargs
):
2856 # This is a special case of the BilinearFormEJA with the
2857 # identity matrix as its bilinear form.
2858 B
= matrix
.identity(ZZ
, n
)
2860 # Don't orthonormalize because our basis is already
2861 # orthonormal with respect to our inner-product.
2862 if "orthonormalize" not in kwargs
: kwargs
["orthonormalize"] = False
2864 # But also don't pass check_field=False here, because the user
2865 # can pass in a field!
2866 super().__init
__(B
, *args
, **kwargs
)
2869 def random_instance(cls
, max_dimension
=None, *args
, **kwargs
):
2871 Return a random instance of this type of algebra.
2873 Needed here to override the implementation for ``BilinearFormEJA``.
2875 class_max_d
= cls
._max
_random
_instance
_dimension
()
2876 if (max_dimension
is None or max_dimension
> class_max_d
):
2877 max_dimension
= class_max_d
2878 max_size
= cls
._max
_random
_instance
_size
(max_dimension
)
2879 n
= ZZ
.random_element(max_size
+ 1)
2880 return cls(n
, **kwargs
)
2883 class TrivialEJA(RationalBasisEJA
, ConcreteEJA
):
2885 The trivial Euclidean Jordan algebra consisting of only a zero element.
2889 sage: from mjo.eja.eja_algebra import TrivialEJA
2893 sage: J = TrivialEJA()
2900 sage: 7*J.one()*12*J.one()
2902 sage: J.one().inner_product(J.one())
2904 sage: J.one().norm()
2906 sage: J.one().subalgebra_generated_by()
2907 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2912 def __init__(self
, field
=AA
, **kwargs
):
2913 jordan_product
= lambda x
,y
: x
2914 inner_product
= lambda x
,y
: field
.zero()
2916 MS
= MatrixSpace(field
,0)
2918 # New defaults for keyword arguments
2919 if "orthonormalize" not in kwargs
: kwargs
["orthonormalize"] = False
2920 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2922 super().__init
__(basis
,
2930 # The rank is zero using my definition, namely the dimension of the
2931 # largest subalgebra generated by any element.
2932 self
.rank
.set_cache(0)
2933 self
.one
.set_cache( self
.zero() )
2936 def random_instance(cls
, max_dimension
=None, *args
, **kwargs
):
2937 # We don't take a "size" argument so the superclass method is
2938 # inappropriate for us. The ``max_dimension`` argument is
2939 # included so that if this method is called generically with a
2940 # ``max_dimension=<whatever>`` argument, we don't try to pass
2941 # it on to the algebra constructor.
2942 return cls(**kwargs
)
2945 class CartesianProductEJA(FiniteDimensionalEJA
):
2947 The external (orthogonal) direct sum of two or more Euclidean
2948 Jordan algebras. Every Euclidean Jordan algebra decomposes into an
2949 orthogonal direct sum of simple Euclidean Jordan algebras which is
2950 then isometric to a Cartesian product, so no generality is lost by
2951 providing only this construction.
2955 sage: from mjo.eja.eja_algebra import (random_eja,
2956 ....: CartesianProductEJA,
2958 ....: JordanSpinEJA,
2959 ....: RealSymmetricEJA)
2963 The Jordan product is inherited from our factors and implemented by
2964 our CombinatorialFreeModule Cartesian product superclass::
2966 sage: set_random_seed()
2967 sage: J1 = HadamardEJA(2)
2968 sage: J2 = RealSymmetricEJA(2)
2969 sage: J = cartesian_product([J1,J2])
2970 sage: x,y = J.random_elements(2)
2974 The ability to retrieve the original factors is implemented by our
2975 CombinatorialFreeModule Cartesian product superclass::
2977 sage: J1 = HadamardEJA(2, field=QQ)
2978 sage: J2 = JordanSpinEJA(3, field=QQ)
2979 sage: J = cartesian_product([J1,J2])
2980 sage: J.cartesian_factors()
2981 (Euclidean Jordan algebra of dimension 2 over Rational Field,
2982 Euclidean Jordan algebra of dimension 3 over Rational Field)
2984 You can provide more than two factors::
2986 sage: J1 = HadamardEJA(2)
2987 sage: J2 = JordanSpinEJA(3)
2988 sage: J3 = RealSymmetricEJA(3)
2989 sage: cartesian_product([J1,J2,J3])
2990 Euclidean Jordan algebra of dimension 2 over Algebraic Real
2991 Field (+) Euclidean Jordan algebra of dimension 3 over Algebraic
2992 Real Field (+) Euclidean Jordan algebra of dimension 6 over
2993 Algebraic Real Field
2995 Rank is additive on a Cartesian product::
2997 sage: J1 = HadamardEJA(1)
2998 sage: J2 = RealSymmetricEJA(2)
2999 sage: J = cartesian_product([J1,J2])
3000 sage: J1.rank.clear_cache()
3001 sage: J2.rank.clear_cache()
3002 sage: J.rank.clear_cache()
3005 sage: J.rank() == J1.rank() + J2.rank()
3008 The same rank computation works over the rationals, with whatever
3011 sage: J1 = HadamardEJA(1, field=QQ, orthonormalize=False)
3012 sage: J2 = RealSymmetricEJA(2, field=QQ, orthonormalize=False)
3013 sage: J = cartesian_product([J1,J2])
3014 sage: J1.rank.clear_cache()
3015 sage: J2.rank.clear_cache()
3016 sage: J.rank.clear_cache()
3019 sage: J.rank() == J1.rank() + J2.rank()
3022 The product algebra will be associative if and only if all of its
3023 components are associative::
3025 sage: J1 = HadamardEJA(2)
3026 sage: J1.is_associative()
3028 sage: J2 = HadamardEJA(3)
3029 sage: J2.is_associative()
3031 sage: J3 = RealSymmetricEJA(3)
3032 sage: J3.is_associative()
3034 sage: CP1 = cartesian_product([J1,J2])
3035 sage: CP1.is_associative()
3037 sage: CP2 = cartesian_product([J1,J3])
3038 sage: CP2.is_associative()
3041 Cartesian products of Cartesian products work::
3043 sage: J1 = JordanSpinEJA(1)
3044 sage: J2 = JordanSpinEJA(1)
3045 sage: J3 = JordanSpinEJA(1)
3046 sage: J = cartesian_product([J1,cartesian_product([J2,J3])])
3047 sage: J.multiplication_table()
3048 +----++----+----+----+
3049 | * || b0 | b1 | b2 |
3050 +====++====+====+====+
3051 | b0 || b0 | 0 | 0 |
3052 +----++----+----+----+
3053 | b1 || 0 | b1 | 0 |
3054 +----++----+----+----+
3055 | b2 || 0 | 0 | b2 |
3056 +----++----+----+----+
3057 sage: HadamardEJA(3).multiplication_table()
3058 +----++----+----+----+
3059 | * || b0 | b1 | b2 |
3060 +====++====+====+====+
3061 | b0 || b0 | 0 | 0 |
3062 +----++----+----+----+
3063 | b1 || 0 | b1 | 0 |
3064 +----++----+----+----+
3065 | b2 || 0 | 0 | b2 |
3066 +----++----+----+----+
3070 All factors must share the same base field::
3072 sage: J1 = HadamardEJA(2, field=QQ)
3073 sage: J2 = RealSymmetricEJA(2)
3074 sage: CartesianProductEJA((J1,J2))
3075 Traceback (most recent call last):
3077 ValueError: all factors must share the same base field
3079 The cached unit element is the same one that would be computed::
3081 sage: set_random_seed() # long time
3082 sage: J1 = random_eja() # long time
3083 sage: J2 = random_eja() # long time
3084 sage: J = cartesian_product([J1,J2]) # long time
3085 sage: actual = J.one() # long time
3086 sage: J.one.clear_cache() # long time
3087 sage: expected = J.one() # long time
3088 sage: actual == expected # long time
3092 Element
= FiniteDimensionalEJAElement
3095 def __init__(self
, factors
, **kwargs
):
3100 self
._sets
= factors
3102 field
= factors
[0].base_ring()
3103 if not all( J
.base_ring() == field
for J
in factors
):
3104 raise ValueError("all factors must share the same base field")
3106 associative
= all( f
.is_associative() for f
in factors
)
3108 # Compute my matrix space. This category isn't perfect, but
3109 # is good enough for what we need to do.
3110 MS_cat
= MagmaticAlgebras(field
).FiniteDimensional().WithBasis()
3111 MS_cat
= MS_cat
.Unital().CartesianProducts()
3112 MS_factors
= tuple( J
.matrix_space() for J
in factors
)
3113 from sage
.sets
.cartesian_product
import CartesianProduct
3114 MS
= CartesianProduct(MS_factors
, MS_cat
)
3119 for b
in factors
[i
].matrix_basis():
3124 basis
= tuple( MS(b
) for b
in basis
)
3126 # Define jordan/inner products that operate on that matrix_basis.
3127 def jordan_product(x
,y
):
3129 (factors
[i
](x
[i
])*factors
[i
](y
[i
])).to_matrix()
3133 def inner_product(x
, y
):
3135 factors
[i
](x
[i
]).inner_product(factors
[i
](y
[i
]))
3139 # There's no need to check the field since it already came
3140 # from an EJA. Likewise the axioms are guaranteed to be
3141 # satisfied, unless the guy writing this class sucks.
3143 # If you want the basis to be orthonormalized, orthonormalize
3145 FiniteDimensionalEJA
.__init
__(self
,
3151 orthonormalize
=False,
3152 associative
=associative
,
3153 cartesian_product
=True,
3157 # Since we don't (re)orthonormalize the basis, the FDEJA
3158 # constructor is going to set self._deortho_matrix to the
3159 # identity matrix. Here we set it to the correct value using
3160 # the deortho matrices from our factors.
3161 self
._deortho
_matrix
= matrix
.block_diagonal( [J
._deortho
_matrix
3164 self
.rank
.set_cache(sum(J
.rank() for J
in factors
))
3165 ones
= tuple(J
.one().to_matrix() for J
in factors
)
3166 self
.one
.set_cache(self(ones
))
3168 def cartesian_factors(self
):
3169 # Copy/pasted from CombinatorialFreeModule_CartesianProduct.
3172 def cartesian_factor(self
, i
):
3174 Return the ``i``th factor of this algebra.
3176 return self
._sets
[i
]
3179 # Copy/pasted from CombinatorialFreeModule_CartesianProduct.
3180 from sage
.categories
.cartesian_product
import cartesian_product
3181 return cartesian_product
.symbol
.join("%s" % factor
3182 for factor
in self
._sets
)
3184 def matrix_space(self
):
3186 Return the space that our matrix basis lives in as a Cartesian
3189 We don't simply use the ``cartesian_product()`` functor here
3190 because it acts differently on SageMath MatrixSpaces and our
3191 custom MatrixAlgebras, which are CombinatorialFreeModules. We
3192 always want the result to be represented (and indexed) as
3197 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
3199 ....: OctonionHermitianEJA,
3200 ....: RealSymmetricEJA)
3204 sage: J1 = HadamardEJA(1)
3205 sage: J2 = RealSymmetricEJA(2)
3206 sage: J = cartesian_product([J1,J2])
3207 sage: J.matrix_space()
3208 The Cartesian product of (Full MatrixSpace of 1 by 1 dense
3209 matrices over Algebraic Real Field, Full MatrixSpace of 2
3210 by 2 dense matrices over Algebraic Real Field)
3214 sage: J1 = ComplexHermitianEJA(1)
3215 sage: J2 = ComplexHermitianEJA(1)
3216 sage: J = cartesian_product([J1,J2])
3217 sage: J.one().to_matrix()[0]
3221 sage: J.one().to_matrix()[1]
3228 sage: J1 = OctonionHermitianEJA(1)
3229 sage: J2 = OctonionHermitianEJA(1)
3230 sage: J = cartesian_product([J1,J2])
3231 sage: J.one().to_matrix()[0]
3235 sage: J.one().to_matrix()[1]
3241 return super().matrix_space()
3245 def cartesian_projection(self
, i
):
3249 sage: from mjo.eja.eja_algebra import (random_eja,
3250 ....: JordanSpinEJA,
3252 ....: RealSymmetricEJA,
3253 ....: ComplexHermitianEJA)
3257 The projection morphisms are Euclidean Jordan algebra
3260 sage: J1 = HadamardEJA(2)
3261 sage: J2 = RealSymmetricEJA(2)
3262 sage: J = cartesian_product([J1,J2])
3263 sage: J.cartesian_projection(0)
3264 Linear operator between finite-dimensional Euclidean Jordan
3265 algebras represented by the matrix:
3268 Domain: Euclidean Jordan algebra of dimension 2 over Algebraic
3269 Real Field (+) Euclidean Jordan algebra of dimension 3 over
3270 Algebraic Real Field
3271 Codomain: Euclidean Jordan algebra of dimension 2 over Algebraic
3273 sage: J.cartesian_projection(1)
3274 Linear operator between finite-dimensional Euclidean Jordan
3275 algebras represented by the matrix:
3279 Domain: Euclidean Jordan algebra of dimension 2 over Algebraic
3280 Real Field (+) Euclidean Jordan algebra of dimension 3 over
3281 Algebraic Real Field
3282 Codomain: Euclidean Jordan algebra of dimension 3 over Algebraic
3285 The projections work the way you'd expect on the vector
3286 representation of an element::
3288 sage: J1 = JordanSpinEJA(2)
3289 sage: J2 = ComplexHermitianEJA(2)
3290 sage: J = cartesian_product([J1,J2])
3291 sage: pi_left = J.cartesian_projection(0)
3292 sage: pi_right = J.cartesian_projection(1)
3293 sage: pi_left(J.one()).to_vector()
3295 sage: pi_right(J.one()).to_vector()
3297 sage: J.one().to_vector()
3302 The answer never changes::
3304 sage: set_random_seed()
3305 sage: J1 = random_eja()
3306 sage: J2 = random_eja()
3307 sage: J = cartesian_product([J1,J2])
3308 sage: P0 = J.cartesian_projection(0)
3309 sage: P1 = J.cartesian_projection(0)
3314 offset
= sum( self
.cartesian_factor(k
).dimension()
3316 Ji
= self
.cartesian_factor(i
)
3317 Pi
= self
._module
_morphism
(lambda j
: Ji
.monomial(j
- offset
),
3320 return FiniteDimensionalEJAOperator(self
,Ji
,Pi
.matrix())
3323 def cartesian_embedding(self
, i
):
3327 sage: from mjo.eja.eja_algebra import (random_eja,
3328 ....: JordanSpinEJA,
3330 ....: RealSymmetricEJA)
3334 The embedding morphisms are Euclidean Jordan algebra
3337 sage: J1 = HadamardEJA(2)
3338 sage: J2 = RealSymmetricEJA(2)
3339 sage: J = cartesian_product([J1,J2])
3340 sage: J.cartesian_embedding(0)
3341 Linear operator between finite-dimensional Euclidean Jordan
3342 algebras represented by the matrix:
3348 Domain: Euclidean Jordan algebra of dimension 2 over
3349 Algebraic Real Field
3350 Codomain: Euclidean Jordan algebra of dimension 2 over
3351 Algebraic Real Field (+) Euclidean Jordan algebra of
3352 dimension 3 over Algebraic Real Field
3353 sage: J.cartesian_embedding(1)
3354 Linear operator between finite-dimensional Euclidean Jordan
3355 algebras represented by the matrix:
3361 Domain: Euclidean Jordan algebra of dimension 3 over
3362 Algebraic Real Field
3363 Codomain: Euclidean Jordan algebra of dimension 2 over
3364 Algebraic Real Field (+) Euclidean Jordan algebra of
3365 dimension 3 over Algebraic Real Field
3367 The embeddings work the way you'd expect on the vector
3368 representation of an element::
3370 sage: J1 = JordanSpinEJA(3)
3371 sage: J2 = RealSymmetricEJA(2)
3372 sage: J = cartesian_product([J1,J2])
3373 sage: iota_left = J.cartesian_embedding(0)
3374 sage: iota_right = J.cartesian_embedding(1)
3375 sage: iota_left(J1.zero()) == J.zero()
3377 sage: iota_right(J2.zero()) == J.zero()
3379 sage: J1.one().to_vector()
3381 sage: iota_left(J1.one()).to_vector()
3383 sage: J2.one().to_vector()
3385 sage: iota_right(J2.one()).to_vector()
3387 sage: J.one().to_vector()
3392 The answer never changes::
3394 sage: set_random_seed()
3395 sage: J1 = random_eja()
3396 sage: J2 = random_eja()
3397 sage: J = cartesian_product([J1,J2])
3398 sage: E0 = J.cartesian_embedding(0)
3399 sage: E1 = J.cartesian_embedding(0)
3403 Composing a projection with the corresponding inclusion should
3404 produce the identity map, and mismatching them should produce
3407 sage: set_random_seed()
3408 sage: J1 = random_eja()
3409 sage: J2 = random_eja()
3410 sage: J = cartesian_product([J1,J2])
3411 sage: iota_left = J.cartesian_embedding(0)
3412 sage: iota_right = J.cartesian_embedding(1)
3413 sage: pi_left = J.cartesian_projection(0)
3414 sage: pi_right = J.cartesian_projection(1)
3415 sage: pi_left*iota_left == J1.one().operator()
3417 sage: pi_right*iota_right == J2.one().operator()
3419 sage: (pi_left*iota_right).is_zero()
3421 sage: (pi_right*iota_left).is_zero()
3425 offset
= sum( self
.cartesian_factor(k
).dimension()
3427 Ji
= self
.cartesian_factor(i
)
3428 Ei
= Ji
._module
_morphism
(lambda j
: self
.monomial(j
+ offset
),
3430 return FiniteDimensionalEJAOperator(Ji
,self
,Ei
.matrix())
3434 FiniteDimensionalEJA
.CartesianProduct
= CartesianProductEJA
3436 class RationalBasisCartesianProductEJA(CartesianProductEJA
,
3439 A separate class for products of algebras for which we know a
3444 sage: from mjo.eja.eja_algebra import (HadamardEJA,
3445 ....: JordanSpinEJA,
3446 ....: OctonionHermitianEJA,
3447 ....: RealSymmetricEJA)
3451 This gives us fast characteristic polynomial computations in
3452 product algebras, too::
3455 sage: J1 = JordanSpinEJA(2)
3456 sage: J2 = RealSymmetricEJA(3)
3457 sage: J = cartesian_product([J1,J2])
3458 sage: J.characteristic_polynomial_of().degree()
3465 The ``cartesian_product()`` function only uses the first factor to
3466 decide where the result will live; thus we have to be careful to
3467 check that all factors do indeed have a `_rational_algebra` member
3468 before we try to access it::
3470 sage: J1 = OctonionHermitianEJA(1) # no rational basis
3471 sage: J2 = HadamardEJA(2)
3472 sage: cartesian_product([J1,J2])
3473 Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
3474 (+) Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
3475 sage: cartesian_product([J2,J1])
3476 Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
3477 (+) Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
3480 def __init__(self
, algebras
, **kwargs
):
3481 CartesianProductEJA
.__init
__(self
, algebras
, **kwargs
)
3483 self
._rational
_algebra
= None
3484 if self
.vector_space().base_field() is not QQ
:
3485 if all( hasattr(r
, "_rational_algebra") for r
in algebras
):
3486 self
._rational
_algebra
= cartesian_product([
3487 r
._rational
_algebra
for r
in algebras
3491 RationalBasisEJA
.CartesianProduct
= RationalBasisCartesianProductEJA
3493 def random_eja(max_dimension
=None, *args
, **kwargs
):
3498 sage: from mjo.eja.eja_algebra import random_eja
3502 sage: set_random_seed()
3503 sage: n = ZZ.random_element(1,5)
3504 sage: J = random_eja(max_dimension=n, field=QQ, orthonormalize=False)
3505 sage: J.dimension() <= n
3509 # Use the ConcreteEJA default as the total upper bound (regardless
3510 # of any whether or not any individual factors set a lower limit).
3511 if max_dimension
is None:
3512 max_dimension
= ConcreteEJA
._max
_random
_instance
_dimension
()
3513 J1
= ConcreteEJA
.random_instance(max_dimension
, *args
, **kwargs
)
3516 # Roll the dice to see if we attempt a Cartesian product.
3517 dice_roll
= ZZ
.random_element(len(ConcreteEJA
.__subclasses
__()) + 1)
3518 new_max_dimension
= max_dimension
- J1
.dimension()
3519 if new_max_dimension
== 0 or dice_roll
!= 0:
3520 # If it's already as big as we're willing to tolerate, just
3521 # return it and don't worry about Cartesian products.
3524 # Use random_eja() again so we can get more than two factors
3525 # if the sub-call also Decides on a cartesian product.
3526 J2
= random_eja(new_max_dimension
, *args
, **kwargs
)
3527 return cartesian_product([J1
,J2
])