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