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