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