]> gitweb.michael.orlitzky.com - sage.d.git/blob - mjo/eja/eja_algebra.py
eja: remove redundant kwargs mangling from MatrixEJA subclasses.
[sage.d.git] / mjo / eja / eja_algebra.py
1 """
2 Representations and constructions for Euclidean Jordan algebras.
3
4 A Euclidean Jordan algebra is a Jordan algebra that has some
5 additional properties:
6
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.
13
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
18 of inner-product.
19
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.
24
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:
30
31 * :class:`BilinearFormEJA`
32 * :class:`RealSymmetricEJA`
33 * :class:`ComplexHermitianEJA`
34 * :class:`QuaternionHermitianEJA`
35 * :class:`OctonionHermitianEJA`
36
37 In addition to these, we provide two other example constructions,
38
39 * :class:`JordanSpinEJA`
40 * :class:`HadamardEJA`
41 * :class:`AlbertEJA`
42 * :class:`TrivialEJA`
43
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.
54
55 At a minimum, the following are required to construct a Euclidean
56 Jordan algebra:
57
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
61
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.
65
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::
73
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)
79 sage: J1
80 Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
81
82 In fact, any positive scalar multiple of that inner-product would work::
83
84 sage: ip2 = lambda X,Y: 16*ip(X,Y)
85 sage: J2 = FiniteDimensionalEJA((b1,), jp, ip2)
86 sage: J2
87 Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
88
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::
92
93 sage: J3 = FiniteDimensionalEJA((b1,), jp, ip2, orthonormalize=False)
94 sage: J3
95 Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
96
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
99 matrix form::
100
101 sage: J1.basis()[0].to_matrix()
102 [1]
103 sage: J2.basis()[0].to_matrix()
104 [1/4]
105 sage: J3.basis()[0].to_matrix()
106 [1]
107
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.
114
115 Another option for your basis is to use elemebts of a
116 :class:`MatrixAlgebra`::
117
118 sage: from mjo.matrix_algebra import MatrixAlgebra
119 sage: A = MatrixAlgebra(1,AA,AA)
120 sage: J4 = FiniteDimensionalEJA(A.gens(), jp, ip)
121 sage: J4
122 Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
123 sage: J4.basis()[0].to_matrix()
124 +---+
125 | 1 |
126 +---+
127
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``
130 method::
131
132 sage: J4.matrix_basis()
133 (+---+
134 | 1 |
135 +---+,)
136
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.
146
147 SETUP::
148
149 sage: from mjo.eja.eja_algebra import random_eja
150
151 EXAMPLES::
152
153 sage: random_eja()
154 Euclidean Jordan algebra of dimension...
155 """
156
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,
167 PolynomialRing,
168 QuadraticField)
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
172
173 def EuclideanJordanAlgebras(field):
174 r"""
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
178 apply to all EJAs.
179 """
180 category = MagmaticAlgebras(field).FiniteDimensional()
181 category = category.WithBasis().Unital().Commutative()
182 return category
183
184 class FiniteDimensionalEJA(CombinatorialFreeModule):
185 r"""
186 A finite-dimensional Euclidean Jordan algebra.
187
188 INPUT:
189
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.
199
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.
204
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.
209
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
213 required.
214
215 - ``field`` -- a subfield of the reals (default: ``AA``); the scalar
216 field for the algebra.
217
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.
222
223 SETUP::
224
225 sage: from mjo.eja.eja_algebra import random_eja
226
227 TESTS:
228
229 We should compute that an element subalgebra is associative even
230 if we circumvent the element method::
231
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()
238 True
239 """
240 Element = FiniteDimensionalEJAElement
241
242 @staticmethod
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")
249
250 @staticmethod
251 def _check_input_axioms(basis, jordan_product, inner_product):
252 if not all( jordan_product(bi,bj) == jordan_product(bj,bi)
253 for bi in basis
254 for bj in basis ):
255 raise ValueError("Jordan product is not commutative")
256
257 if not all( inner_product(bi,bj) == inner_product(bj,bi)
258 for bi in basis
259 for bj in basis ):
260 raise ValueError("inner-product is not commutative")
261
262 def __init__(self,
263 basis,
264 jordan_product,
265 inner_product,
266 field=AA,
267 matrix_space=None,
268 orthonormalize=True,
269 associative=None,
270 check_field=True,
271 check_axioms=True,
272 prefix="b"):
273
274 n = len(basis)
275
276 if check_field:
277 self._check_input_field(field)
278
279 if check_axioms:
280 # Check commutativity of the Jordan and inner-products.
281 # This has to be done before we build the multiplication
282 # and inner-product tables/matrices, because we take
283 # advantage of symmetry in the process.
284 self._check_input_axioms(basis, jordan_product, inner_product)
285
286 if n <= 1:
287 # All zero- and one-dimensional algebras are just the real
288 # numbers with (some positive multiples of) the usual
289 # multiplication as its Jordan and inner-product.
290 associative = True
291 if associative is None:
292 # We should figure it out. As with check_axioms, we have to do
293 # this without the help of the _jordan_product_is_associative()
294 # method because we need to know the category before we
295 # initialize the algebra.
296 associative = all( jordan_product(jordan_product(bi,bj),bk)
297 ==
298 jordan_product(bi,jordan_product(bj,bk))
299 for bi in basis
300 for bj in basis
301 for bk in basis)
302
303 category = EuclideanJordanAlgebras(field)
304
305 if associative:
306 # Element subalgebras can take advantage of this.
307 category = category.Associative()
308
309 # Call the superclass constructor so that we can use its from_vector()
310 # method to build our multiplication table.
311 CombinatorialFreeModule.__init__(self,
312 field,
313 range(n),
314 prefix=prefix,
315 category=category,
316 bracket=False)
317
318 # Now comes all of the hard work. We'll be constructing an
319 # ambient vector space V that our (vectorized) basis lives in,
320 # as well as a subspace W of V spanned by those (vectorized)
321 # basis elements. The W-coordinates are the coefficients that
322 # we see in things like x = 1*b1 + 2*b2.
323
324 degree = 0
325 if n > 0:
326 degree = len(_all2list(basis[0]))
327
328 # Build an ambient space that fits our matrix basis when
329 # written out as "long vectors."
330 V = VectorSpace(field, degree)
331
332 # The matrix that will hold the orthonormal -> unorthonormal
333 # coordinate transformation. Default to an identity matrix of
334 # the appropriate size to avoid special cases for None
335 # everywhere.
336 self._deortho_matrix = matrix.identity(field,n)
337
338 if orthonormalize:
339 # Save a copy of the un-orthonormalized basis for later.
340 # Convert it to ambient V (vector) coordinates while we're
341 # at it, because we'd have to do it later anyway.
342 deortho_vector_basis = tuple( V(_all2list(b)) for b in basis )
343
344 from mjo.eja.eja_utils import gram_schmidt
345 basis = tuple(gram_schmidt(basis, inner_product))
346
347 # Save the (possibly orthonormalized) matrix basis for
348 # later, as well as the space that its elements live in.
349 # In most cases we can deduce the matrix space, but when
350 # n == 0 (that is, there are no basis elements) we cannot.
351 self._matrix_basis = basis
352 if matrix_space is None:
353 self._matrix_space = self._matrix_basis[0].parent()
354 else:
355 self._matrix_space = matrix_space
356
357 # Now create the vector space for the algebra, which will have
358 # its own set of non-ambient coordinates (in terms of the
359 # supplied basis).
360 vector_basis = tuple( V(_all2list(b)) for b in basis )
361
362 # Save the span of our matrix basis (when written out as long
363 # vectors) because otherwise we'll have to reconstruct it
364 # every time we want to coerce a matrix into the algebra.
365 self._matrix_span = V.span_of_basis( vector_basis, check=check_axioms)
366
367 if orthonormalize:
368 # Now "self._matrix_span" is the vector space of our
369 # algebra coordinates. The variables "X1", "X2",... refer
370 # to the entries of vectors in self._matrix_span. Thus to
371 # convert back and forth between the orthonormal
372 # coordinates and the given ones, we need to stick the
373 # original basis in self._matrix_span.
374 U = V.span_of_basis( deortho_vector_basis, check=check_axioms)
375 self._deortho_matrix = matrix.column( U.coordinate_vector(q)
376 for q in vector_basis )
377
378
379 # Now we actually compute the multiplication and inner-product
380 # tables/matrices using the possibly-orthonormalized basis.
381 self._inner_product_matrix = matrix.identity(field, n)
382 zed = self.zero()
383 self._multiplication_table = [ [zed for j in range(i+1)]
384 for i in range(n) ]
385
386 # Note: the Jordan and inner-products are defined in terms
387 # of the ambient basis. It's important that their arguments
388 # are in ambient coordinates as well.
389 for i in range(n):
390 for j in range(i+1):
391 # ortho basis w.r.t. ambient coords
392 q_i = basis[i]
393 q_j = basis[j]
394
395 # The jordan product returns a matrixy answer, so we
396 # have to convert it to the algebra coordinates.
397 elt = jordan_product(q_i, q_j)
398 elt = self._matrix_span.coordinate_vector(V(_all2list(elt)))
399 self._multiplication_table[i][j] = self.from_vector(elt)
400
401 if not orthonormalize:
402 # If we're orthonormalizing the basis with respect
403 # to an inner-product, then the inner-product
404 # matrix with respect to the resulting basis is
405 # just going to be the identity.
406 ip = inner_product(q_i, q_j)
407 self._inner_product_matrix[i,j] = ip
408 self._inner_product_matrix[j,i] = ip
409
410 self._inner_product_matrix._cache = {'hermitian': True}
411 self._inner_product_matrix.set_immutable()
412
413 if check_axioms:
414 if not self._is_jordanian():
415 raise ValueError("Jordan identity does not hold")
416 if not self._inner_product_is_associative():
417 raise ValueError("inner product is not associative")
418
419
420 def _coerce_map_from_base_ring(self):
421 """
422 Disable the map from the base ring into the algebra.
423
424 Performing a nonsense conversion like this automatically
425 is counterpedagogical. The fallback is to try the usual
426 element constructor, which should also fail.
427
428 SETUP::
429
430 sage: from mjo.eja.eja_algebra import random_eja
431
432 TESTS::
433
434 sage: set_random_seed()
435 sage: J = random_eja()
436 sage: J(1)
437 Traceback (most recent call last):
438 ...
439 ValueError: not an element of this algebra
440
441 """
442 return None
443
444
445 def product_on_basis(self, i, j):
446 r"""
447 Returns the Jordan product of the `i` and `j`th basis elements.
448
449 This completely defines the Jordan product on the algebra, and
450 is used direclty by our superclass machinery to implement
451 :meth:`product`.
452
453 SETUP::
454
455 sage: from mjo.eja.eja_algebra import random_eja
456
457 TESTS::
458
459 sage: set_random_seed()
460 sage: J = random_eja()
461 sage: n = J.dimension()
462 sage: bi = J.zero()
463 sage: bj = J.zero()
464 sage: bi_bj = J.zero()*J.zero()
465 sage: if n > 0:
466 ....: i = ZZ.random_element(n)
467 ....: j = ZZ.random_element(n)
468 ....: bi = J.monomial(i)
469 ....: bj = J.monomial(j)
470 ....: bi_bj = J.product_on_basis(i,j)
471 sage: bi*bj == bi_bj
472 True
473
474 """
475 # We only stored the lower-triangular portion of the
476 # multiplication table.
477 if j <= i:
478 return self._multiplication_table[i][j]
479 else:
480 return self._multiplication_table[j][i]
481
482 def inner_product(self, x, y):
483 """
484 The inner product associated with this Euclidean Jordan algebra.
485
486 Defaults to the trace inner product, but can be overridden by
487 subclasses if they are sure that the necessary properties are
488 satisfied.
489
490 SETUP::
491
492 sage: from mjo.eja.eja_algebra import (random_eja,
493 ....: HadamardEJA,
494 ....: BilinearFormEJA)
495
496 EXAMPLES:
497
498 Our inner product is "associative," which means the following for
499 a symmetric bilinear form::
500
501 sage: set_random_seed()
502 sage: J = random_eja()
503 sage: x,y,z = J.random_elements(3)
504 sage: (x*y).inner_product(z) == y.inner_product(x*z)
505 True
506
507 TESTS:
508
509 Ensure that this is the usual inner product for the algebras
510 over `R^n`::
511
512 sage: set_random_seed()
513 sage: J = HadamardEJA.random_instance()
514 sage: x,y = J.random_elements(2)
515 sage: actual = x.inner_product(y)
516 sage: expected = x.to_vector().inner_product(y.to_vector())
517 sage: actual == expected
518 True
519
520 Ensure that this is one-half of the trace inner-product in a
521 BilinearFormEJA that isn't just the reals (when ``n`` isn't
522 one). This is in Faraut and Koranyi, and also my "On the
523 symmetry..." paper::
524
525 sage: set_random_seed()
526 sage: J = BilinearFormEJA.random_instance()
527 sage: n = J.dimension()
528 sage: x = J.random_element()
529 sage: y = J.random_element()
530 sage: (n == 1) or (x.inner_product(y) == (x*y).trace()/2)
531 True
532
533 """
534 B = self._inner_product_matrix
535 return (B*x.to_vector()).inner_product(y.to_vector())
536
537
538 def is_associative(self):
539 r"""
540 Return whether or not this algebra's Jordan product is associative.
541
542 SETUP::
543
544 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
545
546 EXAMPLES::
547
548 sage: J = ComplexHermitianEJA(3, field=QQ, orthonormalize=False)
549 sage: J.is_associative()
550 False
551 sage: x = sum(J.gens())
552 sage: A = x.subalgebra_generated_by(orthonormalize=False)
553 sage: A.is_associative()
554 True
555
556 """
557 return "Associative" in self.category().axioms()
558
559 def _is_commutative(self):
560 r"""
561 Whether or not this algebra's multiplication table is commutative.
562
563 This method should of course always return ``True``, unless
564 this algebra was constructed with ``check_axioms=False`` and
565 passed an invalid multiplication table.
566 """
567 return all( x*y == y*x for x in self.gens() for y in self.gens() )
568
569 def _is_jordanian(self):
570 r"""
571 Whether or not this algebra's multiplication table respects the
572 Jordan identity `(x^{2})(xy) = x(x^{2}y)`.
573
574 We only check one arrangement of `x` and `y`, so for a
575 ``True`` result to be truly true, you should also check
576 :meth:`_is_commutative`. This method should of course always
577 return ``True``, unless this algebra was constructed with
578 ``check_axioms=False`` and passed an invalid multiplication table.
579 """
580 return all( (self.monomial(i)**2)*(self.monomial(i)*self.monomial(j))
581 ==
582 (self.monomial(i))*((self.monomial(i)**2)*self.monomial(j))
583 for i in range(self.dimension())
584 for j in range(self.dimension()) )
585
586 def _jordan_product_is_associative(self):
587 r"""
588 Return whether or not this algebra's Jordan product is
589 associative; that is, whether or not `x*(y*z) = (x*y)*z`
590 for all `x,y,x`.
591
592 This method should agree with :meth:`is_associative` unless
593 you lied about the value of the ``associative`` parameter
594 when you constructed the algebra.
595
596 SETUP::
597
598 sage: from mjo.eja.eja_algebra import (random_eja,
599 ....: RealSymmetricEJA,
600 ....: ComplexHermitianEJA,
601 ....: QuaternionHermitianEJA)
602
603 EXAMPLES::
604
605 sage: J = RealSymmetricEJA(4, orthonormalize=False)
606 sage: J._jordan_product_is_associative()
607 False
608 sage: x = sum(J.gens())
609 sage: A = x.subalgebra_generated_by()
610 sage: A._jordan_product_is_associative()
611 True
612
613 ::
614
615 sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
616 sage: J._jordan_product_is_associative()
617 False
618 sage: x = sum(J.gens())
619 sage: A = x.subalgebra_generated_by(orthonormalize=False)
620 sage: A._jordan_product_is_associative()
621 True
622
623 ::
624
625 sage: J = QuaternionHermitianEJA(2)
626 sage: J._jordan_product_is_associative()
627 False
628 sage: x = sum(J.gens())
629 sage: A = x.subalgebra_generated_by()
630 sage: A._jordan_product_is_associative()
631 True
632
633 TESTS:
634
635 The values we've presupplied to the constructors agree with
636 the computation::
637
638 sage: set_random_seed()
639 sage: J = random_eja()
640 sage: J.is_associative() == J._jordan_product_is_associative()
641 True
642
643 """
644 R = self.base_ring()
645
646 # Used to check whether or not something is zero.
647 epsilon = R.zero()
648 if not R.is_exact():
649 # I don't know of any examples that make this magnitude
650 # necessary because I don't know how to make an
651 # associative algebra when the element subalgebra
652 # construction is unreliable (as it is over RDF; we can't
653 # find the degree of an element because we can't compute
654 # the rank of a matrix). But even multiplication of floats
655 # is non-associative, so *some* epsilon is needed... let's
656 # just take the one from _inner_product_is_associative?
657 epsilon = 1e-15
658
659 for i in range(self.dimension()):
660 for j in range(self.dimension()):
661 for k in range(self.dimension()):
662 x = self.monomial(i)
663 y = self.monomial(j)
664 z = self.monomial(k)
665 diff = (x*y)*z - x*(y*z)
666
667 if diff.norm() > epsilon:
668 return False
669
670 return True
671
672 def _inner_product_is_associative(self):
673 r"""
674 Return whether or not this algebra's inner product `B` is
675 associative; that is, whether or not `B(xy,z) = B(x,yz)`.
676
677 This method should of course always return ``True``, unless
678 this algebra was constructed with ``check_axioms=False`` and
679 passed an invalid Jordan or inner-product.
680 """
681 R = self.base_ring()
682
683 # Used to check whether or not something is zero.
684 epsilon = R.zero()
685 if not R.is_exact():
686 # This choice is sufficient to allow the construction of
687 # QuaternionHermitianEJA(2, field=RDF) with check_axioms=True.
688 epsilon = 1e-15
689
690 for i in range(self.dimension()):
691 for j in range(self.dimension()):
692 for k in range(self.dimension()):
693 x = self.monomial(i)
694 y = self.monomial(j)
695 z = self.monomial(k)
696 diff = (x*y).inner_product(z) - x.inner_product(y*z)
697
698 if diff.abs() > epsilon:
699 return False
700
701 return True
702
703 def _element_constructor_(self, elt):
704 """
705 Construct an element of this algebra or a subalgebra from its
706 EJA element, vector, or matrix representation.
707
708 This gets called only after the parent element _call_ method
709 fails to find a coercion for the argument.
710
711 SETUP::
712
713 sage: from mjo.eja.eja_algebra import (random_eja,
714 ....: JordanSpinEJA,
715 ....: HadamardEJA,
716 ....: RealSymmetricEJA)
717
718 EXAMPLES:
719
720 The identity in `S^n` is converted to the identity in the EJA::
721
722 sage: J = RealSymmetricEJA(3)
723 sage: I = matrix.identity(QQ,3)
724 sage: J(I) == J.one()
725 True
726
727 This skew-symmetric matrix can't be represented in the EJA::
728
729 sage: J = RealSymmetricEJA(3)
730 sage: A = matrix(QQ,3, lambda i,j: i-j)
731 sage: J(A)
732 Traceback (most recent call last):
733 ...
734 ValueError: not an element of this algebra
735
736 Tuples work as well, provided that the matrix basis for the
737 algebra consists of them::
738
739 sage: J1 = HadamardEJA(3)
740 sage: J2 = RealSymmetricEJA(2)
741 sage: J = cartesian_product([J1,J2])
742 sage: J( (J1.matrix_basis()[1], J2.matrix_basis()[2]) )
743 b1 + b5
744
745 Subalgebra elements are embedded into the superalgebra::
746
747 sage: J = JordanSpinEJA(3)
748 sage: J.one()
749 b0
750 sage: x = sum(J.gens())
751 sage: A = x.subalgebra_generated_by()
752 sage: J(A.one())
753 b0
754
755 TESTS:
756
757 Ensure that we can convert any element back and forth
758 faithfully between its matrix and algebra representations::
759
760 sage: set_random_seed()
761 sage: J = random_eja()
762 sage: x = J.random_element()
763 sage: J(x.to_matrix()) == x
764 True
765
766 We cannot coerce elements between algebras just because their
767 matrix representations are compatible::
768
769 sage: J1 = HadamardEJA(3)
770 sage: J2 = JordanSpinEJA(3)
771 sage: J2(J1.one())
772 Traceback (most recent call last):
773 ...
774 ValueError: not an element of this algebra
775 sage: J1(J2.zero())
776 Traceback (most recent call last):
777 ...
778 ValueError: not an element of this algebra
779
780 """
781 msg = "not an element of this algebra"
782 if elt in self.base_ring():
783 # Ensure that no base ring -> algebra coercion is performed
784 # by this method. There's some stupidity in sage that would
785 # otherwise propagate to this method; for example, sage thinks
786 # that the integer 3 belongs to the space of 2-by-2 matrices.
787 raise ValueError(msg)
788
789 if hasattr(elt, 'superalgebra_element'):
790 # Handle subalgebra elements
791 if elt.parent().superalgebra() == self:
792 return elt.superalgebra_element()
793
794 if hasattr(elt, 'sparse_vector'):
795 # Convert a vector into a column-matrix. We check for
796 # "sparse_vector" and not "column" because matrices also
797 # have a "column" method.
798 elt = elt.column()
799
800 if elt not in self.matrix_space():
801 raise ValueError(msg)
802
803 # Thanks for nothing! Matrix spaces aren't vector spaces in
804 # Sage, so we have to figure out its matrix-basis coordinates
805 # ourselves. We use the basis space's ring instead of the
806 # element's ring because the basis space might be an algebraic
807 # closure whereas the base ring of the 3-by-3 identity matrix
808 # could be QQ instead of QQbar.
809 #
810 # And, we also have to handle Cartesian product bases (when
811 # the matrix basis consists of tuples) here. The "good news"
812 # is that we're already converting everything to long vectors,
813 # and that strategy works for tuples as well.
814 #
815 elt = self._matrix_span.ambient_vector_space()(_all2list(elt))
816
817 try:
818 coords = self._matrix_span.coordinate_vector(elt)
819 except ArithmeticError: # vector is not in free module
820 raise ValueError(msg)
821
822 return self.from_vector(coords)
823
824 def _repr_(self):
825 """
826 Return a string representation of ``self``.
827
828 SETUP::
829
830 sage: from mjo.eja.eja_algebra import JordanSpinEJA
831
832 TESTS:
833
834 Ensure that it says what we think it says::
835
836 sage: JordanSpinEJA(2, field=AA)
837 Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
838 sage: JordanSpinEJA(3, field=RDF)
839 Euclidean Jordan algebra of dimension 3 over Real Double Field
840
841 """
842 fmt = "Euclidean Jordan algebra of dimension {} over {}"
843 return fmt.format(self.dimension(), self.base_ring())
844
845
846 @cached_method
847 def characteristic_polynomial_of(self):
848 """
849 Return the algebra's "characteristic polynomial of" function,
850 which is itself a multivariate polynomial that, when evaluated
851 at the coordinates of some algebra element, returns that
852 element's characteristic polynomial.
853
854 The resulting polynomial has `n+1` variables, where `n` is the
855 dimension of this algebra. The first `n` variables correspond to
856 the coordinates of an algebra element: when evaluated at the
857 coordinates of an algebra element with respect to a certain
858 basis, the result is a univariate polynomial (in the one
859 remaining variable ``t``), namely the characteristic polynomial
860 of that element.
861
862 SETUP::
863
864 sage: from mjo.eja.eja_algebra import JordanSpinEJA, TrivialEJA
865
866 EXAMPLES:
867
868 The characteristic polynomial in the spin algebra is given in
869 Alizadeh, Example 11.11::
870
871 sage: J = JordanSpinEJA(3)
872 sage: p = J.characteristic_polynomial_of(); p
873 X1^2 - X2^2 - X3^2 + (-2*t)*X1 + t^2
874 sage: xvec = J.one().to_vector()
875 sage: p(*xvec)
876 t^2 - 2*t + 1
877
878 By definition, the characteristic polynomial is a monic
879 degree-zero polynomial in a rank-zero algebra. Note that
880 Cayley-Hamilton is indeed satisfied since the polynomial
881 ``1`` evaluates to the identity element of the algebra on
882 any argument::
883
884 sage: J = TrivialEJA()
885 sage: J.characteristic_polynomial_of()
886 1
887
888 """
889 r = self.rank()
890 n = self.dimension()
891
892 # The list of coefficient polynomials a_0, a_1, a_2, ..., a_(r-1).
893 a = self._charpoly_coefficients()
894
895 # We go to a bit of trouble here to reorder the
896 # indeterminates, so that it's easier to evaluate the
897 # characteristic polynomial at x's coordinates and get back
898 # something in terms of t, which is what we want.
899 S = PolynomialRing(self.base_ring(),'t')
900 t = S.gen(0)
901 if r > 0:
902 R = a[0].parent()
903 S = PolynomialRing(S, R.variable_names())
904 t = S(t)
905
906 return (t**r + sum( a[k]*(t**k) for k in range(r) ))
907
908 def coordinate_polynomial_ring(self):
909 r"""
910 The multivariate polynomial ring in which this algebra's
911 :meth:`characteristic_polynomial_of` lives.
912
913 SETUP::
914
915 sage: from mjo.eja.eja_algebra import (HadamardEJA,
916 ....: RealSymmetricEJA)
917
918 EXAMPLES::
919
920 sage: J = HadamardEJA(2)
921 sage: J.coordinate_polynomial_ring()
922 Multivariate Polynomial Ring in X1, X2...
923 sage: J = RealSymmetricEJA(3,field=QQ,orthonormalize=False)
924 sage: J.coordinate_polynomial_ring()
925 Multivariate Polynomial Ring in X1, X2, X3, X4, X5, X6...
926
927 """
928 var_names = tuple( "X%d" % z for z in range(1, self.dimension()+1) )
929 return PolynomialRing(self.base_ring(), var_names)
930
931 def inner_product(self, x, y):
932 """
933 The inner product associated with this Euclidean Jordan algebra.
934
935 Defaults to the trace inner product, but can be overridden by
936 subclasses if they are sure that the necessary properties are
937 satisfied.
938
939 SETUP::
940
941 sage: from mjo.eja.eja_algebra import (random_eja,
942 ....: HadamardEJA,
943 ....: BilinearFormEJA)
944
945 EXAMPLES:
946
947 Our inner product is "associative," which means the following for
948 a symmetric bilinear form::
949
950 sage: set_random_seed()
951 sage: J = random_eja()
952 sage: x,y,z = J.random_elements(3)
953 sage: (x*y).inner_product(z) == y.inner_product(x*z)
954 True
955
956 TESTS:
957
958 Ensure that this is the usual inner product for the algebras
959 over `R^n`::
960
961 sage: set_random_seed()
962 sage: J = HadamardEJA.random_instance()
963 sage: x,y = J.random_elements(2)
964 sage: actual = x.inner_product(y)
965 sage: expected = x.to_vector().inner_product(y.to_vector())
966 sage: actual == expected
967 True
968
969 Ensure that this is one-half of the trace inner-product in a
970 BilinearFormEJA that isn't just the reals (when ``n`` isn't
971 one). This is in Faraut and Koranyi, and also my "On the
972 symmetry..." paper::
973
974 sage: set_random_seed()
975 sage: J = BilinearFormEJA.random_instance()
976 sage: n = J.dimension()
977 sage: x = J.random_element()
978 sage: y = J.random_element()
979 sage: (n == 1) or (x.inner_product(y) == (x*y).trace()/2)
980 True
981 """
982 B = self._inner_product_matrix
983 return (B*x.to_vector()).inner_product(y.to_vector())
984
985
986 def is_trivial(self):
987 """
988 Return whether or not this algebra is trivial.
989
990 A trivial algebra contains only the zero element.
991
992 SETUP::
993
994 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
995 ....: TrivialEJA)
996
997 EXAMPLES::
998
999 sage: J = ComplexHermitianEJA(3)
1000 sage: J.is_trivial()
1001 False
1002
1003 ::
1004
1005 sage: J = TrivialEJA()
1006 sage: J.is_trivial()
1007 True
1008
1009 """
1010 return self.dimension() == 0
1011
1012
1013 def multiplication_table(self):
1014 """
1015 Return a visual representation of this algebra's multiplication
1016 table (on basis elements).
1017
1018 SETUP::
1019
1020 sage: from mjo.eja.eja_algebra import JordanSpinEJA
1021
1022 EXAMPLES::
1023
1024 sage: J = JordanSpinEJA(4)
1025 sage: J.multiplication_table()
1026 +----++----+----+----+----+
1027 | * || b0 | b1 | b2 | b3 |
1028 +====++====+====+====+====+
1029 | b0 || b0 | b1 | b2 | b3 |
1030 +----++----+----+----+----+
1031 | b1 || b1 | b0 | 0 | 0 |
1032 +----++----+----+----+----+
1033 | b2 || b2 | 0 | b0 | 0 |
1034 +----++----+----+----+----+
1035 | b3 || b3 | 0 | 0 | b0 |
1036 +----++----+----+----+----+
1037
1038 """
1039 n = self.dimension()
1040 # Prepend the header row.
1041 M = [["*"] + list(self.gens())]
1042
1043 # And to each subsequent row, prepend an entry that belongs to
1044 # the left-side "header column."
1045 M += [ [self.monomial(i)] + [ self.monomial(i)*self.monomial(j)
1046 for j in range(n) ]
1047 for i in range(n) ]
1048
1049 return table(M, header_row=True, header_column=True, frame=True)
1050
1051
1052 def matrix_basis(self):
1053 """
1054 Return an (often more natural) representation of this algebras
1055 basis as an ordered tuple of matrices.
1056
1057 Every finite-dimensional Euclidean Jordan Algebra is a, up to
1058 Jordan isomorphism, a direct sum of five simple
1059 algebras---four of which comprise Hermitian matrices. And the
1060 last type of algebra can of course be thought of as `n`-by-`1`
1061 column matrices (ambiguusly called column vectors) to avoid
1062 special cases. As a result, matrices (and column vectors) are
1063 a natural representation format for Euclidean Jordan algebra
1064 elements.
1065
1066 But, when we construct an algebra from a basis of matrices,
1067 those matrix representations are lost in favor of coordinate
1068 vectors *with respect to* that basis. We could eventually
1069 convert back if we tried hard enough, but having the original
1070 representations handy is valuable enough that we simply store
1071 them and return them from this method.
1072
1073 Why implement this for non-matrix algebras? Avoiding special
1074 cases for the :class:`BilinearFormEJA` pays with simplicity in
1075 its own right. But mainly, we would like to be able to assume
1076 that elements of a :class:`CartesianProductEJA` can be displayed
1077 nicely, without having to have special classes for direct sums
1078 one of whose components was a matrix algebra.
1079
1080 SETUP::
1081
1082 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
1083 ....: RealSymmetricEJA)
1084
1085 EXAMPLES::
1086
1087 sage: J = RealSymmetricEJA(2)
1088 sage: J.basis()
1089 Finite family {0: b0, 1: b1, 2: b2}
1090 sage: J.matrix_basis()
1091 (
1092 [1 0] [ 0 0.7071067811865475?] [0 0]
1093 [0 0], [0.7071067811865475? 0], [0 1]
1094 )
1095
1096 ::
1097
1098 sage: J = JordanSpinEJA(2)
1099 sage: J.basis()
1100 Finite family {0: b0, 1: b1}
1101 sage: J.matrix_basis()
1102 (
1103 [1] [0]
1104 [0], [1]
1105 )
1106 """
1107 return self._matrix_basis
1108
1109
1110 def matrix_space(self):
1111 """
1112 Return the matrix space in which this algebra's elements live, if
1113 we think of them as matrices (including column vectors of the
1114 appropriate size).
1115
1116 "By default" this will be an `n`-by-`1` column-matrix space,
1117 except when the algebra is trivial. There it's `n`-by-`n`
1118 (where `n` is zero), to ensure that two elements of the matrix
1119 space (empty matrices) can be multiplied. For algebras of
1120 matrices, this returns the space in which their
1121 real embeddings live.
1122
1123 SETUP::
1124
1125 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
1126 ....: JordanSpinEJA,
1127 ....: QuaternionHermitianEJA,
1128 ....: TrivialEJA)
1129
1130 EXAMPLES:
1131
1132 By default, the matrix representation is just a column-matrix
1133 equivalent to the vector representation::
1134
1135 sage: J = JordanSpinEJA(3)
1136 sage: J.matrix_space()
1137 Full MatrixSpace of 3 by 1 dense matrices over Algebraic
1138 Real Field
1139
1140 The matrix representation in the trivial algebra is
1141 zero-by-zero instead of the usual `n`-by-one::
1142
1143 sage: J = TrivialEJA()
1144 sage: J.matrix_space()
1145 Full MatrixSpace of 0 by 0 dense matrices over Algebraic
1146 Real Field
1147
1148 The matrix space for complex/quaternion Hermitian matrix EJA
1149 is the space in which their real-embeddings live, not the
1150 original complex/quaternion matrix space::
1151
1152 sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
1153 sage: J.matrix_space()
1154 Module of 2 by 2 matrices with entries in Algebraic Field over
1155 the scalar ring Rational Field
1156 sage: J = QuaternionHermitianEJA(1,field=QQ,orthonormalize=False)
1157 sage: J.matrix_space()
1158 Module of 1 by 1 matrices with entries in Quaternion
1159 Algebra (-1, -1) with base ring Rational Field over
1160 the scalar ring Rational Field
1161
1162 """
1163 return self._matrix_space
1164
1165
1166 @cached_method
1167 def one(self):
1168 """
1169 Return the unit element of this algebra.
1170
1171 SETUP::
1172
1173 sage: from mjo.eja.eja_algebra import (HadamardEJA,
1174 ....: random_eja)
1175
1176 EXAMPLES:
1177
1178 We can compute unit element in the Hadamard EJA::
1179
1180 sage: J = HadamardEJA(5)
1181 sage: J.one()
1182 b0 + b1 + b2 + b3 + b4
1183
1184 The unit element in the Hadamard EJA is inherited in the
1185 subalgebras generated by its elements::
1186
1187 sage: J = HadamardEJA(5)
1188 sage: J.one()
1189 b0 + b1 + b2 + b3 + b4
1190 sage: x = sum(J.gens())
1191 sage: A = x.subalgebra_generated_by(orthonormalize=False)
1192 sage: A.one()
1193 c0
1194 sage: A.one().superalgebra_element()
1195 b0 + b1 + b2 + b3 + b4
1196
1197 TESTS:
1198
1199 The identity element acts like the identity, regardless of
1200 whether or not we orthonormalize::
1201
1202 sage: set_random_seed()
1203 sage: J = random_eja()
1204 sage: x = J.random_element()
1205 sage: J.one()*x == x and x*J.one() == x
1206 True
1207 sage: A = x.subalgebra_generated_by()
1208 sage: y = A.random_element()
1209 sage: A.one()*y == y and y*A.one() == y
1210 True
1211
1212 ::
1213
1214 sage: set_random_seed()
1215 sage: J = random_eja(field=QQ, orthonormalize=False)
1216 sage: x = J.random_element()
1217 sage: J.one()*x == x and x*J.one() == x
1218 True
1219 sage: A = x.subalgebra_generated_by(orthonormalize=False)
1220 sage: y = A.random_element()
1221 sage: A.one()*y == y and y*A.one() == y
1222 True
1223
1224 The matrix of the unit element's operator is the identity,
1225 regardless of the base field and whether or not we
1226 orthonormalize::
1227
1228 sage: set_random_seed()
1229 sage: J = random_eja()
1230 sage: actual = J.one().operator().matrix()
1231 sage: expected = matrix.identity(J.base_ring(), J.dimension())
1232 sage: actual == expected
1233 True
1234 sage: x = J.random_element()
1235 sage: A = x.subalgebra_generated_by()
1236 sage: actual = A.one().operator().matrix()
1237 sage: expected = matrix.identity(A.base_ring(), A.dimension())
1238 sage: actual == expected
1239 True
1240
1241 ::
1242
1243 sage: set_random_seed()
1244 sage: J = random_eja(field=QQ, orthonormalize=False)
1245 sage: actual = J.one().operator().matrix()
1246 sage: expected = matrix.identity(J.base_ring(), J.dimension())
1247 sage: actual == expected
1248 True
1249 sage: x = J.random_element()
1250 sage: A = x.subalgebra_generated_by(orthonormalize=False)
1251 sage: actual = A.one().operator().matrix()
1252 sage: expected = matrix.identity(A.base_ring(), A.dimension())
1253 sage: actual == expected
1254 True
1255
1256 Ensure that the cached unit element (often precomputed by
1257 hand) agrees with the computed one::
1258
1259 sage: set_random_seed()
1260 sage: J = random_eja()
1261 sage: cached = J.one()
1262 sage: J.one.clear_cache()
1263 sage: J.one() == cached
1264 True
1265
1266 ::
1267
1268 sage: set_random_seed()
1269 sage: J = random_eja(field=QQ, orthonormalize=False)
1270 sage: cached = J.one()
1271 sage: J.one.clear_cache()
1272 sage: J.one() == cached
1273 True
1274
1275 """
1276 # We can brute-force compute the matrices of the operators
1277 # that correspond to the basis elements of this algebra.
1278 # If some linear combination of those basis elements is the
1279 # algebra identity, then the same linear combination of
1280 # their matrices has to be the identity matrix.
1281 #
1282 # Of course, matrices aren't vectors in sage, so we have to
1283 # appeal to the "long vectors" isometry.
1284 oper_vecs = [ _mat2vec(g.operator().matrix()) for g in self.gens() ]
1285
1286 # Now we use basic linear algebra to find the coefficients,
1287 # of the matrices-as-vectors-linear-combination, which should
1288 # work for the original algebra basis too.
1289 A = matrix(self.base_ring(), oper_vecs)
1290
1291 # We used the isometry on the left-hand side already, but we
1292 # still need to do it for the right-hand side. Recall that we
1293 # wanted something that summed to the identity matrix.
1294 b = _mat2vec( matrix.identity(self.base_ring(), self.dimension()) )
1295
1296 # Now if there's an identity element in the algebra, this
1297 # should work. We solve on the left to avoid having to
1298 # transpose the matrix "A".
1299 return self.from_vector(A.solve_left(b))
1300
1301
1302 def peirce_decomposition(self, c):
1303 """
1304 The Peirce decomposition of this algebra relative to the
1305 idempotent ``c``.
1306
1307 In the future, this can be extended to a complete system of
1308 orthogonal idempotents.
1309
1310 INPUT:
1311
1312 - ``c`` -- an idempotent of this algebra.
1313
1314 OUTPUT:
1315
1316 A triple (J0, J5, J1) containing two subalgebras and one subspace
1317 of this algebra,
1318
1319 - ``J0`` -- the algebra on the eigenspace of ``c.operator()``
1320 corresponding to the eigenvalue zero.
1321
1322 - ``J5`` -- the eigenspace (NOT a subalgebra) of ``c.operator()``
1323 corresponding to the eigenvalue one-half.
1324
1325 - ``J1`` -- the algebra on the eigenspace of ``c.operator()``
1326 corresponding to the eigenvalue one.
1327
1328 These are the only possible eigenspaces for that operator, and this
1329 algebra is a direct sum of them. The spaces ``J0`` and ``J1`` are
1330 orthogonal, and are subalgebras of this algebra with the appropriate
1331 restrictions.
1332
1333 SETUP::
1334
1335 sage: from mjo.eja.eja_algebra import random_eja, RealSymmetricEJA
1336
1337 EXAMPLES:
1338
1339 The canonical example comes from the symmetric matrices, which
1340 decompose into diagonal and off-diagonal parts::
1341
1342 sage: J = RealSymmetricEJA(3)
1343 sage: C = matrix(QQ, [ [1,0,0],
1344 ....: [0,1,0],
1345 ....: [0,0,0] ])
1346 sage: c = J(C)
1347 sage: J0,J5,J1 = J.peirce_decomposition(c)
1348 sage: J0
1349 Euclidean Jordan algebra of dimension 1...
1350 sage: J5
1351 Vector space of degree 6 and dimension 2...
1352 sage: J1
1353 Euclidean Jordan algebra of dimension 3...
1354 sage: J0.one().to_matrix()
1355 [0 0 0]
1356 [0 0 0]
1357 [0 0 1]
1358 sage: orig_df = AA.options.display_format
1359 sage: AA.options.display_format = 'radical'
1360 sage: J.from_vector(J5.basis()[0]).to_matrix()
1361 [ 0 0 1/2*sqrt(2)]
1362 [ 0 0 0]
1363 [1/2*sqrt(2) 0 0]
1364 sage: J.from_vector(J5.basis()[1]).to_matrix()
1365 [ 0 0 0]
1366 [ 0 0 1/2*sqrt(2)]
1367 [ 0 1/2*sqrt(2) 0]
1368 sage: AA.options.display_format = orig_df
1369 sage: J1.one().to_matrix()
1370 [1 0 0]
1371 [0 1 0]
1372 [0 0 0]
1373
1374 TESTS:
1375
1376 Every algebra decomposes trivially with respect to its identity
1377 element::
1378
1379 sage: set_random_seed()
1380 sage: J = random_eja()
1381 sage: J0,J5,J1 = J.peirce_decomposition(J.one())
1382 sage: J0.dimension() == 0 and J5.dimension() == 0
1383 True
1384 sage: J1.superalgebra() == J and J1.dimension() == J.dimension()
1385 True
1386
1387 The decomposition is into eigenspaces, and its components are
1388 therefore necessarily orthogonal. Moreover, the identity
1389 elements in the two subalgebras are the projections onto their
1390 respective subspaces of the superalgebra's identity element::
1391
1392 sage: set_random_seed()
1393 sage: J = random_eja()
1394 sage: x = J.random_element()
1395 sage: if not J.is_trivial():
1396 ....: while x.is_nilpotent():
1397 ....: x = J.random_element()
1398 sage: c = x.subalgebra_idempotent()
1399 sage: J0,J5,J1 = J.peirce_decomposition(c)
1400 sage: ipsum = 0
1401 sage: for (w,y,z) in zip(J0.basis(), J5.basis(), J1.basis()):
1402 ....: w = w.superalgebra_element()
1403 ....: y = J.from_vector(y)
1404 ....: z = z.superalgebra_element()
1405 ....: ipsum += w.inner_product(y).abs()
1406 ....: ipsum += w.inner_product(z).abs()
1407 ....: ipsum += y.inner_product(z).abs()
1408 sage: ipsum
1409 0
1410 sage: J1(c) == J1.one()
1411 True
1412 sage: J0(J.one() - c) == J0.one()
1413 True
1414
1415 """
1416 if not c.is_idempotent():
1417 raise ValueError("element is not idempotent: %s" % c)
1418
1419 # Default these to what they should be if they turn out to be
1420 # trivial, because eigenspaces_left() won't return eigenvalues
1421 # corresponding to trivial spaces (e.g. it returns only the
1422 # eigenspace corresponding to lambda=1 if you take the
1423 # decomposition relative to the identity element).
1424 trivial = self.subalgebra((), check_axioms=False)
1425 J0 = trivial # eigenvalue zero
1426 J5 = VectorSpace(self.base_ring(), 0) # eigenvalue one-half
1427 J1 = trivial # eigenvalue one
1428
1429 for (eigval, eigspace) in c.operator().matrix().right_eigenspaces():
1430 if eigval == ~(self.base_ring()(2)):
1431 J5 = eigspace
1432 else:
1433 gens = tuple( self.from_vector(b) for b in eigspace.basis() )
1434 subalg = self.subalgebra(gens, check_axioms=False)
1435 if eigval == 0:
1436 J0 = subalg
1437 elif eigval == 1:
1438 J1 = subalg
1439 else:
1440 raise ValueError("unexpected eigenvalue: %s" % eigval)
1441
1442 return (J0, J5, J1)
1443
1444
1445 def random_element(self, thorough=False):
1446 r"""
1447 Return a random element of this algebra.
1448
1449 Our algebra superclass method only returns a linear
1450 combination of at most two basis elements. We instead
1451 want the vector space "random element" method that
1452 returns a more diverse selection.
1453
1454 INPUT:
1455
1456 - ``thorough`` -- (boolean; default False) whether or not we
1457 should generate irrational coefficients for the random
1458 element when our base ring is irrational; this slows the
1459 algebra operations to a crawl, but any truly random method
1460 should include them
1461
1462 """
1463 # For a general base ring... maybe we can trust this to do the
1464 # right thing? Unlikely, but.
1465 V = self.vector_space()
1466 v = V.random_element()
1467
1468 if self.base_ring() is AA:
1469 # The "random element" method of the algebraic reals is
1470 # stupid at the moment, and only returns integers between
1471 # -2 and 2, inclusive:
1472 #
1473 # https://trac.sagemath.org/ticket/30875
1474 #
1475 # Instead, we implement our own "random vector" method,
1476 # and then coerce that into the algebra. We use the vector
1477 # space degree here instead of the dimension because a
1478 # subalgebra could (for example) be spanned by only two
1479 # vectors, each with five coordinates. We need to
1480 # generate all five coordinates.
1481 if thorough:
1482 v *= QQbar.random_element().real()
1483 else:
1484 v *= QQ.random_element()
1485
1486 return self.from_vector(V.coordinate_vector(v))
1487
1488 def random_elements(self, count, thorough=False):
1489 """
1490 Return ``count`` random elements as a tuple.
1491
1492 INPUT:
1493
1494 - ``thorough`` -- (boolean; default False) whether or not we
1495 should generate irrational coefficients for the random
1496 elements when our base ring is irrational; this slows the
1497 algebra operations to a crawl, but any truly random method
1498 should include them
1499
1500 SETUP::
1501
1502 sage: from mjo.eja.eja_algebra import JordanSpinEJA
1503
1504 EXAMPLES::
1505
1506 sage: J = JordanSpinEJA(3)
1507 sage: x,y,z = J.random_elements(3)
1508 sage: all( [ x in J, y in J, z in J ])
1509 True
1510 sage: len( J.random_elements(10) ) == 10
1511 True
1512
1513 """
1514 return tuple( self.random_element(thorough)
1515 for idx in range(count) )
1516
1517
1518 @cached_method
1519 def _charpoly_coefficients(self):
1520 r"""
1521 The `r` polynomial coefficients of the "characteristic polynomial
1522 of" function.
1523
1524 SETUP::
1525
1526 sage: from mjo.eja.eja_algebra import random_eja
1527
1528 TESTS:
1529
1530 The theory shows that these are all homogeneous polynomials of
1531 a known degree::
1532
1533 sage: set_random_seed()
1534 sage: J = random_eja()
1535 sage: all(p.is_homogeneous() for p in J._charpoly_coefficients())
1536 True
1537
1538 """
1539 n = self.dimension()
1540 R = self.coordinate_polynomial_ring()
1541 vars = R.gens()
1542 F = R.fraction_field()
1543
1544 def L_x_i_j(i,j):
1545 # From a result in my book, these are the entries of the
1546 # basis representation of L_x.
1547 return sum( vars[k]*self.monomial(k).operator().matrix()[i,j]
1548 for k in range(n) )
1549
1550 L_x = matrix(F, n, n, L_x_i_j)
1551
1552 r = None
1553 if self.rank.is_in_cache():
1554 r = self.rank()
1555 # There's no need to pad the system with redundant
1556 # columns if we *know* they'll be redundant.
1557 n = r
1558
1559 # Compute an extra power in case the rank is equal to
1560 # the dimension (otherwise, we would stop at x^(r-1)).
1561 x_powers = [ (L_x**k)*self.one().to_vector()
1562 for k in range(n+1) ]
1563 A = matrix.column(F, x_powers[:n])
1564 AE = A.extended_echelon_form()
1565 E = AE[:,n:]
1566 A_rref = AE[:,:n]
1567 if r is None:
1568 r = A_rref.rank()
1569 b = x_powers[r]
1570
1571 # The theory says that only the first "r" coefficients are
1572 # nonzero, and they actually live in the original polynomial
1573 # ring and not the fraction field. We negate them because in
1574 # the actual characteristic polynomial, they get moved to the
1575 # other side where x^r lives. We don't bother to trim A_rref
1576 # down to a square matrix and solve the resulting system,
1577 # because the upper-left r-by-r portion of A_rref is
1578 # guaranteed to be the identity matrix, so e.g.
1579 #
1580 # A_rref.solve_right(Y)
1581 #
1582 # would just be returning Y.
1583 return (-E*b)[:r].change_ring(R)
1584
1585 @cached_method
1586 def rank(self):
1587 r"""
1588 Return the rank of this EJA.
1589
1590 This is a cached method because we know the rank a priori for
1591 all of the algebras we can construct. Thus we can avoid the
1592 expensive ``_charpoly_coefficients()`` call unless we truly
1593 need to compute the whole characteristic polynomial.
1594
1595 SETUP::
1596
1597 sage: from mjo.eja.eja_algebra import (HadamardEJA,
1598 ....: JordanSpinEJA,
1599 ....: RealSymmetricEJA,
1600 ....: ComplexHermitianEJA,
1601 ....: QuaternionHermitianEJA,
1602 ....: random_eja)
1603
1604 EXAMPLES:
1605
1606 The rank of the Jordan spin algebra is always two::
1607
1608 sage: JordanSpinEJA(2).rank()
1609 2
1610 sage: JordanSpinEJA(3).rank()
1611 2
1612 sage: JordanSpinEJA(4).rank()
1613 2
1614
1615 The rank of the `n`-by-`n` Hermitian real, complex, or
1616 quaternion matrices is `n`::
1617
1618 sage: RealSymmetricEJA(4).rank()
1619 4
1620 sage: ComplexHermitianEJA(3).rank()
1621 3
1622 sage: QuaternionHermitianEJA(2).rank()
1623 2
1624
1625 TESTS:
1626
1627 Ensure that every EJA that we know how to construct has a
1628 positive integer rank, unless the algebra is trivial in
1629 which case its rank will be zero::
1630
1631 sage: set_random_seed()
1632 sage: J = random_eja()
1633 sage: r = J.rank()
1634 sage: r in ZZ
1635 True
1636 sage: r > 0 or (r == 0 and J.is_trivial())
1637 True
1638
1639 Ensure that computing the rank actually works, since the ranks
1640 of all simple algebras are known and will be cached by default::
1641
1642 sage: set_random_seed() # long time
1643 sage: J = random_eja() # long time
1644 sage: cached = J.rank() # long time
1645 sage: J.rank.clear_cache() # long time
1646 sage: J.rank() == cached # long time
1647 True
1648
1649 """
1650 return len(self._charpoly_coefficients())
1651
1652
1653 def subalgebra(self, basis, **kwargs):
1654 r"""
1655 Create a subalgebra of this algebra from the given basis.
1656 """
1657 from mjo.eja.eja_subalgebra import FiniteDimensionalEJASubalgebra
1658 return FiniteDimensionalEJASubalgebra(self, basis, **kwargs)
1659
1660
1661 def vector_space(self):
1662 """
1663 Return the vector space that underlies this algebra.
1664
1665 SETUP::
1666
1667 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1668
1669 EXAMPLES::
1670
1671 sage: J = RealSymmetricEJA(2)
1672 sage: J.vector_space()
1673 Vector space of dimension 3 over...
1674
1675 """
1676 return self.zero().to_vector().parent().ambient_vector_space()
1677
1678
1679
1680 class RationalBasisEJA(FiniteDimensionalEJA):
1681 r"""
1682 Algebras whose supplied basis elements have all rational entries.
1683
1684 SETUP::
1685
1686 sage: from mjo.eja.eja_algebra import BilinearFormEJA
1687
1688 EXAMPLES:
1689
1690 The supplied basis is orthonormalized by default::
1691
1692 sage: B = matrix(QQ, [[1, 0, 0], [0, 25, -32], [0, -32, 41]])
1693 sage: J = BilinearFormEJA(B)
1694 sage: J.matrix_basis()
1695 (
1696 [1] [ 0] [ 0]
1697 [0] [1/5] [32/5]
1698 [0], [ 0], [ 5]
1699 )
1700
1701 """
1702 def __init__(self,
1703 basis,
1704 jordan_product,
1705 inner_product,
1706 field=AA,
1707 check_field=True,
1708 **kwargs):
1709
1710 if check_field:
1711 # Abuse the check_field parameter to check that the entries of
1712 # out basis (in ambient coordinates) are in the field QQ.
1713 # Use _all2list to get the vector coordinates of octonion
1714 # entries and not the octonions themselves (which are not
1715 # rational).
1716 if not all( all(b_i in QQ for b_i in _all2list(b))
1717 for b in basis ):
1718 raise TypeError("basis not rational")
1719
1720 super().__init__(basis,
1721 jordan_product,
1722 inner_product,
1723 field=field,
1724 check_field=check_field,
1725 **kwargs)
1726
1727 self._rational_algebra = None
1728 if field is not QQ:
1729 # There's no point in constructing the extra algebra if this
1730 # one is already rational.
1731 #
1732 # Note: the same Jordan and inner-products work here,
1733 # because they are necessarily defined with respect to
1734 # ambient coordinates and not any particular basis.
1735 self._rational_algebra = FiniteDimensionalEJA(
1736 basis,
1737 jordan_product,
1738 inner_product,
1739 field=QQ,
1740 matrix_space=self.matrix_space(),
1741 associative=self.is_associative(),
1742 orthonormalize=False,
1743 check_field=False,
1744 check_axioms=False)
1745
1746 def rational_algebra(self):
1747 # Using None as a flag here (rather than just assigning "self"
1748 # to self._rational_algebra by default) feels a little bit
1749 # more sane to me in a garbage-collected environment.
1750 if self._rational_algebra is None:
1751 return self
1752 else:
1753 return self._rational_algebra
1754
1755 @cached_method
1756 def _charpoly_coefficients(self):
1757 r"""
1758 SETUP::
1759
1760 sage: from mjo.eja.eja_algebra import (BilinearFormEJA,
1761 ....: JordanSpinEJA)
1762
1763 EXAMPLES:
1764
1765 The base ring of the resulting polynomial coefficients is what
1766 it should be, and not the rationals (unless the algebra was
1767 already over the rationals)::
1768
1769 sage: J = JordanSpinEJA(3)
1770 sage: J._charpoly_coefficients()
1771 (X1^2 - X2^2 - X3^2, -2*X1)
1772 sage: a0 = J._charpoly_coefficients()[0]
1773 sage: J.base_ring()
1774 Algebraic Real Field
1775 sage: a0.base_ring()
1776 Algebraic Real Field
1777
1778 """
1779 if self.rational_algebra() is self:
1780 # Bypass the hijinks if they won't benefit us.
1781 return super()._charpoly_coefficients()
1782
1783 # Do the computation over the rationals. The answer will be
1784 # the same, because all we've done is a change of basis.
1785 # Then, change back from QQ to our real base ring
1786 a = ( a_i.change_ring(self.base_ring())
1787 for a_i in self.rational_algebra()._charpoly_coefficients() )
1788
1789 # Otherwise, convert the coordinate variables back to the
1790 # deorthonormalized ones.
1791 R = self.coordinate_polynomial_ring()
1792 from sage.modules.free_module_element import vector
1793 X = vector(R, R.gens())
1794 BX = self._deortho_matrix*X
1795
1796 subs_dict = { X[i]: BX[i] for i in range(len(X)) }
1797 return tuple( a_i.subs(subs_dict) for a_i in a )
1798
1799 class ConcreteEJA(FiniteDimensionalEJA):
1800 r"""
1801 A class for the Euclidean Jordan algebras that we know by name.
1802
1803 These are the Jordan algebras whose basis, multiplication table,
1804 rank, and so on are known a priori. More to the point, they are
1805 the Euclidean Jordan algebras for which we are able to conjure up
1806 a "random instance."
1807
1808 SETUP::
1809
1810 sage: from mjo.eja.eja_algebra import ConcreteEJA
1811
1812 TESTS:
1813
1814 Our basis is normalized with respect to the algebra's inner
1815 product, unless we specify otherwise::
1816
1817 sage: set_random_seed()
1818 sage: J = ConcreteEJA.random_instance()
1819 sage: all( b.norm() == 1 for b in J.gens() )
1820 True
1821
1822 Since our basis is orthonormal with respect to the algebra's inner
1823 product, and since we know that this algebra is an EJA, any
1824 left-multiplication operator's matrix will be symmetric because
1825 natural->EJA basis representation is an isometry and within the
1826 EJA the operator is self-adjoint by the Jordan axiom::
1827
1828 sage: set_random_seed()
1829 sage: J = ConcreteEJA.random_instance()
1830 sage: x = J.random_element()
1831 sage: x.operator().is_self_adjoint()
1832 True
1833 """
1834
1835 @staticmethod
1836 def _max_random_instance_dimension():
1837 r"""
1838 The maximum dimension of any random instance. Ten dimensions seems
1839 to be about the point where everything takes a turn for the
1840 worse. And dimension ten (but not nine) allows the 4-by-4 real
1841 Hermitian matrices, the 2-by-2 quaternion Hermitian matrices,
1842 and the 2-by-2 octonion Hermitian matrices.
1843 """
1844 return 10
1845
1846 @staticmethod
1847 def _max_random_instance_size(max_dimension):
1848 """
1849 Return an integer "size" that is an upper bound on the size of
1850 this algebra when it is used in a random test case. This size
1851 (which can be passed to the algebra's constructor) is itself
1852 based on the ``max_dimension`` parameter.
1853
1854 This method must be implemented in each subclass.
1855 """
1856 raise NotImplementedError
1857
1858 @classmethod
1859 def random_instance(cls, max_dimension=None, *args, **kwargs):
1860 """
1861 Return a random instance of this type of algebra whose dimension
1862 is less than or equal to the lesser of ``max_dimension`` and
1863 the value returned by ``_max_random_instance_dimension()``. If
1864 the dimension bound is omitted, then only the
1865 ``_max_random_instance_dimension()`` is used as a bound.
1866
1867 This method should be implemented in each subclass.
1868
1869 SETUP::
1870
1871 sage: from mjo.eja.eja_algebra import ConcreteEJA
1872
1873 TESTS:
1874
1875 Both the class bound and the ``max_dimension`` argument are upper
1876 bounds on the dimension of the algebra returned::
1877
1878 sage: from sage.misc.prandom import choice
1879 sage: eja_class = choice(ConcreteEJA.__subclasses__())
1880 sage: class_max_d = eja_class._max_random_instance_dimension()
1881 sage: J = eja_class.random_instance(max_dimension=20,
1882 ....: field=QQ,
1883 ....: orthonormalize=False)
1884 sage: J.dimension() <= class_max_d
1885 True
1886 sage: J = eja_class.random_instance(max_dimension=2,
1887 ....: field=QQ,
1888 ....: orthonormalize=False)
1889 sage: J.dimension() <= 2
1890 True
1891
1892 """
1893 from sage.misc.prandom import choice
1894 eja_class = choice(cls.__subclasses__())
1895
1896 # These all bubble up to the RationalBasisEJA superclass
1897 # constructor, so any (kw)args valid there are also valid
1898 # here.
1899 return eja_class.random_instance(max_dimension, *args, **kwargs)
1900
1901
1902 class MatrixEJA(FiniteDimensionalEJA):
1903 @staticmethod
1904 def _denormalized_basis(A):
1905 """
1906 Returns a basis for the space of complex Hermitian n-by-n matrices.
1907
1908 Why do we embed these? Basically, because all of numerical linear
1909 algebra assumes that you're working with vectors consisting of `n`
1910 entries from a field and scalars from the same field. There's no way
1911 to tell SageMath that (for example) the vectors contain complex
1912 numbers, while the scalar field is real.
1913
1914 SETUP::
1915
1916 sage: from mjo.hurwitz import (ComplexMatrixAlgebra,
1917 ....: QuaternionMatrixAlgebra,
1918 ....: OctonionMatrixAlgebra)
1919 sage: from mjo.eja.eja_algebra import MatrixEJA
1920
1921 TESTS::
1922
1923 sage: set_random_seed()
1924 sage: n = ZZ.random_element(1,5)
1925 sage: A = MatrixSpace(QQ, n)
1926 sage: B = MatrixEJA._denormalized_basis(A)
1927 sage: all( M.is_hermitian() for M in B)
1928 True
1929
1930 ::
1931
1932 sage: set_random_seed()
1933 sage: n = ZZ.random_element(1,5)
1934 sage: A = ComplexMatrixAlgebra(n, scalars=QQ)
1935 sage: B = MatrixEJA._denormalized_basis(A)
1936 sage: all( M.is_hermitian() for M in B)
1937 True
1938
1939 ::
1940
1941 sage: set_random_seed()
1942 sage: n = ZZ.random_element(1,5)
1943 sage: A = QuaternionMatrixAlgebra(n, scalars=QQ)
1944 sage: B = MatrixEJA._denormalized_basis(A)
1945 sage: all( M.is_hermitian() for M in B )
1946 True
1947
1948 ::
1949
1950 sage: set_random_seed()
1951 sage: n = ZZ.random_element(1,5)
1952 sage: A = OctonionMatrixAlgebra(n, scalars=QQ)
1953 sage: B = MatrixEJA._denormalized_basis(A)
1954 sage: all( M.is_hermitian() for M in B )
1955 True
1956
1957 """
1958 # These work for real MatrixSpace, whose monomials only have
1959 # two coordinates (because the last one would always be "1").
1960 es = A.base_ring().gens()
1961 gen = lambda A,m: A.monomial(m[:2])
1962
1963 if hasattr(A, 'entry_algebra_gens'):
1964 # We've got a MatrixAlgebra, and its monomials will have
1965 # three coordinates.
1966 es = A.entry_algebra_gens()
1967 gen = lambda A,m: A.monomial(m)
1968
1969 basis = []
1970 for i in range(A.nrows()):
1971 for j in range(i+1):
1972 if i == j:
1973 E_ii = gen(A, (i,j,es[0]))
1974 basis.append(E_ii)
1975 else:
1976 for e in es:
1977 E_ij = gen(A, (i,j,e))
1978 E_ij += E_ij.conjugate_transpose()
1979 basis.append(E_ij)
1980
1981 return tuple( basis )
1982
1983 @staticmethod
1984 def jordan_product(X,Y):
1985 return (X*Y + Y*X)/2
1986
1987 @staticmethod
1988 def trace_inner_product(X,Y):
1989 r"""
1990 A trace inner-product for matrices that aren't embedded in the
1991 reals. It takes MATRICES as arguments, not EJA elements.
1992
1993 SETUP::
1994
1995 sage: from mjo.eja.eja_algebra import (RealSymmetricEJA,
1996 ....: ComplexHermitianEJA,
1997 ....: QuaternionHermitianEJA,
1998 ....: OctonionHermitianEJA)
1999
2000 EXAMPLES::
2001
2002 sage: J = RealSymmetricEJA(2,field=QQ,orthonormalize=False)
2003 sage: I = J.one().to_matrix()
2004 sage: J.trace_inner_product(I, -I)
2005 -2
2006
2007 ::
2008
2009 sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
2010 sage: I = J.one().to_matrix()
2011 sage: J.trace_inner_product(I, -I)
2012 -2
2013
2014 ::
2015
2016 sage: J = QuaternionHermitianEJA(2,field=QQ,orthonormalize=False)
2017 sage: I = J.one().to_matrix()
2018 sage: J.trace_inner_product(I, -I)
2019 -2
2020
2021 ::
2022
2023 sage: J = OctonionHermitianEJA(2,field=QQ,orthonormalize=False)
2024 sage: I = J.one().to_matrix()
2025 sage: J.trace_inner_product(I, -I)
2026 -2
2027
2028 """
2029 tr = (X*Y).trace()
2030 if hasattr(tr, 'coefficient'):
2031 # Works for octonions, and has to come first because they
2032 # also have a "real()" method that doesn't return an
2033 # element of the scalar ring.
2034 return tr.coefficient(0)
2035 elif hasattr(tr, 'coefficient_tuple'):
2036 # Works for quaternions.
2037 return tr.coefficient_tuple()[0]
2038
2039 # Works for real and complex numbers.
2040 return tr.real()
2041
2042
2043 def __init__(self, matrix_space, **kwargs):
2044 # We know this is a valid EJA, but will double-check
2045 # if the user passes check_axioms=True.
2046 if "check_axioms" not in kwargs: kwargs["check_axioms"] = False
2047
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,
2053 **kwargs)
2054
2055 self.rank.set_cache(matrix_space.nrows())
2056 self.one.set_cache( self(matrix_space.one()) )
2057
2058 class RealSymmetricEJA(MatrixEJA, RationalBasisEJA, ConcreteEJA):
2059 """
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.
2063
2064 SETUP::
2065
2066 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
2067
2068 EXAMPLES::
2069
2070 sage: J = RealSymmetricEJA(2)
2071 sage: b0, b1, b2 = J.gens()
2072 sage: b0*b0
2073 b0
2074 sage: b1*b1
2075 1/2*b0 + 1/2*b2
2076 sage: b2*b2
2077 b2
2078
2079 In theory, our "field" can be any subfield of the reals::
2080
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
2086
2087 TESTS:
2088
2089 The dimension of this algebra is `(n^2 + n) / 2`::
2090
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
2096 True
2097
2098 The Jordan multiplication is what we think it is::
2099
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
2108 True
2109 sage: J(expected) == x*y
2110 True
2111
2112 We can change the generator prefix::
2113
2114 sage: RealSymmetricEJA(3, prefix='q').gens()
2115 (q0, q1, q2, q3, q4, q5)
2116
2117 We can construct the (trivial) algebra of rank zero::
2118
2119 sage: RealSymmetricEJA(0)
2120 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2121
2122 """
2123 @staticmethod
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))
2128
2129 @classmethod
2130 def random_instance(cls, max_dimension=None, *args, **kwargs):
2131 """
2132 Return a random instance of this type of algebra.
2133 """
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)
2140
2141 def __init__(self, n, field=AA, **kwargs):
2142 A = MatrixSpace(field, n)
2143 super().__init__(A, **kwargs)
2144
2145 from mjo.eja.eja_cache import real_symmetric_eja_coeffs
2146 a = real_symmetric_eja_coeffs(self)
2147 if a is not None:
2148 self.rational_algebra()._charpoly_coefficients.set_cache(a)
2149
2150
2151
2152 class ComplexHermitianEJA(MatrixEJA, RationalBasisEJA, ConcreteEJA):
2153 """
2154 The rank-n simple EJA consisting of complex Hermitian n-by-n
2155 matrices over the real numbers, the usual symmetric Jordan product,
2156 and the real-part-of-trace inner product. It has dimension `n^2` over
2157 the reals.
2158
2159 SETUP::
2160
2161 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
2162
2163 EXAMPLES:
2164
2165 In theory, our "field" can be any subfield of the reals, but we
2166 can't use inexact real fields at the moment because SageMath
2167 doesn't know how to convert their elements into complex numbers,
2168 or even into algebraic reals::
2169
2170 sage: QQbar(RDF(1))
2171 Traceback (most recent call last):
2172 ...
2173 TypeError: Illegal initializer for algebraic number
2174 sage: AA(RR(1))
2175 Traceback (most recent call last):
2176 ...
2177 TypeError: Illegal initializer for algebraic number
2178
2179 This causes the following error when we try to scale a matrix of
2180 complex numbers by an inexact real number::
2181
2182 sage: ComplexHermitianEJA(2,field=RR)
2183 Traceback (most recent call last):
2184 ...
2185 TypeError: Unable to coerce entries (=(1.00000000000000,
2186 -0.000000000000000)) to coefficients in Algebraic Real Field
2187
2188 TESTS:
2189
2190 The dimension of this algebra is `n^2`::
2191
2192 sage: set_random_seed()
2193 sage: d = ComplexHermitianEJA._max_random_instance_dimension()
2194 sage: n = ComplexHermitianEJA._max_random_instance_size(d)
2195 sage: J = ComplexHermitianEJA(n)
2196 sage: J.dimension() == n^2
2197 True
2198
2199 The Jordan multiplication is what we think it is::
2200
2201 sage: set_random_seed()
2202 sage: J = ComplexHermitianEJA.random_instance()
2203 sage: x,y = J.random_elements(2)
2204 sage: actual = (x*y).to_matrix()
2205 sage: X = x.to_matrix()
2206 sage: Y = y.to_matrix()
2207 sage: expected = (X*Y + Y*X)/2
2208 sage: actual == expected
2209 True
2210 sage: J(expected) == x*y
2211 True
2212
2213 We can change the generator prefix::
2214
2215 sage: ComplexHermitianEJA(2, prefix='z').gens()
2216 (z0, z1, z2, z3)
2217
2218 We can construct the (trivial) algebra of rank zero::
2219
2220 sage: ComplexHermitianEJA(0)
2221 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2222
2223 """
2224 def __init__(self, n, field=AA, **kwargs):
2225 from mjo.hurwitz import ComplexMatrixAlgebra
2226 A = ComplexMatrixAlgebra(n, scalars=field)
2227 super().__init__(A, **kwargs)
2228
2229 from mjo.eja.eja_cache import complex_hermitian_eja_coeffs
2230 a = complex_hermitian_eja_coeffs(self)
2231 if a is not None:
2232 self.rational_algebra()._charpoly_coefficients.set_cache(a)
2233
2234 @staticmethod
2235 def _max_random_instance_size(max_dimension):
2236 # Obtained by solving d = n^2.
2237 # The ZZ-int-ZZ thing is just "floor."
2238 return ZZ(int(ZZ(max_dimension).sqrt()))
2239
2240 @classmethod
2241 def random_instance(cls, max_dimension=None, *args, **kwargs):
2242 """
2243 Return a random instance of this type of algebra.
2244 """
2245 class_max_d = cls._max_random_instance_dimension()
2246 if (max_dimension is None or max_dimension > class_max_d):
2247 max_dimension = class_max_d
2248 max_size = cls._max_random_instance_size(max_dimension)
2249 n = ZZ.random_element(max_size + 1)
2250 return cls(n, **kwargs)
2251
2252
2253 class QuaternionHermitianEJA(MatrixEJA, RationalBasisEJA, ConcreteEJA):
2254 r"""
2255 The rank-n simple EJA consisting of self-adjoint n-by-n quaternion
2256 matrices, the usual symmetric Jordan product, and the
2257 real-part-of-trace inner product. It has dimension `2n^2 - n` over
2258 the reals.
2259
2260 SETUP::
2261
2262 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
2263
2264 EXAMPLES:
2265
2266 In theory, our "field" can be any subfield of the reals::
2267
2268 sage: QuaternionHermitianEJA(2, field=RDF, check_axioms=True)
2269 Euclidean Jordan algebra of dimension 6 over Real Double Field
2270 sage: QuaternionHermitianEJA(2, field=RR, check_axioms=True)
2271 Euclidean Jordan algebra of dimension 6 over Real Field with
2272 53 bits of precision
2273
2274 TESTS:
2275
2276 The dimension of this algebra is `2*n^2 - n`::
2277
2278 sage: set_random_seed()
2279 sage: d = QuaternionHermitianEJA._max_random_instance_dimension()
2280 sage: n = QuaternionHermitianEJA._max_random_instance_size(d)
2281 sage: J = QuaternionHermitianEJA(n)
2282 sage: J.dimension() == 2*(n^2) - n
2283 True
2284
2285 The Jordan multiplication is what we think it is::
2286
2287 sage: set_random_seed()
2288 sage: J = QuaternionHermitianEJA.random_instance()
2289 sage: x,y = J.random_elements(2)
2290 sage: actual = (x*y).to_matrix()
2291 sage: X = x.to_matrix()
2292 sage: Y = y.to_matrix()
2293 sage: expected = (X*Y + Y*X)/2
2294 sage: actual == expected
2295 True
2296 sage: J(expected) == x*y
2297 True
2298
2299 We can change the generator prefix::
2300
2301 sage: QuaternionHermitianEJA(2, prefix='a').gens()
2302 (a0, a1, a2, a3, a4, a5)
2303
2304 We can construct the (trivial) algebra of rank zero::
2305
2306 sage: QuaternionHermitianEJA(0)
2307 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2308
2309 """
2310 def __init__(self, n, field=AA, **kwargs):
2311 from mjo.hurwitz import QuaternionMatrixAlgebra
2312 A = QuaternionMatrixAlgebra(n, scalars=field)
2313 super().__init__(A, **kwargs)
2314
2315 from mjo.eja.eja_cache import quaternion_hermitian_eja_coeffs
2316 a = quaternion_hermitian_eja_coeffs(self)
2317 if a is not None:
2318 self.rational_algebra()._charpoly_coefficients.set_cache(a)
2319
2320
2321
2322 @staticmethod
2323 def _max_random_instance_size(max_dimension):
2324 r"""
2325 The maximum rank of a random QuaternionHermitianEJA.
2326 """
2327 # Obtained by solving d = 2n^2 - n.
2328 # The ZZ-int-ZZ thing is just "floor."
2329 return ZZ(int(ZZ(8*max_dimension + 1).sqrt()/4 + 1/4))
2330
2331 @classmethod
2332 def random_instance(cls, max_dimension=None, *args, **kwargs):
2333 """
2334 Return a random instance of this type of algebra.
2335 """
2336 class_max_d = cls._max_random_instance_dimension()
2337 if (max_dimension is None or max_dimension > class_max_d):
2338 max_dimension = class_max_d
2339 max_size = cls._max_random_instance_size(max_dimension)
2340 n = ZZ.random_element(max_size + 1)
2341 return cls(n, **kwargs)
2342
2343 class OctonionHermitianEJA(MatrixEJA, RationalBasisEJA, ConcreteEJA):
2344 r"""
2345 SETUP::
2346
2347 sage: from mjo.eja.eja_algebra import (FiniteDimensionalEJA,
2348 ....: OctonionHermitianEJA)
2349 sage: from mjo.hurwitz import Octonions, OctonionMatrixAlgebra
2350
2351 EXAMPLES:
2352
2353 The 3-by-3 algebra satisfies the axioms of an EJA::
2354
2355 sage: OctonionHermitianEJA(3, # long time
2356 ....: field=QQ, # long time
2357 ....: orthonormalize=False, # long time
2358 ....: check_axioms=True) # long time
2359 Euclidean Jordan algebra of dimension 27 over Rational Field
2360
2361 After a change-of-basis, the 2-by-2 algebra has the same
2362 multiplication table as the ten-dimensional Jordan spin algebra::
2363
2364 sage: A = OctonionMatrixAlgebra(2,Octonions(QQ),QQ)
2365 sage: b = OctonionHermitianEJA._denormalized_basis(A)
2366 sage: basis = (b[0] + b[9],) + b[1:9] + (b[0] - b[9],)
2367 sage: jp = OctonionHermitianEJA.jordan_product
2368 sage: ip = OctonionHermitianEJA.trace_inner_product
2369 sage: J = FiniteDimensionalEJA(basis,
2370 ....: jp,
2371 ....: ip,
2372 ....: field=QQ,
2373 ....: orthonormalize=False)
2374 sage: J.multiplication_table()
2375 +----++----+----+----+----+----+----+----+----+----+----+
2376 | * || b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7 | b8 | b9 |
2377 +====++====+====+====+====+====+====+====+====+====+====+
2378 | b0 || b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7 | b8 | b9 |
2379 +----++----+----+----+----+----+----+----+----+----+----+
2380 | b1 || b1 | b0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2381 +----++----+----+----+----+----+----+----+----+----+----+
2382 | b2 || b2 | 0 | b0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2383 +----++----+----+----+----+----+----+----+----+----+----+
2384 | b3 || b3 | 0 | 0 | b0 | 0 | 0 | 0 | 0 | 0 | 0 |
2385 +----++----+----+----+----+----+----+----+----+----+----+
2386 | b4 || b4 | 0 | 0 | 0 | b0 | 0 | 0 | 0 | 0 | 0 |
2387 +----++----+----+----+----+----+----+----+----+----+----+
2388 | b5 || b5 | 0 | 0 | 0 | 0 | b0 | 0 | 0 | 0 | 0 |
2389 +----++----+----+----+----+----+----+----+----+----+----+
2390 | b6 || b6 | 0 | 0 | 0 | 0 | 0 | b0 | 0 | 0 | 0 |
2391 +----++----+----+----+----+----+----+----+----+----+----+
2392 | b7 || b7 | 0 | 0 | 0 | 0 | 0 | 0 | b0 | 0 | 0 |
2393 +----++----+----+----+----+----+----+----+----+----+----+
2394 | b8 || b8 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | b0 | 0 |
2395 +----++----+----+----+----+----+----+----+----+----+----+
2396 | b9 || b9 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | b0 |
2397 +----++----+----+----+----+----+----+----+----+----+----+
2398
2399 TESTS:
2400
2401 We can actually construct the 27-dimensional Albert algebra,
2402 and we get the right unit element if we recompute it::
2403
2404 sage: J = OctonionHermitianEJA(3, # long time
2405 ....: field=QQ, # long time
2406 ....: orthonormalize=False) # long time
2407 sage: J.one.clear_cache() # long time
2408 sage: J.one() # long time
2409 b0 + b9 + b26
2410 sage: J.one().to_matrix() # long time
2411 +----+----+----+
2412 | e0 | 0 | 0 |
2413 +----+----+----+
2414 | 0 | e0 | 0 |
2415 +----+----+----+
2416 | 0 | 0 | e0 |
2417 +----+----+----+
2418
2419 The 2-by-2 algebra is isomorphic to the ten-dimensional Jordan
2420 spin algebra, but just to be sure, we recompute its rank::
2421
2422 sage: J = OctonionHermitianEJA(2, # long time
2423 ....: field=QQ, # long time
2424 ....: orthonormalize=False) # long time
2425 sage: J.rank.clear_cache() # long time
2426 sage: J.rank() # long time
2427 2
2428
2429 """
2430 @staticmethod
2431 def _max_random_instance_size(max_dimension):
2432 r"""
2433 The maximum rank of a random QuaternionHermitianEJA.
2434 """
2435 # There's certainly a formula for this, but with only four
2436 # cases to worry about, I'm not that motivated to derive it.
2437 if max_dimension >= 27:
2438 return 3
2439 elif max_dimension >= 10:
2440 return 2
2441 elif max_dimension >= 1:
2442 return 1
2443 else:
2444 return 0
2445
2446 @classmethod
2447 def random_instance(cls, max_dimension=None, *args, **kwargs):
2448 """
2449 Return a random instance of this type of algebra.
2450 """
2451 class_max_d = cls._max_random_instance_dimension()
2452 if (max_dimension is None or max_dimension > class_max_d):
2453 max_dimension = class_max_d
2454 max_size = cls._max_random_instance_size(max_dimension)
2455 n = ZZ.random_element(max_size + 1)
2456 return cls(n, **kwargs)
2457
2458 def __init__(self, n, field=AA, **kwargs):
2459 if n > 3:
2460 # Otherwise we don't get an EJA.
2461 raise ValueError("n cannot exceed 3")
2462
2463 # We know this is a valid EJA, but will double-check
2464 # if the user passes check_axioms=True.
2465 if "check_axioms" not in kwargs: kwargs["check_axioms"] = False
2466
2467 from mjo.hurwitz import OctonionMatrixAlgebra
2468 A = OctonionMatrixAlgebra(n, scalars=field)
2469 super().__init__(A, **kwargs)
2470
2471 from mjo.eja.eja_cache import octonion_hermitian_eja_coeffs
2472 a = octonion_hermitian_eja_coeffs(self)
2473 if a is not None:
2474 self.rational_algebra()._charpoly_coefficients.set_cache(a)
2475
2476
2477 class AlbertEJA(OctonionHermitianEJA):
2478 r"""
2479 The Albert algebra is the algebra of three-by-three Hermitian
2480 matrices whose entries are octonions.
2481
2482 SETUP::
2483
2484 sage: from mjo.eja.eja_algebra import AlbertEJA
2485
2486 EXAMPLES::
2487
2488 sage: AlbertEJA(field=QQ, orthonormalize=False)
2489 Euclidean Jordan algebra of dimension 27 over Rational Field
2490 sage: AlbertEJA() # long time
2491 Euclidean Jordan algebra of dimension 27 over Algebraic Real Field
2492
2493 """
2494 def __init__(self, *args, **kwargs):
2495 super().__init__(3, *args, **kwargs)
2496
2497
2498 class HadamardEJA(RationalBasisEJA, ConcreteEJA):
2499 """
2500 Return the Euclidean Jordan algebra on `R^n` with the Hadamard
2501 (pointwise real-number multiplication) Jordan product and the
2502 usual inner-product.
2503
2504 This is nothing more than the Cartesian product of ``n`` copies of
2505 the one-dimensional Jordan spin algebra, and is the most common
2506 example of a non-simple Euclidean Jordan algebra.
2507
2508 SETUP::
2509
2510 sage: from mjo.eja.eja_algebra import HadamardEJA
2511
2512 EXAMPLES:
2513
2514 This multiplication table can be verified by hand::
2515
2516 sage: J = HadamardEJA(3)
2517 sage: b0,b1,b2 = J.gens()
2518 sage: b0*b0
2519 b0
2520 sage: b0*b1
2521 0
2522 sage: b0*b2
2523 0
2524 sage: b1*b1
2525 b1
2526 sage: b1*b2
2527 0
2528 sage: b2*b2
2529 b2
2530
2531 TESTS:
2532
2533 We can change the generator prefix::
2534
2535 sage: HadamardEJA(3, prefix='r').gens()
2536 (r0, r1, r2)
2537 """
2538 def __init__(self, n, field=AA, **kwargs):
2539 MS = MatrixSpace(field, n, 1)
2540
2541 if n == 0:
2542 jordan_product = lambda x,y: x
2543 inner_product = lambda x,y: x
2544 else:
2545 def jordan_product(x,y):
2546 return MS( xi*yi for (xi,yi) in zip(x,y) )
2547
2548 def inner_product(x,y):
2549 return (x.T*y)[0,0]
2550
2551 # New defaults for keyword arguments. Don't orthonormalize
2552 # because our basis is already orthonormal with respect to our
2553 # inner-product. Don't check the axioms, because we know this
2554 # is a valid EJA... but do double-check if the user passes
2555 # check_axioms=True. Note: we DON'T override the "check_field"
2556 # default here, because the user can pass in a field!
2557 if "orthonormalize" not in kwargs: kwargs["orthonormalize"] = False
2558 if "check_axioms" not in kwargs: kwargs["check_axioms"] = False
2559
2560 column_basis = tuple( MS(b) for b in FreeModule(field, n).basis() )
2561 super().__init__(column_basis,
2562 jordan_product,
2563 inner_product,
2564 field=field,
2565 matrix_space=MS,
2566 associative=True,
2567 **kwargs)
2568 self.rank.set_cache(n)
2569
2570 self.one.set_cache( self.sum(self.gens()) )
2571
2572 @staticmethod
2573 def _max_random_instance_dimension():
2574 r"""
2575 There's no reason to go higher than five here. That's
2576 enough to get the point across.
2577 """
2578 return 5
2579
2580 @staticmethod
2581 def _max_random_instance_size(max_dimension):
2582 r"""
2583 The maximum size (=dimension) of a random HadamardEJA.
2584 """
2585 return max_dimension
2586
2587 @classmethod
2588 def random_instance(cls, max_dimension=None, *args, **kwargs):
2589 """
2590 Return a random instance of this type of algebra.
2591 """
2592 class_max_d = cls._max_random_instance_dimension()
2593 if (max_dimension is None or max_dimension > class_max_d):
2594 max_dimension = class_max_d
2595 max_size = cls._max_random_instance_size(max_dimension)
2596 n = ZZ.random_element(max_size + 1)
2597 return cls(n, **kwargs)
2598
2599
2600 class BilinearFormEJA(RationalBasisEJA, ConcreteEJA):
2601 r"""
2602 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
2603 with the half-trace inner product and jordan product ``x*y =
2604 (<Bx,y>,y_bar>, x0*y_bar + y0*x_bar)`` where `B = 1 \times B22` is
2605 a symmetric positive-definite "bilinear form" matrix. Its
2606 dimension is the size of `B`, and it has rank two in dimensions
2607 larger than two. It reduces to the ``JordanSpinEJA`` when `B` is
2608 the identity matrix of order ``n``.
2609
2610 We insist that the one-by-one upper-left identity block of `B` be
2611 passed in as well so that we can be passed a matrix of size zero
2612 to construct a trivial algebra.
2613
2614 SETUP::
2615
2616 sage: from mjo.eja.eja_algebra import (BilinearFormEJA,
2617 ....: JordanSpinEJA)
2618
2619 EXAMPLES:
2620
2621 When no bilinear form is specified, the identity matrix is used,
2622 and the resulting algebra is the Jordan spin algebra::
2623
2624 sage: B = matrix.identity(AA,3)
2625 sage: J0 = BilinearFormEJA(B)
2626 sage: J1 = JordanSpinEJA(3)
2627 sage: J0.multiplication_table() == J0.multiplication_table()
2628 True
2629
2630 An error is raised if the matrix `B` does not correspond to a
2631 positive-definite bilinear form::
2632
2633 sage: B = matrix.random(QQ,2,3)
2634 sage: J = BilinearFormEJA(B)
2635 Traceback (most recent call last):
2636 ...
2637 ValueError: bilinear form is not positive-definite
2638 sage: B = matrix.zero(QQ,3)
2639 sage: J = BilinearFormEJA(B)
2640 Traceback (most recent call last):
2641 ...
2642 ValueError: bilinear form is not positive-definite
2643
2644 TESTS:
2645
2646 We can create a zero-dimensional algebra::
2647
2648 sage: B = matrix.identity(AA,0)
2649 sage: J = BilinearFormEJA(B)
2650 sage: J.basis()
2651 Finite family {}
2652
2653 We can check the multiplication condition given in the Jordan, von
2654 Neumann, and Wigner paper (and also discussed on my "On the
2655 symmetry..." paper). Note that this relies heavily on the standard
2656 choice of basis, as does anything utilizing the bilinear form
2657 matrix. We opt not to orthonormalize the basis, because if we
2658 did, we would have to normalize the `s_{i}` in a similar manner::
2659
2660 sage: set_random_seed()
2661 sage: n = ZZ.random_element(5)
2662 sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular')
2663 sage: B11 = matrix.identity(QQ,1)
2664 sage: B22 = M.transpose()*M
2665 sage: B = block_matrix(2,2,[ [B11,0 ],
2666 ....: [0, B22 ] ])
2667 sage: J = BilinearFormEJA(B, orthonormalize=False)
2668 sage: eis = VectorSpace(M.base_ring(), M.ncols()).basis()
2669 sage: V = J.vector_space()
2670 sage: sis = [ J( V([0] + (M.inverse()*ei).list()).column() )
2671 ....: for ei in eis ]
2672 sage: actual = [ sis[i]*sis[j]
2673 ....: for i in range(n-1)
2674 ....: for j in range(n-1) ]
2675 sage: expected = [ J.one() if i == j else J.zero()
2676 ....: for i in range(n-1)
2677 ....: for j in range(n-1) ]
2678 sage: actual == expected
2679 True
2680
2681 """
2682 def __init__(self, B, field=AA, **kwargs):
2683 # The matrix "B" is supplied by the user in most cases,
2684 # so it makes sense to check whether or not its positive-
2685 # definite unless we are specifically asked not to...
2686 if ("check_axioms" not in kwargs) or kwargs["check_axioms"]:
2687 if not B.is_positive_definite():
2688 raise ValueError("bilinear form is not positive-definite")
2689
2690 # However, all of the other data for this EJA is computed
2691 # by us in manner that guarantees the axioms are
2692 # satisfied. So, again, unless we are specifically asked to
2693 # verify things, we'll skip the rest of the checks.
2694 if "check_axioms" not in kwargs: kwargs["check_axioms"] = False
2695
2696 n = B.nrows()
2697 MS = MatrixSpace(field, n, 1)
2698
2699 def inner_product(x,y):
2700 return (y.T*B*x)[0,0]
2701
2702 def jordan_product(x,y):
2703 x0 = x[0,0]
2704 xbar = x[1:,0]
2705 y0 = y[0,0]
2706 ybar = y[1:,0]
2707 z0 = inner_product(y,x)
2708 zbar = y0*xbar + x0*ybar
2709 return MS([z0] + zbar.list())
2710
2711 column_basis = tuple( MS(b) for b in FreeModule(field, n).basis() )
2712
2713 # TODO: I haven't actually checked this, but it seems legit.
2714 associative = False
2715 if n <= 2:
2716 associative = True
2717
2718 super().__init__(column_basis,
2719 jordan_product,
2720 inner_product,
2721 field=field,
2722 matrix_space=MS,
2723 associative=associative,
2724 **kwargs)
2725
2726 # The rank of this algebra is two, unless we're in a
2727 # one-dimensional ambient space (because the rank is bounded
2728 # by the ambient dimension).
2729 self.rank.set_cache(min(n,2))
2730 if n == 0:
2731 self.one.set_cache( self.zero() )
2732 else:
2733 self.one.set_cache( self.monomial(0) )
2734
2735 @staticmethod
2736 def _max_random_instance_dimension():
2737 r"""
2738 There's no reason to go higher than five here. That's
2739 enough to get the point across.
2740 """
2741 return 5
2742
2743 @staticmethod
2744 def _max_random_instance_size(max_dimension):
2745 r"""
2746 The maximum size (=dimension) of a random BilinearFormEJA.
2747 """
2748 return max_dimension
2749
2750 @classmethod
2751 def random_instance(cls, max_dimension=None, *args, **kwargs):
2752 """
2753 Return a random instance of this algebra.
2754 """
2755 class_max_d = cls._max_random_instance_dimension()
2756 if (max_dimension is None or max_dimension > class_max_d):
2757 max_dimension = class_max_d
2758 max_size = cls._max_random_instance_size(max_dimension)
2759 n = ZZ.random_element(max_size + 1)
2760
2761 if n.is_zero():
2762 B = matrix.identity(ZZ, n)
2763 return cls(B, **kwargs)
2764
2765 B11 = matrix.identity(ZZ, 1)
2766 M = matrix.random(ZZ, n-1)
2767 I = matrix.identity(ZZ, n-1)
2768 alpha = ZZ.zero()
2769 while alpha.is_zero():
2770 alpha = ZZ.random_element().abs()
2771
2772 B22 = M.transpose()*M + alpha*I
2773
2774 from sage.matrix.special import block_matrix
2775 B = block_matrix(2,2, [ [B11, ZZ(0) ],
2776 [ZZ(0), B22 ] ])
2777
2778 return cls(B, **kwargs)
2779
2780
2781 class JordanSpinEJA(BilinearFormEJA):
2782 """
2783 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
2784 with the usual inner product and jordan product ``x*y =
2785 (<x,y>, x0*y_bar + y0*x_bar)``. It has dimension `n` over
2786 the reals.
2787
2788 SETUP::
2789
2790 sage: from mjo.eja.eja_algebra import JordanSpinEJA
2791
2792 EXAMPLES:
2793
2794 This multiplication table can be verified by hand::
2795
2796 sage: J = JordanSpinEJA(4)
2797 sage: b0,b1,b2,b3 = J.gens()
2798 sage: b0*b0
2799 b0
2800 sage: b0*b1
2801 b1
2802 sage: b0*b2
2803 b2
2804 sage: b0*b3
2805 b3
2806 sage: b1*b2
2807 0
2808 sage: b1*b3
2809 0
2810 sage: b2*b3
2811 0
2812
2813 We can change the generator prefix::
2814
2815 sage: JordanSpinEJA(2, prefix='B').gens()
2816 (B0, B1)
2817
2818 TESTS:
2819
2820 Ensure that we have the usual inner product on `R^n`::
2821
2822 sage: set_random_seed()
2823 sage: J = JordanSpinEJA.random_instance()
2824 sage: x,y = J.random_elements(2)
2825 sage: actual = x.inner_product(y)
2826 sage: expected = x.to_vector().inner_product(y.to_vector())
2827 sage: actual == expected
2828 True
2829
2830 """
2831 def __init__(self, n, *args, **kwargs):
2832 # This is a special case of the BilinearFormEJA with the
2833 # identity matrix as its bilinear form.
2834 B = matrix.identity(ZZ, n)
2835
2836 # Don't orthonormalize because our basis is already
2837 # orthonormal with respect to our inner-product.
2838 if "orthonormalize" not in kwargs: kwargs["orthonormalize"] = False
2839
2840 # But also don't pass check_field=False here, because the user
2841 # can pass in a field!
2842 super().__init__(B, *args, **kwargs)
2843
2844 @classmethod
2845 def random_instance(cls, max_dimension=None, *args, **kwargs):
2846 """
2847 Return a random instance of this type of algebra.
2848
2849 Needed here to override the implementation for ``BilinearFormEJA``.
2850 """
2851 class_max_d = cls._max_random_instance_dimension()
2852 if (max_dimension is None or max_dimension > class_max_d):
2853 max_dimension = class_max_d
2854 max_size = cls._max_random_instance_size(max_dimension)
2855 n = ZZ.random_element(max_size + 1)
2856 return cls(n, **kwargs)
2857
2858
2859 class TrivialEJA(RationalBasisEJA, ConcreteEJA):
2860 """
2861 The trivial Euclidean Jordan algebra consisting of only a zero element.
2862
2863 SETUP::
2864
2865 sage: from mjo.eja.eja_algebra import TrivialEJA
2866
2867 EXAMPLES::
2868
2869 sage: J = TrivialEJA()
2870 sage: J.dimension()
2871 0
2872 sage: J.zero()
2873 0
2874 sage: J.one()
2875 0
2876 sage: 7*J.one()*12*J.one()
2877 0
2878 sage: J.one().inner_product(J.one())
2879 0
2880 sage: J.one().norm()
2881 0
2882 sage: J.one().subalgebra_generated_by()
2883 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2884 sage: J.rank()
2885 0
2886
2887 """
2888 def __init__(self, field=AA, **kwargs):
2889 jordan_product = lambda x,y: x
2890 inner_product = lambda x,y: field.zero()
2891 basis = ()
2892 MS = MatrixSpace(field,0)
2893
2894 # New defaults for keyword arguments
2895 if "orthonormalize" not in kwargs: kwargs["orthonormalize"] = False
2896 if "check_axioms" not in kwargs: kwargs["check_axioms"] = False
2897
2898 super().__init__(basis,
2899 jordan_product,
2900 inner_product,
2901 associative=True,
2902 field=field,
2903 matrix_space=MS,
2904 **kwargs)
2905
2906 # The rank is zero using my definition, namely the dimension of the
2907 # largest subalgebra generated by any element.
2908 self.rank.set_cache(0)
2909 self.one.set_cache( self.zero() )
2910
2911 @classmethod
2912 def random_instance(cls, max_dimension=None, *args, **kwargs):
2913 # We don't take a "size" argument so the superclass method is
2914 # inappropriate for us. The ``max_dimension`` argument is
2915 # included so that if this method is called generically with a
2916 # ``max_dimension=<whatever>`` argument, we don't try to pass
2917 # it on to the algebra constructor.
2918 return cls(**kwargs)
2919
2920
2921 class CartesianProductEJA(FiniteDimensionalEJA):
2922 r"""
2923 The external (orthogonal) direct sum of two or more Euclidean
2924 Jordan algebras. Every Euclidean Jordan algebra decomposes into an
2925 orthogonal direct sum of simple Euclidean Jordan algebras which is
2926 then isometric to a Cartesian product, so no generality is lost by
2927 providing only this construction.
2928
2929 SETUP::
2930
2931 sage: from mjo.eja.eja_algebra import (random_eja,
2932 ....: CartesianProductEJA,
2933 ....: ComplexHermitianEJA,
2934 ....: HadamardEJA,
2935 ....: JordanSpinEJA,
2936 ....: RealSymmetricEJA)
2937
2938 EXAMPLES:
2939
2940 The Jordan product is inherited from our factors and implemented by
2941 our CombinatorialFreeModule Cartesian product superclass::
2942
2943 sage: set_random_seed()
2944 sage: J1 = HadamardEJA(2)
2945 sage: J2 = RealSymmetricEJA(2)
2946 sage: J = cartesian_product([J1,J2])
2947 sage: x,y = J.random_elements(2)
2948 sage: x*y in J
2949 True
2950
2951 The ability to retrieve the original factors is implemented by our
2952 CombinatorialFreeModule Cartesian product superclass::
2953
2954 sage: J1 = HadamardEJA(2, field=QQ)
2955 sage: J2 = JordanSpinEJA(3, field=QQ)
2956 sage: J = cartesian_product([J1,J2])
2957 sage: J.cartesian_factors()
2958 (Euclidean Jordan algebra of dimension 2 over Rational Field,
2959 Euclidean Jordan algebra of dimension 3 over Rational Field)
2960
2961 You can provide more than two factors::
2962
2963 sage: J1 = HadamardEJA(2)
2964 sage: J2 = JordanSpinEJA(3)
2965 sage: J3 = RealSymmetricEJA(3)
2966 sage: cartesian_product([J1,J2,J3])
2967 Euclidean Jordan algebra of dimension 2 over Algebraic Real
2968 Field (+) Euclidean Jordan algebra of dimension 3 over Algebraic
2969 Real Field (+) Euclidean Jordan algebra of dimension 6 over
2970 Algebraic Real Field
2971
2972 Rank is additive on a Cartesian product::
2973
2974 sage: J1 = HadamardEJA(1)
2975 sage: J2 = RealSymmetricEJA(2)
2976 sage: J = cartesian_product([J1,J2])
2977 sage: J1.rank.clear_cache()
2978 sage: J2.rank.clear_cache()
2979 sage: J.rank.clear_cache()
2980 sage: J.rank()
2981 3
2982 sage: J.rank() == J1.rank() + J2.rank()
2983 True
2984
2985 The same rank computation works over the rationals, with whatever
2986 basis you like::
2987
2988 sage: J1 = HadamardEJA(1, field=QQ, orthonormalize=False)
2989 sage: J2 = RealSymmetricEJA(2, field=QQ, orthonormalize=False)
2990 sage: J = cartesian_product([J1,J2])
2991 sage: J1.rank.clear_cache()
2992 sage: J2.rank.clear_cache()
2993 sage: J.rank.clear_cache()
2994 sage: J.rank()
2995 3
2996 sage: J.rank() == J1.rank() + J2.rank()
2997 True
2998
2999 The product algebra will be associative if and only if all of its
3000 components are associative::
3001
3002 sage: J1 = HadamardEJA(2)
3003 sage: J1.is_associative()
3004 True
3005 sage: J2 = HadamardEJA(3)
3006 sage: J2.is_associative()
3007 True
3008 sage: J3 = RealSymmetricEJA(3)
3009 sage: J3.is_associative()
3010 False
3011 sage: CP1 = cartesian_product([J1,J2])
3012 sage: CP1.is_associative()
3013 True
3014 sage: CP2 = cartesian_product([J1,J3])
3015 sage: CP2.is_associative()
3016 False
3017
3018 Cartesian products of Cartesian products work::
3019
3020 sage: J1 = JordanSpinEJA(1)
3021 sage: J2 = JordanSpinEJA(1)
3022 sage: J3 = JordanSpinEJA(1)
3023 sage: J = cartesian_product([J1,cartesian_product([J2,J3])])
3024 sage: J.multiplication_table()
3025 +----++----+----+----+
3026 | * || b0 | b1 | b2 |
3027 +====++====+====+====+
3028 | b0 || b0 | 0 | 0 |
3029 +----++----+----+----+
3030 | b1 || 0 | b1 | 0 |
3031 +----++----+----+----+
3032 | b2 || 0 | 0 | b2 |
3033 +----++----+----+----+
3034 sage: HadamardEJA(3).multiplication_table()
3035 +----++----+----+----+
3036 | * || b0 | b1 | b2 |
3037 +====++====+====+====+
3038 | b0 || b0 | 0 | 0 |
3039 +----++----+----+----+
3040 | b1 || 0 | b1 | 0 |
3041 +----++----+----+----+
3042 | b2 || 0 | 0 | b2 |
3043 +----++----+----+----+
3044
3045 The "matrix space" of a Cartesian product always consists of
3046 ordered pairs (or triples, or...) whose components are the
3047 matrix spaces of its factors::
3048
3049 sage: J1 = HadamardEJA(2)
3050 sage: J2 = ComplexHermitianEJA(2)
3051 sage: J = cartesian_product([J1,J2])
3052 sage: J.matrix_space()
3053 The Cartesian product of (Full MatrixSpace of 2 by 1 dense
3054 matrices over Algebraic Real Field, Module of 2 by 2 matrices
3055 with entries in Algebraic Field over the scalar ring Algebraic
3056 Real Field)
3057 sage: J.one().to_matrix()[0]
3058 [1]
3059 [1]
3060 sage: J.one().to_matrix()[1]
3061 +---+---+
3062 | 1 | 0 |
3063 +---+---+
3064 | 0 | 1 |
3065 +---+---+
3066
3067 TESTS:
3068
3069 All factors must share the same base field::
3070
3071 sage: J1 = HadamardEJA(2, field=QQ)
3072 sage: J2 = RealSymmetricEJA(2)
3073 sage: CartesianProductEJA((J1,J2))
3074 Traceback (most recent call last):
3075 ...
3076 ValueError: all factors must share the same base field
3077
3078 The cached unit element is the same one that would be computed::
3079
3080 sage: set_random_seed() # long time
3081 sage: J1 = random_eja() # long time
3082 sage: J2 = random_eja() # long time
3083 sage: J = cartesian_product([J1,J2]) # long time
3084 sage: actual = J.one() # long time
3085 sage: J.one.clear_cache() # long time
3086 sage: expected = J.one() # long time
3087 sage: actual == expected # long time
3088 True
3089 """
3090 def __init__(self, factors, **kwargs):
3091 m = len(factors)
3092 if m == 0:
3093 return TrivialEJA()
3094
3095 self._sets = factors
3096
3097 field = factors[0].base_ring()
3098 if not all( J.base_ring() == field for J in factors ):
3099 raise ValueError("all factors must share the same base field")
3100
3101 # Figure out the category to use.
3102 associative = all( f.is_associative() for f in factors )
3103 category = EuclideanJordanAlgebras(field)
3104 if associative: category = category.Associative()
3105 category = category.join([category, category.CartesianProducts()])
3106
3107 # Compute my matrix space. We don't simply use the
3108 # ``cartesian_product()`` functor here because it acts
3109 # differently on SageMath MatrixSpaces and our custom
3110 # MatrixAlgebras, which are CombinatorialFreeModules. We
3111 # always want the result to be represented (and indexed) as an
3112 # ordered tuple. This category isn't perfect, but is good
3113 # enough for what we need to do.
3114 MS_cat = MagmaticAlgebras(field).FiniteDimensional().WithBasis()
3115 MS_cat = MS_cat.Unital().CartesianProducts()
3116 MS_factors = tuple( J.matrix_space() for J in factors )
3117 from sage.sets.cartesian_product import CartesianProduct
3118 self._matrix_space = CartesianProduct(MS_factors, MS_cat)
3119
3120 self._matrix_basis = []
3121 zero = self._matrix_space.zero()
3122 for i in range(m):
3123 for b in factors[i].matrix_basis():
3124 z = list(zero)
3125 z[i] = b
3126 self._matrix_basis.append(z)
3127
3128 self._matrix_basis = tuple( self._matrix_space(b)
3129 for b in self._matrix_basis )
3130 n = len(self._matrix_basis)
3131
3132 # We already have what we need for the super-superclass constructor.
3133 CombinatorialFreeModule.__init__(self,
3134 field,
3135 range(n),
3136 prefix="b",
3137 category=category,
3138 bracket=False)
3139
3140 # Now create the vector space for the algebra, which will have
3141 # its own set of non-ambient coordinates (in terms of the
3142 # supplied basis).
3143 degree = sum( f._matrix_span.ambient_vector_space().degree()
3144 for f in factors )
3145 V = VectorSpace(field, degree)
3146 vector_basis = tuple( V(_all2list(b)) for b in self._matrix_basis )
3147
3148 # Save the span of our matrix basis (when written out as long
3149 # vectors) because otherwise we'll have to reconstruct it
3150 # every time we want to coerce a matrix into the algebra.
3151 self._matrix_span = V.span_of_basis( vector_basis, check=False)
3152
3153 # Since we don't (re)orthonormalize the basis, the FDEJA
3154 # constructor is going to set self._deortho_matrix to the
3155 # identity matrix. Here we set it to the correct value using
3156 # the deortho matrices from our factors.
3157 self._deortho_matrix = matrix.block_diagonal(
3158 [J._deortho_matrix for J in factors]
3159 )
3160
3161 self._inner_product_matrix = matrix.block_diagonal(
3162 [J._inner_product_matrix for J in factors]
3163 )
3164 self._inner_product_matrix._cache = {'hermitian': True}
3165 self._inner_product_matrix.set_immutable()
3166
3167 # Building the multiplication table is a bit more tricky
3168 # because we have to embed the entries of the factors'
3169 # multiplication tables into the product EJA.
3170 zed = self.zero()
3171 self._multiplication_table = [ [zed for j in range(i+1)]
3172 for i in range(n) ]
3173
3174 # Keep track of an offset that tallies the dimensions of all
3175 # previous factors. If the second factor is dim=2 and if the
3176 # first one is dim=3, then we want to skip the first 3x3 block
3177 # when copying the multiplication table for the second factor.
3178 offset = 0
3179 for f in range(m):
3180 phi_f = self.cartesian_embedding(f)
3181 factor_dim = factors[f].dimension()
3182 for i in range(factor_dim):
3183 for j in range(i+1):
3184 f_ij = factors[f]._multiplication_table[i][j]
3185 e = phi_f(f_ij)
3186 self._multiplication_table[offset+i][offset+j] = e
3187 offset += factor_dim
3188
3189 self.rank.set_cache(sum(J.rank() for J in factors))
3190 ones = tuple(J.one().to_matrix() for J in factors)
3191 self.one.set_cache(self(ones))
3192
3193 def cartesian_factors(self):
3194 # Copy/pasted from CombinatorialFreeModule_CartesianProduct.
3195 return self._sets
3196
3197 def cartesian_factor(self, i):
3198 r"""
3199 Return the ``i``th factor of this algebra.
3200 """
3201 return self._sets[i]
3202
3203 def _repr_(self):
3204 # Copy/pasted from CombinatorialFreeModule_CartesianProduct.
3205 from sage.categories.cartesian_product import cartesian_product
3206 return cartesian_product.symbol.join("%s" % factor
3207 for factor in self._sets)
3208
3209
3210 @cached_method
3211 def cartesian_projection(self, i):
3212 r"""
3213 SETUP::
3214
3215 sage: from mjo.eja.eja_algebra import (random_eja,
3216 ....: JordanSpinEJA,
3217 ....: HadamardEJA,
3218 ....: RealSymmetricEJA,
3219 ....: ComplexHermitianEJA)
3220
3221 EXAMPLES:
3222
3223 The projection morphisms are Euclidean Jordan algebra
3224 operators::
3225
3226 sage: J1 = HadamardEJA(2)
3227 sage: J2 = RealSymmetricEJA(2)
3228 sage: J = cartesian_product([J1,J2])
3229 sage: J.cartesian_projection(0)
3230 Linear operator between finite-dimensional Euclidean Jordan
3231 algebras represented by the matrix:
3232 [1 0 0 0 0]
3233 [0 1 0 0 0]
3234 Domain: Euclidean Jordan algebra of dimension 2 over Algebraic
3235 Real Field (+) Euclidean Jordan algebra of dimension 3 over
3236 Algebraic Real Field
3237 Codomain: Euclidean Jordan algebra of dimension 2 over Algebraic
3238 Real Field
3239 sage: J.cartesian_projection(1)
3240 Linear operator between finite-dimensional Euclidean Jordan
3241 algebras represented by the matrix:
3242 [0 0 1 0 0]
3243 [0 0 0 1 0]
3244 [0 0 0 0 1]
3245 Domain: Euclidean Jordan algebra of dimension 2 over Algebraic
3246 Real Field (+) Euclidean Jordan algebra of dimension 3 over
3247 Algebraic Real Field
3248 Codomain: Euclidean Jordan algebra of dimension 3 over Algebraic
3249 Real Field
3250
3251 The projections work the way you'd expect on the vector
3252 representation of an element::
3253
3254 sage: J1 = JordanSpinEJA(2)
3255 sage: J2 = ComplexHermitianEJA(2)
3256 sage: J = cartesian_product([J1,J2])
3257 sage: pi_left = J.cartesian_projection(0)
3258 sage: pi_right = J.cartesian_projection(1)
3259 sage: pi_left(J.one()).to_vector()
3260 (1, 0)
3261 sage: pi_right(J.one()).to_vector()
3262 (1, 0, 0, 1)
3263 sage: J.one().to_vector()
3264 (1, 0, 1, 0, 0, 1)
3265
3266 TESTS:
3267
3268 The answer never changes::
3269
3270 sage: set_random_seed()
3271 sage: J1 = random_eja()
3272 sage: J2 = random_eja()
3273 sage: J = cartesian_product([J1,J2])
3274 sage: P0 = J.cartesian_projection(0)
3275 sage: P1 = J.cartesian_projection(0)
3276 sage: P0 == P1
3277 True
3278
3279 """
3280 offset = sum( self.cartesian_factor(k).dimension()
3281 for k in range(i) )
3282 Ji = self.cartesian_factor(i)
3283 Pi = self._module_morphism(lambda j: Ji.monomial(j - offset),
3284 codomain=Ji)
3285
3286 return FiniteDimensionalEJAOperator(self,Ji,Pi.matrix())
3287
3288 @cached_method
3289 def cartesian_embedding(self, i):
3290 r"""
3291 SETUP::
3292
3293 sage: from mjo.eja.eja_algebra import (random_eja,
3294 ....: JordanSpinEJA,
3295 ....: HadamardEJA,
3296 ....: RealSymmetricEJA)
3297
3298 EXAMPLES:
3299
3300 The embedding morphisms are Euclidean Jordan algebra
3301 operators::
3302
3303 sage: J1 = HadamardEJA(2)
3304 sage: J2 = RealSymmetricEJA(2)
3305 sage: J = cartesian_product([J1,J2])
3306 sage: J.cartesian_embedding(0)
3307 Linear operator between finite-dimensional Euclidean Jordan
3308 algebras represented by the matrix:
3309 [1 0]
3310 [0 1]
3311 [0 0]
3312 [0 0]
3313 [0 0]
3314 Domain: Euclidean Jordan algebra of dimension 2 over
3315 Algebraic Real Field
3316 Codomain: Euclidean Jordan algebra of dimension 2 over
3317 Algebraic Real Field (+) Euclidean Jordan algebra of
3318 dimension 3 over Algebraic Real Field
3319 sage: J.cartesian_embedding(1)
3320 Linear operator between finite-dimensional Euclidean Jordan
3321 algebras represented by the matrix:
3322 [0 0 0]
3323 [0 0 0]
3324 [1 0 0]
3325 [0 1 0]
3326 [0 0 1]
3327 Domain: Euclidean Jordan algebra of dimension 3 over
3328 Algebraic Real Field
3329 Codomain: Euclidean Jordan algebra of dimension 2 over
3330 Algebraic Real Field (+) Euclidean Jordan algebra of
3331 dimension 3 over Algebraic Real Field
3332
3333 The embeddings work the way you'd expect on the vector
3334 representation of an element::
3335
3336 sage: J1 = JordanSpinEJA(3)
3337 sage: J2 = RealSymmetricEJA(2)
3338 sage: J = cartesian_product([J1,J2])
3339 sage: iota_left = J.cartesian_embedding(0)
3340 sage: iota_right = J.cartesian_embedding(1)
3341 sage: iota_left(J1.zero()) == J.zero()
3342 True
3343 sage: iota_right(J2.zero()) == J.zero()
3344 True
3345 sage: J1.one().to_vector()
3346 (1, 0, 0)
3347 sage: iota_left(J1.one()).to_vector()
3348 (1, 0, 0, 0, 0, 0)
3349 sage: J2.one().to_vector()
3350 (1, 0, 1)
3351 sage: iota_right(J2.one()).to_vector()
3352 (0, 0, 0, 1, 0, 1)
3353 sage: J.one().to_vector()
3354 (1, 0, 0, 1, 0, 1)
3355
3356 TESTS:
3357
3358 The answer never changes::
3359
3360 sage: set_random_seed()
3361 sage: J1 = random_eja()
3362 sage: J2 = random_eja()
3363 sage: J = cartesian_product([J1,J2])
3364 sage: E0 = J.cartesian_embedding(0)
3365 sage: E1 = J.cartesian_embedding(0)
3366 sage: E0 == E1
3367 True
3368
3369 Composing a projection with the corresponding inclusion should
3370 produce the identity map, and mismatching them should produce
3371 the zero map::
3372
3373 sage: set_random_seed()
3374 sage: J1 = random_eja()
3375 sage: J2 = random_eja()
3376 sage: J = cartesian_product([J1,J2])
3377 sage: iota_left = J.cartesian_embedding(0)
3378 sage: iota_right = J.cartesian_embedding(1)
3379 sage: pi_left = J.cartesian_projection(0)
3380 sage: pi_right = J.cartesian_projection(1)
3381 sage: pi_left*iota_left == J1.one().operator()
3382 True
3383 sage: pi_right*iota_right == J2.one().operator()
3384 True
3385 sage: (pi_left*iota_right).is_zero()
3386 True
3387 sage: (pi_right*iota_left).is_zero()
3388 True
3389
3390 """
3391 offset = sum( self.cartesian_factor(k).dimension()
3392 for k in range(i) )
3393 Ji = self.cartesian_factor(i)
3394 Ei = Ji._module_morphism(lambda j: self.monomial(j + offset),
3395 codomain=self)
3396 return FiniteDimensionalEJAOperator(Ji,self,Ei.matrix())
3397
3398
3399
3400 FiniteDimensionalEJA.CartesianProduct = CartesianProductEJA
3401
3402 class RationalBasisCartesianProductEJA(CartesianProductEJA,
3403 RationalBasisEJA):
3404 r"""
3405 A separate class for products of algebras for which we know a
3406 rational basis.
3407
3408 SETUP::
3409
3410 sage: from mjo.eja.eja_algebra import (FiniteDimensionalEJA,
3411 ....: HadamardEJA,
3412 ....: JordanSpinEJA,
3413 ....: RealSymmetricEJA)
3414
3415 EXAMPLES:
3416
3417 This gives us fast characteristic polynomial computations in
3418 product algebras, too::
3419
3420
3421 sage: J1 = JordanSpinEJA(2)
3422 sage: J2 = RealSymmetricEJA(3)
3423 sage: J = cartesian_product([J1,J2])
3424 sage: J.characteristic_polynomial_of().degree()
3425 5
3426 sage: J.rank()
3427 5
3428
3429 TESTS:
3430
3431 The ``cartesian_product()`` function only uses the first factor to
3432 decide where the result will live; thus we have to be careful to
3433 check that all factors do indeed have a ``rational_algebra()`` method
3434 before we construct an algebra that claims to have a rational basis::
3435
3436 sage: J1 = HadamardEJA(2)
3437 sage: jp = lambda X,Y: X*Y
3438 sage: ip = lambda X,Y: X[0,0]*Y[0,0]
3439 sage: b1 = matrix(QQ, [[1]])
3440 sage: J2 = FiniteDimensionalEJA((b1,), jp, ip)
3441 sage: cartesian_product([J2,J1]) # factor one not RationalBasisEJA
3442 Euclidean Jordan algebra of dimension 1 over Algebraic Real
3443 Field (+) Euclidean Jordan algebra of dimension 2 over Algebraic
3444 Real Field
3445 sage: cartesian_product([J1,J2]) # factor one is RationalBasisEJA
3446 Traceback (most recent call last):
3447 ...
3448 ValueError: factor not a RationalBasisEJA
3449
3450 """
3451 def __init__(self, algebras, **kwargs):
3452 if not all( hasattr(r, "rational_algebra") for r in algebras ):
3453 raise ValueError("factor not a RationalBasisEJA")
3454
3455 CartesianProductEJA.__init__(self, algebras, **kwargs)
3456
3457 @cached_method
3458 def rational_algebra(self):
3459 if self.base_ring() is QQ:
3460 return self
3461
3462 return cartesian_product([
3463 r.rational_algebra() for r in self.cartesian_factors()
3464 ])
3465
3466
3467 RationalBasisEJA.CartesianProduct = RationalBasisCartesianProductEJA
3468
3469 def random_eja(max_dimension=None, *args, **kwargs):
3470 r"""
3471
3472 SETUP::
3473
3474 sage: from mjo.eja.eja_algebra import random_eja
3475
3476 TESTS::
3477
3478 sage: set_random_seed()
3479 sage: n = ZZ.random_element(1,5)
3480 sage: J = random_eja(max_dimension=n, field=QQ, orthonormalize=False)
3481 sage: J.dimension() <= n
3482 True
3483
3484 """
3485 # Use the ConcreteEJA default as the total upper bound (regardless
3486 # of any whether or not any individual factors set a lower limit).
3487 if max_dimension is None:
3488 max_dimension = ConcreteEJA._max_random_instance_dimension()
3489 J1 = ConcreteEJA.random_instance(max_dimension, *args, **kwargs)
3490
3491
3492 # Roll the dice to see if we attempt a Cartesian product.
3493 dice_roll = ZZ.random_element(len(ConcreteEJA.__subclasses__()) + 1)
3494 new_max_dimension = max_dimension - J1.dimension()
3495 if new_max_dimension == 0 or dice_roll != 0:
3496 # If it's already as big as we're willing to tolerate, just
3497 # return it and don't worry about Cartesian products.
3498 return J1
3499 else:
3500 # Use random_eja() again so we can get more than two factors
3501 # if the sub-call also Decides on a cartesian product.
3502 J2 = random_eja(new_max_dimension, *args, **kwargs)
3503 return cartesian_product([J1,J2])