]> gitweb.michael.orlitzky.com - sage.d.git/blob - mjo/eja/eja_algebra.py
eja: enable consistency checks by default in (non-element) subalgebras.
[sage.d.git] / mjo / eja / eja_algebra.py
1 """
2 Euclidean Jordan Algebras. These are formally-real Jordan Algebras;
3 specifically those where u^2 + v^2 = 0 implies that u = v = 0. They
4 are used in optimization, and have some additional nice methods beyond
5 what can be supported in a general Jordan Algebra.
6 """
7
8 from itertools import repeat
9
10 from sage.algebras.quatalg.quaternion_algebra import QuaternionAlgebra
11 from sage.categories.magmatic_algebras import MagmaticAlgebras
12 from sage.combinat.free_module import CombinatorialFreeModule
13 from sage.matrix.constructor import matrix
14 from sage.matrix.matrix_space import MatrixSpace
15 from sage.misc.cachefunc import cached_method
16 from sage.misc.lazy_import import lazy_import
17 from sage.misc.prandom import choice
18 from sage.misc.table import table
19 from sage.modules.free_module import FreeModule, VectorSpace
20 from sage.rings.all import (ZZ, QQ, AA, QQbar, RR, RLF, CLF,
21 PolynomialRing,
22 QuadraticField)
23 from mjo.eja.eja_element import FiniteDimensionalEuclideanJordanAlgebraElement
24 lazy_import('mjo.eja.eja_subalgebra',
25 'FiniteDimensionalEuclideanJordanSubalgebra')
26 from mjo.eja.eja_utils import _mat2vec
27
28 class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
29
30 def _coerce_map_from_base_ring(self):
31 """
32 Disable the map from the base ring into the algebra.
33
34 Performing a nonsense conversion like this automatically
35 is counterpedagogical. The fallback is to try the usual
36 element constructor, which should also fail.
37
38 SETUP::
39
40 sage: from mjo.eja.eja_algebra import random_eja
41
42 TESTS::
43
44 sage: set_random_seed()
45 sage: J = random_eja()
46 sage: J(1)
47 Traceback (most recent call last):
48 ...
49 ValueError: not a naturally-represented algebra element
50
51 """
52 return None
53
54 def __init__(self,
55 field,
56 mult_table,
57 prefix='e',
58 category=None,
59 natural_basis=None,
60 check=True):
61 """
62 SETUP::
63
64 sage: from mjo.eja.eja_algebra import (
65 ....: FiniteDimensionalEuclideanJordanAlgebra,
66 ....: JordanSpinEJA,
67 ....: random_eja)
68
69 EXAMPLES:
70
71 By definition, Jordan multiplication commutes::
72
73 sage: set_random_seed()
74 sage: J = random_eja()
75 sage: x,y = J.random_elements(2)
76 sage: x*y == y*x
77 True
78
79 TESTS:
80
81 The ``field`` we're given must be real with ``check=True``::
82
83 sage: JordanSpinEJA(2,QQbar)
84 Traceback (most recent call last):
85 ...
86 ValueError: field is not real
87
88 The multiplication table must be square with ``check=True``::
89
90 sage: FiniteDimensionalEuclideanJordanAlgebra(QQ,((),()))
91 Traceback (most recent call last):
92 ...
93 ValueError: multiplication table is not square
94
95 """
96 if check:
97 if not field.is_subring(RR):
98 # Note: this does return true for the real algebraic
99 # field, and any quadratic field where we've specified
100 # a real embedding.
101 raise ValueError('field is not real')
102
103 self._natural_basis = natural_basis
104
105 if category is None:
106 category = MagmaticAlgebras(field).FiniteDimensional()
107 category = category.WithBasis().Unital()
108
109 # The multiplication table had better be square
110 n = len(mult_table)
111 if check:
112 if not all( len(l) == n for l in mult_table ):
113 raise ValueError("multiplication table is not square")
114
115 fda = super(FiniteDimensionalEuclideanJordanAlgebra, self)
116 fda.__init__(field,
117 range(n),
118 prefix=prefix,
119 category=category)
120 self.print_options(bracket='')
121
122 # The multiplication table we're given is necessarily in terms
123 # of vectors, because we don't have an algebra yet for
124 # anything to be an element of. However, it's faster in the
125 # long run to have the multiplication table be in terms of
126 # algebra elements. We do this after calling the superclass
127 # constructor so that from_vector() knows what to do.
128 self._multiplication_table = [
129 list(map(lambda x: self.from_vector(x), ls))
130 for ls in mult_table
131 ]
132
133 if check:
134 if not self._is_commutative():
135 raise ValueError("algebra is not commutative")
136 if not self._is_jordanian():
137 raise ValueError("Jordan identity does not hold")
138 if not self._inner_product_is_associative():
139 raise ValueError("inner product is not associative")
140
141 def _element_constructor_(self, elt):
142 """
143 Construct an element of this algebra from its natural
144 representation.
145
146 This gets called only after the parent element _call_ method
147 fails to find a coercion for the argument.
148
149 SETUP::
150
151 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
152 ....: HadamardEJA,
153 ....: RealSymmetricEJA)
154
155 EXAMPLES:
156
157 The identity in `S^n` is converted to the identity in the EJA::
158
159 sage: J = RealSymmetricEJA(3)
160 sage: I = matrix.identity(QQ,3)
161 sage: J(I) == J.one()
162 True
163
164 This skew-symmetric matrix can't be represented in the EJA::
165
166 sage: J = RealSymmetricEJA(3)
167 sage: A = matrix(QQ,3, lambda i,j: i-j)
168 sage: J(A)
169 Traceback (most recent call last):
170 ...
171 ArithmeticError: vector is not in free module
172
173 TESTS:
174
175 Ensure that we can convert any element of the two non-matrix
176 simple algebras (whose natural representations are their usual
177 vector representations) back and forth faithfully::
178
179 sage: set_random_seed()
180 sage: J = HadamardEJA.random_instance()
181 sage: x = J.random_element()
182 sage: J(x.to_vector().column()) == x
183 True
184 sage: J = JordanSpinEJA.random_instance()
185 sage: x = J.random_element()
186 sage: J(x.to_vector().column()) == x
187 True
188
189 """
190 msg = "not a naturally-represented algebra element"
191 if elt == 0:
192 # The superclass implementation of random_element()
193 # needs to be able to coerce "0" into the algebra.
194 return self.zero()
195 elif elt in self.base_ring():
196 # Ensure that no base ring -> algebra coercion is performed
197 # by this method. There's some stupidity in sage that would
198 # otherwise propagate to this method; for example, sage thinks
199 # that the integer 3 belongs to the space of 2-by-2 matrices.
200 raise ValueError(msg)
201
202 natural_basis = self.natural_basis()
203 basis_space = natural_basis[0].matrix_space()
204 if elt not in basis_space:
205 raise ValueError(msg)
206
207 # Thanks for nothing! Matrix spaces aren't vector spaces in
208 # Sage, so we have to figure out its natural-basis coordinates
209 # ourselves. We use the basis space's ring instead of the
210 # element's ring because the basis space might be an algebraic
211 # closure whereas the base ring of the 3-by-3 identity matrix
212 # could be QQ instead of QQbar.
213 V = VectorSpace(basis_space.base_ring(), elt.nrows()*elt.ncols())
214 W = V.span_of_basis( _mat2vec(s) for s in natural_basis )
215 coords = W.coordinate_vector(_mat2vec(elt))
216 return self.from_vector(coords)
217
218 @staticmethod
219 def _max_test_case_size():
220 """
221 Return an integer "size" that is an upper bound on the size of
222 this algebra when it is used in a random test
223 case. Unfortunately, the term "size" is quite vague -- when
224 dealing with `R^n` under either the Hadamard or Jordan spin
225 product, the "size" refers to the dimension `n`. When dealing
226 with a matrix algebra (real symmetric or complex/quaternion
227 Hermitian), it refers to the size of the matrix, which is
228 far less than the dimension of the underlying vector space.
229
230 We default to five in this class, which is safe in `R^n`. The
231 matrix algebra subclasses (or any class where the "size" is
232 interpreted to be far less than the dimension) should override
233 with a smaller number.
234 """
235 return 5
236
237 def _repr_(self):
238 """
239 Return a string representation of ``self``.
240
241 SETUP::
242
243 sage: from mjo.eja.eja_algebra import JordanSpinEJA
244
245 TESTS:
246
247 Ensure that it says what we think it says::
248
249 sage: JordanSpinEJA(2, field=AA)
250 Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
251 sage: JordanSpinEJA(3, field=RDF)
252 Euclidean Jordan algebra of dimension 3 over Real Double Field
253
254 """
255 fmt = "Euclidean Jordan algebra of dimension {} over {}"
256 return fmt.format(self.dimension(), self.base_ring())
257
258 def product_on_basis(self, i, j):
259 return self._multiplication_table[i][j]
260
261 def _is_commutative(self):
262 r"""
263 Whether or not this algebra's multiplication table is commutative.
264
265 This method should of course always return ``True``, unless
266 this algebra was constructed with ``check=False`` and passed
267 an invalid multiplication table.
268 """
269 return all( self.product_on_basis(i,j) == self.product_on_basis(i,j)
270 for i in range(self.dimension())
271 for j in range(self.dimension()) )
272
273 def _is_jordanian(self):
274 r"""
275 Whether or not this algebra's multiplication table respects the
276 Jordan identity `(x^{2})(xy) = x(x^{2}y)`.
277
278 We only check one arrangement of `x` and `y`, so for a
279 ``True`` result to be truly true, you should also check
280 :meth:`_is_commutative`. This method should of course always
281 return ``True``, unless this algebra was constructed with
282 ``check=False`` and passed an invalid multiplication table.
283 """
284 return all( (self.monomial(i)**2)*(self.monomial(i)*self.monomial(j))
285 ==
286 (self.monomial(i))*((self.monomial(i)**2)*self.monomial(j))
287 for i in range(self.dimension())
288 for j in range(self.dimension()) )
289
290 def _inner_product_is_associative(self):
291 r"""
292 Return whether or not this algebra's inner product `B` is
293 associative; that is, whether or not `B(xy,z) = B(x,yz)`.
294
295 This method should of course always return ``True``, unless
296 this algebra was constructed with ``check=False`` and passed
297 an invalid multiplication table.
298 """
299
300 # Used to check whether or not something is zero in an inexact
301 # ring. This number is sufficient to allow the construction of
302 # QuaternionHermitianEJA(2, RDF) with check=True.
303 epsilon = 1e-16
304
305 for i in range(self.dimension()):
306 for j in range(self.dimension()):
307 for k in range(self.dimension()):
308 x = self.monomial(i)
309 y = self.monomial(j)
310 z = self.monomial(k)
311 diff = (x*y).inner_product(z) - x.inner_product(y*z)
312
313 if self.base_ring().is_exact():
314 if diff != 0:
315 return False
316 else:
317 if diff.abs() > epsilon:
318 return False
319
320 return True
321
322 @cached_method
323 def characteristic_polynomial_of(self):
324 """
325 Return the algebra's "characteristic polynomial of" function,
326 which is itself a multivariate polynomial that, when evaluated
327 at the coordinates of some algebra element, returns that
328 element's characteristic polynomial.
329
330 The resulting polynomial has `n+1` variables, where `n` is the
331 dimension of this algebra. The first `n` variables correspond to
332 the coordinates of an algebra element: when evaluated at the
333 coordinates of an algebra element with respect to a certain
334 basis, the result is a univariate polynomial (in the one
335 remaining variable ``t``), namely the characteristic polynomial
336 of that element.
337
338 SETUP::
339
340 sage: from mjo.eja.eja_algebra import JordanSpinEJA, TrivialEJA
341
342 EXAMPLES:
343
344 The characteristic polynomial in the spin algebra is given in
345 Alizadeh, Example 11.11::
346
347 sage: J = JordanSpinEJA(3)
348 sage: p = J.characteristic_polynomial_of(); p
349 X1^2 - X2^2 - X3^2 + (-2*t)*X1 + t^2
350 sage: xvec = J.one().to_vector()
351 sage: p(*xvec)
352 t^2 - 2*t + 1
353
354 By definition, the characteristic polynomial is a monic
355 degree-zero polynomial in a rank-zero algebra. Note that
356 Cayley-Hamilton is indeed satisfied since the polynomial
357 ``1`` evaluates to the identity element of the algebra on
358 any argument::
359
360 sage: J = TrivialEJA()
361 sage: J.characteristic_polynomial_of()
362 1
363
364 """
365 r = self.rank()
366 n = self.dimension()
367
368 # The list of coefficient polynomials a_0, a_1, a_2, ..., a_(r-1).
369 a = self._charpoly_coefficients()
370
371 # We go to a bit of trouble here to reorder the
372 # indeterminates, so that it's easier to evaluate the
373 # characteristic polynomial at x's coordinates and get back
374 # something in terms of t, which is what we want.
375 S = PolynomialRing(self.base_ring(),'t')
376 t = S.gen(0)
377 if r > 0:
378 R = a[0].parent()
379 S = PolynomialRing(S, R.variable_names())
380 t = S(t)
381
382 return (t**r + sum( a[k]*(t**k) for k in range(r) ))
383
384
385 def inner_product(self, x, y):
386 """
387 The inner product associated with this Euclidean Jordan algebra.
388
389 Defaults to the trace inner product, but can be overridden by
390 subclasses if they are sure that the necessary properties are
391 satisfied.
392
393 SETUP::
394
395 sage: from mjo.eja.eja_algebra import random_eja
396
397 EXAMPLES:
398
399 Our inner product is "associative," which means the following for
400 a symmetric bilinear form::
401
402 sage: set_random_seed()
403 sage: J = random_eja()
404 sage: x,y,z = J.random_elements(3)
405 sage: (x*y).inner_product(z) == y.inner_product(x*z)
406 True
407
408 """
409 X = x.natural_representation()
410 Y = y.natural_representation()
411 return self.natural_inner_product(X,Y)
412
413
414 def is_trivial(self):
415 """
416 Return whether or not this algebra is trivial.
417
418 A trivial algebra contains only the zero element.
419
420 SETUP::
421
422 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
423 ....: TrivialEJA)
424
425 EXAMPLES::
426
427 sage: J = ComplexHermitianEJA(3)
428 sage: J.is_trivial()
429 False
430
431 ::
432
433 sage: J = TrivialEJA()
434 sage: J.is_trivial()
435 True
436
437 """
438 return self.dimension() == 0
439
440
441 def multiplication_table(self):
442 """
443 Return a visual representation of this algebra's multiplication
444 table (on basis elements).
445
446 SETUP::
447
448 sage: from mjo.eja.eja_algebra import JordanSpinEJA
449
450 EXAMPLES::
451
452 sage: J = JordanSpinEJA(4)
453 sage: J.multiplication_table()
454 +----++----+----+----+----+
455 | * || e0 | e1 | e2 | e3 |
456 +====++====+====+====+====+
457 | e0 || e0 | e1 | e2 | e3 |
458 +----++----+----+----+----+
459 | e1 || e1 | e0 | 0 | 0 |
460 +----++----+----+----+----+
461 | e2 || e2 | 0 | e0 | 0 |
462 +----++----+----+----+----+
463 | e3 || e3 | 0 | 0 | e0 |
464 +----++----+----+----+----+
465
466 """
467 M = list(self._multiplication_table) # copy
468 for i in range(len(M)):
469 # M had better be "square"
470 M[i] = [self.monomial(i)] + M[i]
471 M = [["*"] + list(self.gens())] + M
472 return table(M, header_row=True, header_column=True, frame=True)
473
474
475 def natural_basis(self):
476 """
477 Return a more-natural representation of this algebra's basis.
478
479 Every finite-dimensional Euclidean Jordan Algebra is a direct
480 sum of five simple algebras, four of which comprise Hermitian
481 matrices. This method returns the original "natural" basis
482 for our underlying vector space. (Typically, the natural basis
483 is used to construct the multiplication table in the first place.)
484
485 Note that this will always return a matrix. The standard basis
486 in `R^n` will be returned as `n`-by-`1` column matrices.
487
488 SETUP::
489
490 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
491 ....: RealSymmetricEJA)
492
493 EXAMPLES::
494
495 sage: J = RealSymmetricEJA(2)
496 sage: J.basis()
497 Finite family {0: e0, 1: e1, 2: e2}
498 sage: J.natural_basis()
499 (
500 [1 0] [ 0 0.7071067811865475?] [0 0]
501 [0 0], [0.7071067811865475? 0], [0 1]
502 )
503
504 ::
505
506 sage: J = JordanSpinEJA(2)
507 sage: J.basis()
508 Finite family {0: e0, 1: e1}
509 sage: J.natural_basis()
510 (
511 [1] [0]
512 [0], [1]
513 )
514
515 """
516 if self._natural_basis is None:
517 M = self.natural_basis_space()
518 return tuple( M(b.to_vector()) for b in self.basis() )
519 else:
520 return self._natural_basis
521
522
523 def natural_basis_space(self):
524 """
525 Return the matrix space in which this algebra's natural basis
526 elements live.
527
528 Generally this will be an `n`-by-`1` column-vector space,
529 except when the algebra is trivial. There it's `n`-by-`n`
530 (where `n` is zero), to ensure that two elements of the
531 natural basis space (empty matrices) can be multiplied.
532 """
533 if self.is_trivial():
534 return MatrixSpace(self.base_ring(), 0)
535 elif self._natural_basis is None or len(self._natural_basis) == 0:
536 return MatrixSpace(self.base_ring(), self.dimension(), 1)
537 else:
538 return self._natural_basis[0].matrix_space()
539
540
541 @staticmethod
542 def natural_inner_product(X,Y):
543 """
544 Compute the inner product of two naturally-represented elements.
545
546 For example in the real symmetric matrix EJA, this will compute
547 the trace inner-product of two n-by-n symmetric matrices. The
548 default should work for the real cartesian product EJA, the
549 Jordan spin EJA, and the real symmetric matrices. The others
550 will have to be overridden.
551 """
552 return (X.conjugate_transpose()*Y).trace()
553
554
555 @cached_method
556 def one(self):
557 """
558 Return the unit element of this algebra.
559
560 SETUP::
561
562 sage: from mjo.eja.eja_algebra import (HadamardEJA,
563 ....: random_eja)
564
565 EXAMPLES::
566
567 sage: J = HadamardEJA(5)
568 sage: J.one()
569 e0 + e1 + e2 + e3 + e4
570
571 TESTS:
572
573 The identity element acts like the identity::
574
575 sage: set_random_seed()
576 sage: J = random_eja()
577 sage: x = J.random_element()
578 sage: J.one()*x == x and x*J.one() == x
579 True
580
581 The matrix of the unit element's operator is the identity::
582
583 sage: set_random_seed()
584 sage: J = random_eja()
585 sage: actual = J.one().operator().matrix()
586 sage: expected = matrix.identity(J.base_ring(), J.dimension())
587 sage: actual == expected
588 True
589
590 """
591 # We can brute-force compute the matrices of the operators
592 # that correspond to the basis elements of this algebra.
593 # If some linear combination of those basis elements is the
594 # algebra identity, then the same linear combination of
595 # their matrices has to be the identity matrix.
596 #
597 # Of course, matrices aren't vectors in sage, so we have to
598 # appeal to the "long vectors" isometry.
599 oper_vecs = [ _mat2vec(g.operator().matrix()) for g in self.gens() ]
600
601 # Now we use basis linear algebra to find the coefficients,
602 # of the matrices-as-vectors-linear-combination, which should
603 # work for the original algebra basis too.
604 A = matrix.column(self.base_ring(), oper_vecs)
605
606 # We used the isometry on the left-hand side already, but we
607 # still need to do it for the right-hand side. Recall that we
608 # wanted something that summed to the identity matrix.
609 b = _mat2vec( matrix.identity(self.base_ring(), self.dimension()) )
610
611 # Now if there's an identity element in the algebra, this should work.
612 coeffs = A.solve_right(b)
613 return self.linear_combination(zip(self.gens(), coeffs))
614
615
616 def peirce_decomposition(self, c):
617 """
618 The Peirce decomposition of this algebra relative to the
619 idempotent ``c``.
620
621 In the future, this can be extended to a complete system of
622 orthogonal idempotents.
623
624 INPUT:
625
626 - ``c`` -- an idempotent of this algebra.
627
628 OUTPUT:
629
630 A triple (J0, J5, J1) containing two subalgebras and one subspace
631 of this algebra,
632
633 - ``J0`` -- the algebra on the eigenspace of ``c.operator()``
634 corresponding to the eigenvalue zero.
635
636 - ``J5`` -- the eigenspace (NOT a subalgebra) of ``c.operator()``
637 corresponding to the eigenvalue one-half.
638
639 - ``J1`` -- the algebra on the eigenspace of ``c.operator()``
640 corresponding to the eigenvalue one.
641
642 These are the only possible eigenspaces for that operator, and this
643 algebra is a direct sum of them. The spaces ``J0`` and ``J1`` are
644 orthogonal, and are subalgebras of this algebra with the appropriate
645 restrictions.
646
647 SETUP::
648
649 sage: from mjo.eja.eja_algebra import random_eja, RealSymmetricEJA
650
651 EXAMPLES:
652
653 The canonical example comes from the symmetric matrices, which
654 decompose into diagonal and off-diagonal parts::
655
656 sage: J = RealSymmetricEJA(3)
657 sage: C = matrix(QQ, [ [1,0,0],
658 ....: [0,1,0],
659 ....: [0,0,0] ])
660 sage: c = J(C)
661 sage: J0,J5,J1 = J.peirce_decomposition(c)
662 sage: J0
663 Euclidean Jordan algebra of dimension 1...
664 sage: J5
665 Vector space of degree 6 and dimension 2...
666 sage: J1
667 Euclidean Jordan algebra of dimension 3...
668 sage: J0.one().natural_representation()
669 [0 0 0]
670 [0 0 0]
671 [0 0 1]
672 sage: orig_df = AA.options.display_format
673 sage: AA.options.display_format = 'radical'
674 sage: J.from_vector(J5.basis()[0]).natural_representation()
675 [ 0 0 1/2*sqrt(2)]
676 [ 0 0 0]
677 [1/2*sqrt(2) 0 0]
678 sage: J.from_vector(J5.basis()[1]).natural_representation()
679 [ 0 0 0]
680 [ 0 0 1/2*sqrt(2)]
681 [ 0 1/2*sqrt(2) 0]
682 sage: AA.options.display_format = orig_df
683 sage: J1.one().natural_representation()
684 [1 0 0]
685 [0 1 0]
686 [0 0 0]
687
688 TESTS:
689
690 Every algebra decomposes trivially with respect to its identity
691 element::
692
693 sage: set_random_seed()
694 sage: J = random_eja()
695 sage: J0,J5,J1 = J.peirce_decomposition(J.one())
696 sage: J0.dimension() == 0 and J5.dimension() == 0
697 True
698 sage: J1.superalgebra() == J and J1.dimension() == J.dimension()
699 True
700
701 The decomposition is into eigenspaces, and its components are
702 therefore necessarily orthogonal. Moreover, the identity
703 elements in the two subalgebras are the projections onto their
704 respective subspaces of the superalgebra's identity element::
705
706 sage: set_random_seed()
707 sage: J = random_eja()
708 sage: x = J.random_element()
709 sage: if not J.is_trivial():
710 ....: while x.is_nilpotent():
711 ....: x = J.random_element()
712 sage: c = x.subalgebra_idempotent()
713 sage: J0,J5,J1 = J.peirce_decomposition(c)
714 sage: ipsum = 0
715 sage: for (w,y,z) in zip(J0.basis(), J5.basis(), J1.basis()):
716 ....: w = w.superalgebra_element()
717 ....: y = J.from_vector(y)
718 ....: z = z.superalgebra_element()
719 ....: ipsum += w.inner_product(y).abs()
720 ....: ipsum += w.inner_product(z).abs()
721 ....: ipsum += y.inner_product(z).abs()
722 sage: ipsum
723 0
724 sage: J1(c) == J1.one()
725 True
726 sage: J0(J.one() - c) == J0.one()
727 True
728
729 """
730 if not c.is_idempotent():
731 raise ValueError("element is not idempotent: %s" % c)
732
733 # Default these to what they should be if they turn out to be
734 # trivial, because eigenspaces_left() won't return eigenvalues
735 # corresponding to trivial spaces (e.g. it returns only the
736 # eigenspace corresponding to lambda=1 if you take the
737 # decomposition relative to the identity element).
738 trivial = FiniteDimensionalEuclideanJordanSubalgebra(self, ())
739 J0 = trivial # eigenvalue zero
740 J5 = VectorSpace(self.base_ring(), 0) # eigenvalue one-half
741 J1 = trivial # eigenvalue one
742
743 for (eigval, eigspace) in c.operator().matrix().right_eigenspaces():
744 if eigval == ~(self.base_ring()(2)):
745 J5 = eigspace
746 else:
747 gens = tuple( self.from_vector(b) for b in eigspace.basis() )
748 subalg = FiniteDimensionalEuclideanJordanSubalgebra(self,
749 gens,
750 check=False)
751 if eigval == 0:
752 J0 = subalg
753 elif eigval == 1:
754 J1 = subalg
755 else:
756 raise ValueError("unexpected eigenvalue: %s" % eigval)
757
758 return (J0, J5, J1)
759
760
761 def random_element(self, thorough=False):
762 r"""
763 Return a random element of this algebra.
764
765 Our algebra superclass method only returns a linear
766 combination of at most two basis elements. We instead
767 want the vector space "random element" method that
768 returns a more diverse selection.
769
770 INPUT:
771
772 - ``thorough`` -- (boolean; default False) whether or not we
773 should generate irrational coefficients for the random
774 element when our base ring is irrational; this slows the
775 algebra operations to a crawl, but any truly random method
776 should include them
777
778 """
779 # For a general base ring... maybe we can trust this to do the
780 # right thing? Unlikely, but.
781 V = self.vector_space()
782 v = V.random_element()
783
784 if self.base_ring() is AA:
785 # The "random element" method of the algebraic reals is
786 # stupid at the moment, and only returns integers between
787 # -2 and 2, inclusive:
788 #
789 # https://trac.sagemath.org/ticket/30875
790 #
791 # Instead, we implement our own "random vector" method,
792 # and then coerce that into the algebra. We use the vector
793 # space degree here instead of the dimension because a
794 # subalgebra could (for example) be spanned by only two
795 # vectors, each with five coordinates. We need to
796 # generate all five coordinates.
797 if thorough:
798 v *= QQbar.random_element().real()
799 else:
800 v *= QQ.random_element()
801
802 return self.from_vector(V.coordinate_vector(v))
803
804 def random_elements(self, count, thorough=False):
805 """
806 Return ``count`` random elements as a tuple.
807
808 INPUT:
809
810 - ``thorough`` -- (boolean; default False) whether or not we
811 should generate irrational coefficients for the random
812 elements when our base ring is irrational; this slows the
813 algebra operations to a crawl, but any truly random method
814 should include them
815
816 SETUP::
817
818 sage: from mjo.eja.eja_algebra import JordanSpinEJA
819
820 EXAMPLES::
821
822 sage: J = JordanSpinEJA(3)
823 sage: x,y,z = J.random_elements(3)
824 sage: all( [ x in J, y in J, z in J ])
825 True
826 sage: len( J.random_elements(10) ) == 10
827 True
828
829 """
830 return tuple( self.random_element(thorough)
831 for idx in range(count) )
832
833 @classmethod
834 def random_instance(cls, field=AA, **kwargs):
835 """
836 Return a random instance of this type of algebra.
837
838 Beware, this will crash for "most instances" because the
839 constructor below looks wrong.
840 """
841 if cls is TrivialEJA:
842 # The TrivialEJA class doesn't take an "n" argument because
843 # there's only one.
844 return cls(field)
845
846 n = ZZ.random_element(cls._max_test_case_size() + 1)
847 return cls(n, field, **kwargs)
848
849 @cached_method
850 def _charpoly_coefficients(self):
851 r"""
852 The `r` polynomial coefficients of the "characteristic polynomial
853 of" function.
854 """
855 n = self.dimension()
856 var_names = [ "X" + str(z) for z in range(1,n+1) ]
857 R = PolynomialRing(self.base_ring(), var_names)
858 vars = R.gens()
859 F = R.fraction_field()
860
861 def L_x_i_j(i,j):
862 # From a result in my book, these are the entries of the
863 # basis representation of L_x.
864 return sum( vars[k]*self.monomial(k).operator().matrix()[i,j]
865 for k in range(n) )
866
867 L_x = matrix(F, n, n, L_x_i_j)
868
869 r = None
870 if self.rank.is_in_cache():
871 r = self.rank()
872 # There's no need to pad the system with redundant
873 # columns if we *know* they'll be redundant.
874 n = r
875
876 # Compute an extra power in case the rank is equal to
877 # the dimension (otherwise, we would stop at x^(r-1)).
878 x_powers = [ (L_x**k)*self.one().to_vector()
879 for k in range(n+1) ]
880 A = matrix.column(F, x_powers[:n])
881 AE = A.extended_echelon_form()
882 E = AE[:,n:]
883 A_rref = AE[:,:n]
884 if r is None:
885 r = A_rref.rank()
886 b = x_powers[r]
887
888 # The theory says that only the first "r" coefficients are
889 # nonzero, and they actually live in the original polynomial
890 # ring and not the fraction field. We negate them because
891 # in the actual characteristic polynomial, they get moved
892 # to the other side where x^r lives.
893 return -A_rref.solve_right(E*b).change_ring(R)[:r]
894
895 @cached_method
896 def rank(self):
897 r"""
898 Return the rank of this EJA.
899
900 This is a cached method because we know the rank a priori for
901 all of the algebras we can construct. Thus we can avoid the
902 expensive ``_charpoly_coefficients()`` call unless we truly
903 need to compute the whole characteristic polynomial.
904
905 SETUP::
906
907 sage: from mjo.eja.eja_algebra import (HadamardEJA,
908 ....: JordanSpinEJA,
909 ....: RealSymmetricEJA,
910 ....: ComplexHermitianEJA,
911 ....: QuaternionHermitianEJA,
912 ....: random_eja)
913
914 EXAMPLES:
915
916 The rank of the Jordan spin algebra is always two::
917
918 sage: JordanSpinEJA(2).rank()
919 2
920 sage: JordanSpinEJA(3).rank()
921 2
922 sage: JordanSpinEJA(4).rank()
923 2
924
925 The rank of the `n`-by-`n` Hermitian real, complex, or
926 quaternion matrices is `n`::
927
928 sage: RealSymmetricEJA(4).rank()
929 4
930 sage: ComplexHermitianEJA(3).rank()
931 3
932 sage: QuaternionHermitianEJA(2).rank()
933 2
934
935 TESTS:
936
937 Ensure that every EJA that we know how to construct has a
938 positive integer rank, unless the algebra is trivial in
939 which case its rank will be zero::
940
941 sage: set_random_seed()
942 sage: J = random_eja()
943 sage: r = J.rank()
944 sage: r in ZZ
945 True
946 sage: r > 0 or (r == 0 and J.is_trivial())
947 True
948
949 Ensure that computing the rank actually works, since the ranks
950 of all simple algebras are known and will be cached by default::
951
952 sage: J = HadamardEJA(4)
953 sage: J.rank.clear_cache()
954 sage: J.rank()
955 4
956
957 ::
958
959 sage: J = JordanSpinEJA(4)
960 sage: J.rank.clear_cache()
961 sage: J.rank()
962 2
963
964 ::
965
966 sage: J = RealSymmetricEJA(3)
967 sage: J.rank.clear_cache()
968 sage: J.rank()
969 3
970
971 ::
972
973 sage: J = ComplexHermitianEJA(2)
974 sage: J.rank.clear_cache()
975 sage: J.rank()
976 2
977
978 ::
979
980 sage: J = QuaternionHermitianEJA(2)
981 sage: J.rank.clear_cache()
982 sage: J.rank()
983 2
984 """
985 return len(self._charpoly_coefficients())
986
987
988 def vector_space(self):
989 """
990 Return the vector space that underlies this algebra.
991
992 SETUP::
993
994 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
995
996 EXAMPLES::
997
998 sage: J = RealSymmetricEJA(2)
999 sage: J.vector_space()
1000 Vector space of dimension 3 over...
1001
1002 """
1003 return self.zero().to_vector().parent().ambient_vector_space()
1004
1005
1006 Element = FiniteDimensionalEuclideanJordanAlgebraElement
1007
1008
1009 class HadamardEJA(FiniteDimensionalEuclideanJordanAlgebra):
1010 """
1011 Return the Euclidean Jordan Algebra corresponding to the set
1012 `R^n` under the Hadamard product.
1013
1014 Note: this is nothing more than the Cartesian product of ``n``
1015 copies of the spin algebra. Once Cartesian product algebras
1016 are implemented, this can go.
1017
1018 SETUP::
1019
1020 sage: from mjo.eja.eja_algebra import HadamardEJA
1021
1022 EXAMPLES:
1023
1024 This multiplication table can be verified by hand::
1025
1026 sage: J = HadamardEJA(3)
1027 sage: e0,e1,e2 = J.gens()
1028 sage: e0*e0
1029 e0
1030 sage: e0*e1
1031 0
1032 sage: e0*e2
1033 0
1034 sage: e1*e1
1035 e1
1036 sage: e1*e2
1037 0
1038 sage: e2*e2
1039 e2
1040
1041 TESTS:
1042
1043 We can change the generator prefix::
1044
1045 sage: HadamardEJA(3, prefix='r').gens()
1046 (r0, r1, r2)
1047
1048 """
1049 def __init__(self, n, field=AA, **kwargs):
1050 V = VectorSpace(field, n)
1051 mult_table = [ [ V.gen(i)*(i == j) for j in range(n) ]
1052 for i in range(n) ]
1053
1054 super(HadamardEJA, self).__init__(field,
1055 mult_table,
1056 check=False,
1057 **kwargs)
1058 self.rank.set_cache(n)
1059
1060 def inner_product(self, x, y):
1061 """
1062 Faster to reimplement than to use natural representations.
1063
1064 SETUP::
1065
1066 sage: from mjo.eja.eja_algebra import HadamardEJA
1067
1068 TESTS:
1069
1070 Ensure that this is the usual inner product for the algebras
1071 over `R^n`::
1072
1073 sage: set_random_seed()
1074 sage: J = HadamardEJA.random_instance()
1075 sage: x,y = J.random_elements(2)
1076 sage: X = x.natural_representation()
1077 sage: Y = y.natural_representation()
1078 sage: x.inner_product(y) == J.natural_inner_product(X,Y)
1079 True
1080
1081 """
1082 return x.to_vector().inner_product(y.to_vector())
1083
1084
1085 def random_eja(field=AA):
1086 """
1087 Return a "random" finite-dimensional Euclidean Jordan Algebra.
1088
1089 SETUP::
1090
1091 sage: from mjo.eja.eja_algebra import random_eja
1092
1093 TESTS::
1094
1095 sage: random_eja()
1096 Euclidean Jordan algebra of dimension...
1097
1098 """
1099 classname = choice([TrivialEJA,
1100 HadamardEJA,
1101 JordanSpinEJA,
1102 RealSymmetricEJA,
1103 ComplexHermitianEJA,
1104 QuaternionHermitianEJA])
1105 return classname.random_instance(field=field)
1106
1107
1108
1109
1110 class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
1111 @staticmethod
1112 def _max_test_case_size():
1113 # Play it safe, since this will be squared and the underlying
1114 # field can have dimension 4 (quaternions) too.
1115 return 2
1116
1117 def __init__(self, field, basis, normalize_basis=True, **kwargs):
1118 """
1119 Compared to the superclass constructor, we take a basis instead of
1120 a multiplication table because the latter can be computed in terms
1121 of the former when the product is known (like it is here).
1122 """
1123 # Used in this class's fast _charpoly_coefficients() override.
1124 self._basis_normalizers = None
1125
1126 # We're going to loop through this a few times, so now's a good
1127 # time to ensure that it isn't a generator expression.
1128 basis = tuple(basis)
1129
1130 if len(basis) > 1 and normalize_basis:
1131 # We'll need sqrt(2) to normalize the basis, and this
1132 # winds up in the multiplication table, so the whole
1133 # algebra needs to be over the field extension.
1134 R = PolynomialRing(field, 'z')
1135 z = R.gen()
1136 p = z**2 - 2
1137 if p.is_irreducible():
1138 field = field.extension(p, 'sqrt2', embedding=RLF(2).sqrt())
1139 basis = tuple( s.change_ring(field) for s in basis )
1140 self._basis_normalizers = tuple(
1141 ~(self.natural_inner_product(s,s).sqrt()) for s in basis )
1142 basis = tuple(s*c for (s,c) in zip(basis,self._basis_normalizers))
1143
1144 Qs = self.multiplication_table_from_matrix_basis(basis)
1145
1146 super(MatrixEuclideanJordanAlgebra, self).__init__(field,
1147 Qs,
1148 natural_basis=basis,
1149 **kwargs)
1150
1151
1152 @cached_method
1153 def _charpoly_coefficients(self):
1154 r"""
1155 Override the parent method with something that tries to compute
1156 over a faster (non-extension) field.
1157 """
1158 if self._basis_normalizers is None:
1159 # We didn't normalize, so assume that the basis we started
1160 # with had entries in a nice field.
1161 return super(MatrixEuclideanJordanAlgebra, self)._charpoly_coefficients()
1162 else:
1163 basis = ( (b/n) for (b,n) in zip(self.natural_basis(),
1164 self._basis_normalizers) )
1165
1166 # Do this over the rationals and convert back at the end.
1167 # Only works because we know the entries of the basis are
1168 # integers. The argument ``check=False`` is required
1169 # because the trace inner-product method for this
1170 # class is a stub and can't actually be checked.
1171 J = MatrixEuclideanJordanAlgebra(QQ,
1172 basis,
1173 normalize_basis=False,
1174 check=False)
1175 a = J._charpoly_coefficients()
1176
1177 # Unfortunately, changing the basis does change the
1178 # coefficients of the characteristic polynomial, but since
1179 # these are really the coefficients of the "characteristic
1180 # polynomial of" function, everything is still nice and
1181 # unevaluated. It's therefore "obvious" how scaling the
1182 # basis affects the coordinate variables X1, X2, et
1183 # cetera. Scaling the first basis vector up by "n" adds a
1184 # factor of 1/n into every "X1" term, for example. So here
1185 # we simply undo the basis_normalizer scaling that we
1186 # performed earlier.
1187 #
1188 # The a[0] access here is safe because trivial algebras
1189 # won't have any basis normalizers and therefore won't
1190 # make it to this "else" branch.
1191 XS = a[0].parent().gens()
1192 subs_dict = { XS[i]: self._basis_normalizers[i]*XS[i]
1193 for i in range(len(XS)) }
1194 return tuple( a_i.subs(subs_dict) for a_i in a )
1195
1196
1197 @staticmethod
1198 def multiplication_table_from_matrix_basis(basis):
1199 """
1200 At least three of the five simple Euclidean Jordan algebras have the
1201 symmetric multiplication (A,B) |-> (AB + BA)/2, where the
1202 multiplication on the right is matrix multiplication. Given a basis
1203 for the underlying matrix space, this function returns a
1204 multiplication table (obtained by looping through the basis
1205 elements) for an algebra of those matrices.
1206 """
1207 # In S^2, for example, we nominally have four coordinates even
1208 # though the space is of dimension three only. The vector space V
1209 # is supposed to hold the entire long vector, and the subspace W
1210 # of V will be spanned by the vectors that arise from symmetric
1211 # matrices. Thus for S^2, dim(V) == 4 and dim(W) == 3.
1212 if len(basis) == 0:
1213 return []
1214
1215 field = basis[0].base_ring()
1216 dimension = basis[0].nrows()
1217
1218 V = VectorSpace(field, dimension**2)
1219 W = V.span_of_basis( _mat2vec(s) for s in basis )
1220 n = len(basis)
1221 mult_table = [[W.zero() for j in range(n)] for i in range(n)]
1222 for i in range(n):
1223 for j in range(n):
1224 mat_entry = (basis[i]*basis[j] + basis[j]*basis[i])/2
1225 mult_table[i][j] = W.coordinate_vector(_mat2vec(mat_entry))
1226
1227 return mult_table
1228
1229
1230 @staticmethod
1231 def real_embed(M):
1232 """
1233 Embed the matrix ``M`` into a space of real matrices.
1234
1235 The matrix ``M`` can have entries in any field at the moment:
1236 the real numbers, complex numbers, or quaternions. And although
1237 they are not a field, we can probably support octonions at some
1238 point, too. This function returns a real matrix that "acts like"
1239 the original with respect to matrix multiplication; i.e.
1240
1241 real_embed(M*N) = real_embed(M)*real_embed(N)
1242
1243 """
1244 raise NotImplementedError
1245
1246
1247 @staticmethod
1248 def real_unembed(M):
1249 """
1250 The inverse of :meth:`real_embed`.
1251 """
1252 raise NotImplementedError
1253
1254
1255 @classmethod
1256 def natural_inner_product(cls,X,Y):
1257 Xu = cls.real_unembed(X)
1258 Yu = cls.real_unembed(Y)
1259 tr = (Xu*Yu).trace()
1260
1261 if tr in RLF:
1262 # It's real already.
1263 return tr
1264
1265 # Otherwise, try the thing that works for complex numbers; and
1266 # if that doesn't work, the thing that works for quaternions.
1267 try:
1268 return tr.vector()[0] # real part, imag part is index 1
1269 except AttributeError:
1270 # A quaternions doesn't have a vector() method, but does
1271 # have coefficient_tuple() method that returns the
1272 # coefficients of 1, i, j, and k -- in that order.
1273 return tr.coefficient_tuple()[0]
1274
1275
1276 class RealMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
1277 @staticmethod
1278 def real_embed(M):
1279 """
1280 The identity function, for embedding real matrices into real
1281 matrices.
1282 """
1283 return M
1284
1285 @staticmethod
1286 def real_unembed(M):
1287 """
1288 The identity function, for unembedding real matrices from real
1289 matrices.
1290 """
1291 return M
1292
1293
1294 class RealSymmetricEJA(RealMatrixEuclideanJordanAlgebra):
1295 """
1296 The rank-n simple EJA consisting of real symmetric n-by-n
1297 matrices, the usual symmetric Jordan product, and the trace inner
1298 product. It has dimension `(n^2 + n)/2` over the reals.
1299
1300 SETUP::
1301
1302 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1303
1304 EXAMPLES::
1305
1306 sage: J = RealSymmetricEJA(2)
1307 sage: e0, e1, e2 = J.gens()
1308 sage: e0*e0
1309 e0
1310 sage: e1*e1
1311 1/2*e0 + 1/2*e2
1312 sage: e2*e2
1313 e2
1314
1315 In theory, our "field" can be any subfield of the reals::
1316
1317 sage: RealSymmetricEJA(2, RDF)
1318 Euclidean Jordan algebra of dimension 3 over Real Double Field
1319 sage: RealSymmetricEJA(2, RR)
1320 Euclidean Jordan algebra of dimension 3 over Real Field with
1321 53 bits of precision
1322
1323 TESTS:
1324
1325 The dimension of this algebra is `(n^2 + n) / 2`::
1326
1327 sage: set_random_seed()
1328 sage: n_max = RealSymmetricEJA._max_test_case_size()
1329 sage: n = ZZ.random_element(1, n_max)
1330 sage: J = RealSymmetricEJA(n)
1331 sage: J.dimension() == (n^2 + n)/2
1332 True
1333
1334 The Jordan multiplication is what we think it is::
1335
1336 sage: set_random_seed()
1337 sage: J = RealSymmetricEJA.random_instance()
1338 sage: x,y = J.random_elements(2)
1339 sage: actual = (x*y).natural_representation()
1340 sage: X = x.natural_representation()
1341 sage: Y = y.natural_representation()
1342 sage: expected = (X*Y + Y*X)/2
1343 sage: actual == expected
1344 True
1345 sage: J(expected) == x*y
1346 True
1347
1348 We can change the generator prefix::
1349
1350 sage: RealSymmetricEJA(3, prefix='q').gens()
1351 (q0, q1, q2, q3, q4, q5)
1352
1353 Our natural basis is normalized with respect to the natural inner
1354 product unless we specify otherwise::
1355
1356 sage: set_random_seed()
1357 sage: J = RealSymmetricEJA.random_instance()
1358 sage: all( b.norm() == 1 for b in J.gens() )
1359 True
1360
1361 Since our natural basis is normalized with respect to the natural
1362 inner product, and since we know that this algebra is an EJA, any
1363 left-multiplication operator's matrix will be symmetric because
1364 natural->EJA basis representation is an isometry and within the EJA
1365 the operator is self-adjoint by the Jordan axiom::
1366
1367 sage: set_random_seed()
1368 sage: x = RealSymmetricEJA.random_instance().random_element()
1369 sage: x.operator().matrix().is_symmetric()
1370 True
1371
1372 We can construct the (trivial) algebra of rank zero::
1373
1374 sage: RealSymmetricEJA(0)
1375 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
1376
1377 """
1378 @classmethod
1379 def _denormalized_basis(cls, n, field):
1380 """
1381 Return a basis for the space of real symmetric n-by-n matrices.
1382
1383 SETUP::
1384
1385 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1386
1387 TESTS::
1388
1389 sage: set_random_seed()
1390 sage: n = ZZ.random_element(1,5)
1391 sage: B = RealSymmetricEJA._denormalized_basis(n,QQ)
1392 sage: all( M.is_symmetric() for M in B)
1393 True
1394
1395 """
1396 # The basis of symmetric matrices, as matrices, in their R^(n-by-n)
1397 # coordinates.
1398 S = []
1399 for i in range(n):
1400 for j in range(i+1):
1401 Eij = matrix(field, n, lambda k,l: k==i and l==j)
1402 if i == j:
1403 Sij = Eij
1404 else:
1405 Sij = Eij + Eij.transpose()
1406 S.append(Sij)
1407 return S
1408
1409
1410 @staticmethod
1411 def _max_test_case_size():
1412 return 4 # Dimension 10
1413
1414
1415 def __init__(self, n, field=AA, **kwargs):
1416 basis = self._denormalized_basis(n, field)
1417 super(RealSymmetricEJA, self).__init__(field,
1418 basis,
1419 check=False,
1420 **kwargs)
1421 self.rank.set_cache(n)
1422
1423
1424 class ComplexMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
1425 @staticmethod
1426 def real_embed(M):
1427 """
1428 Embed the n-by-n complex matrix ``M`` into the space of real
1429 matrices of size 2n-by-2n via the map the sends each entry `z = a +
1430 bi` to the block matrix ``[[a,b],[-b,a]]``.
1431
1432 SETUP::
1433
1434 sage: from mjo.eja.eja_algebra import \
1435 ....: ComplexMatrixEuclideanJordanAlgebra
1436
1437 EXAMPLES::
1438
1439 sage: F = QuadraticField(-1, 'I')
1440 sage: x1 = F(4 - 2*i)
1441 sage: x2 = F(1 + 2*i)
1442 sage: x3 = F(-i)
1443 sage: x4 = F(6)
1444 sage: M = matrix(F,2,[[x1,x2],[x3,x4]])
1445 sage: ComplexMatrixEuclideanJordanAlgebra.real_embed(M)
1446 [ 4 -2| 1 2]
1447 [ 2 4|-2 1]
1448 [-----+-----]
1449 [ 0 -1| 6 0]
1450 [ 1 0| 0 6]
1451
1452 TESTS:
1453
1454 Embedding is a homomorphism (isomorphism, in fact)::
1455
1456 sage: set_random_seed()
1457 sage: n_max = ComplexMatrixEuclideanJordanAlgebra._max_test_case_size()
1458 sage: n = ZZ.random_element(n_max)
1459 sage: F = QuadraticField(-1, 'I')
1460 sage: X = random_matrix(F, n)
1461 sage: Y = random_matrix(F, n)
1462 sage: Xe = ComplexMatrixEuclideanJordanAlgebra.real_embed(X)
1463 sage: Ye = ComplexMatrixEuclideanJordanAlgebra.real_embed(Y)
1464 sage: XYe = ComplexMatrixEuclideanJordanAlgebra.real_embed(X*Y)
1465 sage: Xe*Ye == XYe
1466 True
1467
1468 """
1469 n = M.nrows()
1470 if M.ncols() != n:
1471 raise ValueError("the matrix 'M' must be square")
1472
1473 # We don't need any adjoined elements...
1474 field = M.base_ring().base_ring()
1475
1476 blocks = []
1477 for z in M.list():
1478 a = z.list()[0] # real part, I guess
1479 b = z.list()[1] # imag part, I guess
1480 blocks.append(matrix(field, 2, [[a,b],[-b,a]]))
1481
1482 return matrix.block(field, n, blocks)
1483
1484
1485 @staticmethod
1486 def real_unembed(M):
1487 """
1488 The inverse of _embed_complex_matrix().
1489
1490 SETUP::
1491
1492 sage: from mjo.eja.eja_algebra import \
1493 ....: ComplexMatrixEuclideanJordanAlgebra
1494
1495 EXAMPLES::
1496
1497 sage: A = matrix(QQ,[ [ 1, 2, 3, 4],
1498 ....: [-2, 1, -4, 3],
1499 ....: [ 9, 10, 11, 12],
1500 ....: [-10, 9, -12, 11] ])
1501 sage: ComplexMatrixEuclideanJordanAlgebra.real_unembed(A)
1502 [ 2*I + 1 4*I + 3]
1503 [ 10*I + 9 12*I + 11]
1504
1505 TESTS:
1506
1507 Unembedding is the inverse of embedding::
1508
1509 sage: set_random_seed()
1510 sage: F = QuadraticField(-1, 'I')
1511 sage: M = random_matrix(F, 3)
1512 sage: Me = ComplexMatrixEuclideanJordanAlgebra.real_embed(M)
1513 sage: ComplexMatrixEuclideanJordanAlgebra.real_unembed(Me) == M
1514 True
1515
1516 """
1517 n = ZZ(M.nrows())
1518 if M.ncols() != n:
1519 raise ValueError("the matrix 'M' must be square")
1520 if not n.mod(2).is_zero():
1521 raise ValueError("the matrix 'M' must be a complex embedding")
1522
1523 # If "M" was normalized, its base ring might have roots
1524 # adjoined and they can stick around after unembedding.
1525 field = M.base_ring()
1526 R = PolynomialRing(field, 'z')
1527 z = R.gen()
1528 if field is AA:
1529 # Sage doesn't know how to embed AA into QQbar, i.e. how
1530 # to adjoin sqrt(-1) to AA.
1531 F = QQbar
1532 else:
1533 F = field.extension(z**2 + 1, 'I', embedding=CLF(-1).sqrt())
1534 i = F.gen()
1535
1536 # Go top-left to bottom-right (reading order), converting every
1537 # 2-by-2 block we see to a single complex element.
1538 elements = []
1539 for k in range(n/2):
1540 for j in range(n/2):
1541 submat = M[2*k:2*k+2,2*j:2*j+2]
1542 if submat[0,0] != submat[1,1]:
1543 raise ValueError('bad on-diagonal submatrix')
1544 if submat[0,1] != -submat[1,0]:
1545 raise ValueError('bad off-diagonal submatrix')
1546 z = submat[0,0] + submat[0,1]*i
1547 elements.append(z)
1548
1549 return matrix(F, n/2, elements)
1550
1551
1552 @classmethod
1553 def natural_inner_product(cls,X,Y):
1554 """
1555 Compute a natural inner product in this algebra directly from
1556 its real embedding.
1557
1558 SETUP::
1559
1560 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1561
1562 TESTS:
1563
1564 This gives the same answer as the slow, default method implemented
1565 in :class:`MatrixEuclideanJordanAlgebra`::
1566
1567 sage: set_random_seed()
1568 sage: J = ComplexHermitianEJA.random_instance()
1569 sage: x,y = J.random_elements(2)
1570 sage: Xe = x.natural_representation()
1571 sage: Ye = y.natural_representation()
1572 sage: X = ComplexHermitianEJA.real_unembed(Xe)
1573 sage: Y = ComplexHermitianEJA.real_unembed(Ye)
1574 sage: expected = (X*Y).trace().real()
1575 sage: actual = ComplexHermitianEJA.natural_inner_product(Xe,Ye)
1576 sage: actual == expected
1577 True
1578
1579 """
1580 return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/2
1581
1582
1583 class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra):
1584 """
1585 The rank-n simple EJA consisting of complex Hermitian n-by-n
1586 matrices over the real numbers, the usual symmetric Jordan product,
1587 and the real-part-of-trace inner product. It has dimension `n^2` over
1588 the reals.
1589
1590 SETUP::
1591
1592 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1593
1594 EXAMPLES:
1595
1596 In theory, our "field" can be any subfield of the reals::
1597
1598 sage: ComplexHermitianEJA(2, RDF)
1599 Euclidean Jordan algebra of dimension 4 over Real Double Field
1600 sage: ComplexHermitianEJA(2, RR)
1601 Euclidean Jordan algebra of dimension 4 over Real Field with
1602 53 bits of precision
1603
1604 TESTS:
1605
1606 The dimension of this algebra is `n^2`::
1607
1608 sage: set_random_seed()
1609 sage: n_max = ComplexHermitianEJA._max_test_case_size()
1610 sage: n = ZZ.random_element(1, n_max)
1611 sage: J = ComplexHermitianEJA(n)
1612 sage: J.dimension() == n^2
1613 True
1614
1615 The Jordan multiplication is what we think it is::
1616
1617 sage: set_random_seed()
1618 sage: J = ComplexHermitianEJA.random_instance()
1619 sage: x,y = J.random_elements(2)
1620 sage: actual = (x*y).natural_representation()
1621 sage: X = x.natural_representation()
1622 sage: Y = y.natural_representation()
1623 sage: expected = (X*Y + Y*X)/2
1624 sage: actual == expected
1625 True
1626 sage: J(expected) == x*y
1627 True
1628
1629 We can change the generator prefix::
1630
1631 sage: ComplexHermitianEJA(2, prefix='z').gens()
1632 (z0, z1, z2, z3)
1633
1634 Our natural basis is normalized with respect to the natural inner
1635 product unless we specify otherwise::
1636
1637 sage: set_random_seed()
1638 sage: J = ComplexHermitianEJA.random_instance()
1639 sage: all( b.norm() == 1 for b in J.gens() )
1640 True
1641
1642 Since our natural basis is normalized with respect to the natural
1643 inner product, and since we know that this algebra is an EJA, any
1644 left-multiplication operator's matrix will be symmetric because
1645 natural->EJA basis representation is an isometry and within the EJA
1646 the operator is self-adjoint by the Jordan axiom::
1647
1648 sage: set_random_seed()
1649 sage: x = ComplexHermitianEJA.random_instance().random_element()
1650 sage: x.operator().matrix().is_symmetric()
1651 True
1652
1653 We can construct the (trivial) algebra of rank zero::
1654
1655 sage: ComplexHermitianEJA(0)
1656 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
1657
1658 """
1659
1660 @classmethod
1661 def _denormalized_basis(cls, n, field):
1662 """
1663 Returns a basis for the space of complex Hermitian n-by-n matrices.
1664
1665 Why do we embed these? Basically, because all of numerical linear
1666 algebra assumes that you're working with vectors consisting of `n`
1667 entries from a field and scalars from the same field. There's no way
1668 to tell SageMath that (for example) the vectors contain complex
1669 numbers, while the scalar field is real.
1670
1671 SETUP::
1672
1673 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1674
1675 TESTS::
1676
1677 sage: set_random_seed()
1678 sage: n = ZZ.random_element(1,5)
1679 sage: field = QuadraticField(2, 'sqrt2')
1680 sage: B = ComplexHermitianEJA._denormalized_basis(n, field)
1681 sage: all( M.is_symmetric() for M in B)
1682 True
1683
1684 """
1685 R = PolynomialRing(field, 'z')
1686 z = R.gen()
1687 F = field.extension(z**2 + 1, 'I')
1688 I = F.gen()
1689
1690 # This is like the symmetric case, but we need to be careful:
1691 #
1692 # * We want conjugate-symmetry, not just symmetry.
1693 # * The diagonal will (as a result) be real.
1694 #
1695 S = []
1696 for i in range(n):
1697 for j in range(i+1):
1698 Eij = matrix(F, n, lambda k,l: k==i and l==j)
1699 if i == j:
1700 Sij = cls.real_embed(Eij)
1701 S.append(Sij)
1702 else:
1703 # The second one has a minus because it's conjugated.
1704 Sij_real = cls.real_embed(Eij + Eij.transpose())
1705 S.append(Sij_real)
1706 Sij_imag = cls.real_embed(I*Eij - I*Eij.transpose())
1707 S.append(Sij_imag)
1708
1709 # Since we embedded these, we can drop back to the "field" that we
1710 # started with instead of the complex extension "F".
1711 return ( s.change_ring(field) for s in S )
1712
1713
1714 def __init__(self, n, field=AA, **kwargs):
1715 basis = self._denormalized_basis(n,field)
1716 super(ComplexHermitianEJA,self).__init__(field,
1717 basis,
1718 check=False,
1719 **kwargs)
1720 self.rank.set_cache(n)
1721
1722
1723 class QuaternionMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
1724 @staticmethod
1725 def real_embed(M):
1726 """
1727 Embed the n-by-n quaternion matrix ``M`` into the space of real
1728 matrices of size 4n-by-4n by first sending each quaternion entry `z
1729 = a + bi + cj + dk` to the block-complex matrix ``[[a + bi,
1730 c+di],[-c + di, a-bi]]`, and then embedding those into a real
1731 matrix.
1732
1733 SETUP::
1734
1735 sage: from mjo.eja.eja_algebra import \
1736 ....: QuaternionMatrixEuclideanJordanAlgebra
1737
1738 EXAMPLES::
1739
1740 sage: Q = QuaternionAlgebra(QQ,-1,-1)
1741 sage: i,j,k = Q.gens()
1742 sage: x = 1 + 2*i + 3*j + 4*k
1743 sage: M = matrix(Q, 1, [[x]])
1744 sage: QuaternionMatrixEuclideanJordanAlgebra.real_embed(M)
1745 [ 1 2 3 4]
1746 [-2 1 -4 3]
1747 [-3 4 1 -2]
1748 [-4 -3 2 1]
1749
1750 Embedding is a homomorphism (isomorphism, in fact)::
1751
1752 sage: set_random_seed()
1753 sage: n_max = QuaternionMatrixEuclideanJordanAlgebra._max_test_case_size()
1754 sage: n = ZZ.random_element(n_max)
1755 sage: Q = QuaternionAlgebra(QQ,-1,-1)
1756 sage: X = random_matrix(Q, n)
1757 sage: Y = random_matrix(Q, n)
1758 sage: Xe = QuaternionMatrixEuclideanJordanAlgebra.real_embed(X)
1759 sage: Ye = QuaternionMatrixEuclideanJordanAlgebra.real_embed(Y)
1760 sage: XYe = QuaternionMatrixEuclideanJordanAlgebra.real_embed(X*Y)
1761 sage: Xe*Ye == XYe
1762 True
1763
1764 """
1765 quaternions = M.base_ring()
1766 n = M.nrows()
1767 if M.ncols() != n:
1768 raise ValueError("the matrix 'M' must be square")
1769
1770 F = QuadraticField(-1, 'I')
1771 i = F.gen()
1772
1773 blocks = []
1774 for z in M.list():
1775 t = z.coefficient_tuple()
1776 a = t[0]
1777 b = t[1]
1778 c = t[2]
1779 d = t[3]
1780 cplxM = matrix(F, 2, [[ a + b*i, c + d*i],
1781 [-c + d*i, a - b*i]])
1782 realM = ComplexMatrixEuclideanJordanAlgebra.real_embed(cplxM)
1783 blocks.append(realM)
1784
1785 # We should have real entries by now, so use the realest field
1786 # we've got for the return value.
1787 return matrix.block(quaternions.base_ring(), n, blocks)
1788
1789
1790
1791 @staticmethod
1792 def real_unembed(M):
1793 """
1794 The inverse of _embed_quaternion_matrix().
1795
1796 SETUP::
1797
1798 sage: from mjo.eja.eja_algebra import \
1799 ....: QuaternionMatrixEuclideanJordanAlgebra
1800
1801 EXAMPLES::
1802
1803 sage: M = matrix(QQ, [[ 1, 2, 3, 4],
1804 ....: [-2, 1, -4, 3],
1805 ....: [-3, 4, 1, -2],
1806 ....: [-4, -3, 2, 1]])
1807 sage: QuaternionMatrixEuclideanJordanAlgebra.real_unembed(M)
1808 [1 + 2*i + 3*j + 4*k]
1809
1810 TESTS:
1811
1812 Unembedding is the inverse of embedding::
1813
1814 sage: set_random_seed()
1815 sage: Q = QuaternionAlgebra(QQ, -1, -1)
1816 sage: M = random_matrix(Q, 3)
1817 sage: Me = QuaternionMatrixEuclideanJordanAlgebra.real_embed(M)
1818 sage: QuaternionMatrixEuclideanJordanAlgebra.real_unembed(Me) == M
1819 True
1820
1821 """
1822 n = ZZ(M.nrows())
1823 if M.ncols() != n:
1824 raise ValueError("the matrix 'M' must be square")
1825 if not n.mod(4).is_zero():
1826 raise ValueError("the matrix 'M' must be a quaternion embedding")
1827
1828 # Use the base ring of the matrix to ensure that its entries can be
1829 # multiplied by elements of the quaternion algebra.
1830 field = M.base_ring()
1831 Q = QuaternionAlgebra(field,-1,-1)
1832 i,j,k = Q.gens()
1833
1834 # Go top-left to bottom-right (reading order), converting every
1835 # 4-by-4 block we see to a 2-by-2 complex block, to a 1-by-1
1836 # quaternion block.
1837 elements = []
1838 for l in range(n/4):
1839 for m in range(n/4):
1840 submat = ComplexMatrixEuclideanJordanAlgebra.real_unembed(
1841 M[4*l:4*l+4,4*m:4*m+4] )
1842 if submat[0,0] != submat[1,1].conjugate():
1843 raise ValueError('bad on-diagonal submatrix')
1844 if submat[0,1] != -submat[1,0].conjugate():
1845 raise ValueError('bad off-diagonal submatrix')
1846 z = submat[0,0].real()
1847 z += submat[0,0].imag()*i
1848 z += submat[0,1].real()*j
1849 z += submat[0,1].imag()*k
1850 elements.append(z)
1851
1852 return matrix(Q, n/4, elements)
1853
1854
1855 @classmethod
1856 def natural_inner_product(cls,X,Y):
1857 """
1858 Compute a natural inner product in this algebra directly from
1859 its real embedding.
1860
1861 SETUP::
1862
1863 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
1864
1865 TESTS:
1866
1867 This gives the same answer as the slow, default method implemented
1868 in :class:`MatrixEuclideanJordanAlgebra`::
1869
1870 sage: set_random_seed()
1871 sage: J = QuaternionHermitianEJA.random_instance()
1872 sage: x,y = J.random_elements(2)
1873 sage: Xe = x.natural_representation()
1874 sage: Ye = y.natural_representation()
1875 sage: X = QuaternionHermitianEJA.real_unembed(Xe)
1876 sage: Y = QuaternionHermitianEJA.real_unembed(Ye)
1877 sage: expected = (X*Y).trace().coefficient_tuple()[0]
1878 sage: actual = QuaternionHermitianEJA.natural_inner_product(Xe,Ye)
1879 sage: actual == expected
1880 True
1881
1882 """
1883 return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/4
1884
1885
1886 class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra):
1887 """
1888 The rank-n simple EJA consisting of self-adjoint n-by-n quaternion
1889 matrices, the usual symmetric Jordan product, and the
1890 real-part-of-trace inner product. It has dimension `2n^2 - n` over
1891 the reals.
1892
1893 SETUP::
1894
1895 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
1896
1897 EXAMPLES:
1898
1899 In theory, our "field" can be any subfield of the reals::
1900
1901 sage: QuaternionHermitianEJA(2, RDF)
1902 Euclidean Jordan algebra of dimension 6 over Real Double Field
1903 sage: QuaternionHermitianEJA(2, RR)
1904 Euclidean Jordan algebra of dimension 6 over Real Field with
1905 53 bits of precision
1906
1907 TESTS:
1908
1909 The dimension of this algebra is `2*n^2 - n`::
1910
1911 sage: set_random_seed()
1912 sage: n_max = QuaternionHermitianEJA._max_test_case_size()
1913 sage: n = ZZ.random_element(1, n_max)
1914 sage: J = QuaternionHermitianEJA(n)
1915 sage: J.dimension() == 2*(n^2) - n
1916 True
1917
1918 The Jordan multiplication is what we think it is::
1919
1920 sage: set_random_seed()
1921 sage: J = QuaternionHermitianEJA.random_instance()
1922 sage: x,y = J.random_elements(2)
1923 sage: actual = (x*y).natural_representation()
1924 sage: X = x.natural_representation()
1925 sage: Y = y.natural_representation()
1926 sage: expected = (X*Y + Y*X)/2
1927 sage: actual == expected
1928 True
1929 sage: J(expected) == x*y
1930 True
1931
1932 We can change the generator prefix::
1933
1934 sage: QuaternionHermitianEJA(2, prefix='a').gens()
1935 (a0, a1, a2, a3, a4, a5)
1936
1937 Our natural basis is normalized with respect to the natural inner
1938 product unless we specify otherwise::
1939
1940 sage: set_random_seed()
1941 sage: J = QuaternionHermitianEJA.random_instance()
1942 sage: all( b.norm() == 1 for b in J.gens() )
1943 True
1944
1945 Since our natural basis is normalized with respect to the natural
1946 inner product, and since we know that this algebra is an EJA, any
1947 left-multiplication operator's matrix will be symmetric because
1948 natural->EJA basis representation is an isometry and within the EJA
1949 the operator is self-adjoint by the Jordan axiom::
1950
1951 sage: set_random_seed()
1952 sage: x = QuaternionHermitianEJA.random_instance().random_element()
1953 sage: x.operator().matrix().is_symmetric()
1954 True
1955
1956 We can construct the (trivial) algebra of rank zero::
1957
1958 sage: QuaternionHermitianEJA(0)
1959 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
1960
1961 """
1962 @classmethod
1963 def _denormalized_basis(cls, n, field):
1964 """
1965 Returns a basis for the space of quaternion Hermitian n-by-n matrices.
1966
1967 Why do we embed these? Basically, because all of numerical
1968 linear algebra assumes that you're working with vectors consisting
1969 of `n` entries from a field and scalars from the same field. There's
1970 no way to tell SageMath that (for example) the vectors contain
1971 complex numbers, while the scalar field is real.
1972
1973 SETUP::
1974
1975 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
1976
1977 TESTS::
1978
1979 sage: set_random_seed()
1980 sage: n = ZZ.random_element(1,5)
1981 sage: B = QuaternionHermitianEJA._denormalized_basis(n,QQ)
1982 sage: all( M.is_symmetric() for M in B )
1983 True
1984
1985 """
1986 Q = QuaternionAlgebra(QQ,-1,-1)
1987 I,J,K = Q.gens()
1988
1989 # This is like the symmetric case, but we need to be careful:
1990 #
1991 # * We want conjugate-symmetry, not just symmetry.
1992 # * The diagonal will (as a result) be real.
1993 #
1994 S = []
1995 for i in range(n):
1996 for j in range(i+1):
1997 Eij = matrix(Q, n, lambda k,l: k==i and l==j)
1998 if i == j:
1999 Sij = cls.real_embed(Eij)
2000 S.append(Sij)
2001 else:
2002 # The second, third, and fourth ones have a minus
2003 # because they're conjugated.
2004 Sij_real = cls.real_embed(Eij + Eij.transpose())
2005 S.append(Sij_real)
2006 Sij_I = cls.real_embed(I*Eij - I*Eij.transpose())
2007 S.append(Sij_I)
2008 Sij_J = cls.real_embed(J*Eij - J*Eij.transpose())
2009 S.append(Sij_J)
2010 Sij_K = cls.real_embed(K*Eij - K*Eij.transpose())
2011 S.append(Sij_K)
2012
2013 # Since we embedded these, we can drop back to the "field" that we
2014 # started with instead of the quaternion algebra "Q".
2015 return ( s.change_ring(field) for s in S )
2016
2017
2018 def __init__(self, n, field=AA, **kwargs):
2019 basis = self._denormalized_basis(n,field)
2020 super(QuaternionHermitianEJA,self).__init__(field,
2021 basis,
2022 check=False,
2023 **kwargs)
2024 self.rank.set_cache(n)
2025
2026
2027 class BilinearFormEJA(FiniteDimensionalEuclideanJordanAlgebra):
2028 r"""
2029 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
2030 with the half-trace inner product and jordan product ``x*y =
2031 (x0*y0 + <B*x_bar,y_bar>, x0*y_bar + y0*x_bar)`` where ``B`` is a
2032 symmetric positive-definite "bilinear form" matrix. It has
2033 dimension `n` over the reals, and reduces to the ``JordanSpinEJA``
2034 when ``B`` is the identity matrix of order ``n-1``.
2035
2036 SETUP::
2037
2038 sage: from mjo.eja.eja_algebra import (BilinearFormEJA,
2039 ....: JordanSpinEJA)
2040
2041 EXAMPLES:
2042
2043 When no bilinear form is specified, the identity matrix is used,
2044 and the resulting algebra is the Jordan spin algebra::
2045
2046 sage: J0 = BilinearFormEJA(3)
2047 sage: J1 = JordanSpinEJA(3)
2048 sage: J0.multiplication_table() == J0.multiplication_table()
2049 True
2050
2051 TESTS:
2052
2053 We can create a zero-dimensional algebra::
2054
2055 sage: J = BilinearFormEJA(0)
2056 sage: J.basis()
2057 Finite family {}
2058
2059 We can check the multiplication condition given in the Jordan, von
2060 Neumann, and Wigner paper (and also discussed on my "On the
2061 symmetry..." paper). Note that this relies heavily on the standard
2062 choice of basis, as does anything utilizing the bilinear form matrix::
2063
2064 sage: set_random_seed()
2065 sage: n = ZZ.random_element(5)
2066 sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular')
2067 sage: B = M.transpose()*M
2068 sage: J = BilinearFormEJA(n, B=B)
2069 sage: eis = VectorSpace(M.base_ring(), M.ncols()).basis()
2070 sage: V = J.vector_space()
2071 sage: sis = [ J.from_vector(V([0] + (M.inverse()*ei).list()))
2072 ....: for ei in eis ]
2073 sage: actual = [ sis[i]*sis[j]
2074 ....: for i in range(n-1)
2075 ....: for j in range(n-1) ]
2076 sage: expected = [ J.one() if i == j else J.zero()
2077 ....: for i in range(n-1)
2078 ....: for j in range(n-1) ]
2079 sage: actual == expected
2080 True
2081 """
2082 def __init__(self, n, field=AA, B=None, **kwargs):
2083 if B is None:
2084 self._B = matrix.identity(field, max(0,n-1))
2085 else:
2086 self._B = B
2087
2088 V = VectorSpace(field, n)
2089 mult_table = [[V.zero() for j in range(n)] for i in range(n)]
2090 for i in range(n):
2091 for j in range(n):
2092 x = V.gen(i)
2093 y = V.gen(j)
2094 x0 = x[0]
2095 xbar = x[1:]
2096 y0 = y[0]
2097 ybar = y[1:]
2098 z0 = x0*y0 + (self._B*xbar).inner_product(ybar)
2099 zbar = y0*xbar + x0*ybar
2100 z = V([z0] + zbar.list())
2101 mult_table[i][j] = z
2102
2103 # The rank of this algebra is two, unless we're in a
2104 # one-dimensional ambient space (because the rank is bounded
2105 # by the ambient dimension).
2106 super(BilinearFormEJA, self).__init__(field,
2107 mult_table,
2108 check=False,
2109 **kwargs)
2110 self.rank.set_cache(min(n,2))
2111
2112 def inner_product(self, x, y):
2113 r"""
2114 Half of the trace inner product.
2115
2116 This is defined so that the special case of the Jordan spin
2117 algebra gets the usual inner product.
2118
2119 SETUP::
2120
2121 sage: from mjo.eja.eja_algebra import BilinearFormEJA
2122
2123 TESTS:
2124
2125 Ensure that this is one-half of the trace inner-product when
2126 the algebra isn't just the reals (when ``n`` isn't one). This
2127 is in Faraut and Koranyi, and also my "On the symmetry..."
2128 paper::
2129
2130 sage: set_random_seed()
2131 sage: n = ZZ.random_element(2,5)
2132 sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular')
2133 sage: B = M.transpose()*M
2134 sage: J = BilinearFormEJA(n, B=B)
2135 sage: x = J.random_element()
2136 sage: y = J.random_element()
2137 sage: x.inner_product(y) == (x*y).trace()/2
2138 True
2139
2140 """
2141 xvec = x.to_vector()
2142 xbar = xvec[1:]
2143 yvec = y.to_vector()
2144 ybar = yvec[1:]
2145 return x[0]*y[0] + (self._B*xbar).inner_product(ybar)
2146
2147
2148 class JordanSpinEJA(BilinearFormEJA):
2149 """
2150 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
2151 with the usual inner product and jordan product ``x*y =
2152 (<x,y>, x0*y_bar + y0*x_bar)``. It has dimension `n` over
2153 the reals.
2154
2155 SETUP::
2156
2157 sage: from mjo.eja.eja_algebra import JordanSpinEJA
2158
2159 EXAMPLES:
2160
2161 This multiplication table can be verified by hand::
2162
2163 sage: J = JordanSpinEJA(4)
2164 sage: e0,e1,e2,e3 = J.gens()
2165 sage: e0*e0
2166 e0
2167 sage: e0*e1
2168 e1
2169 sage: e0*e2
2170 e2
2171 sage: e0*e3
2172 e3
2173 sage: e1*e2
2174 0
2175 sage: e1*e3
2176 0
2177 sage: e2*e3
2178 0
2179
2180 We can change the generator prefix::
2181
2182 sage: JordanSpinEJA(2, prefix='B').gens()
2183 (B0, B1)
2184
2185 TESTS:
2186
2187 Ensure that we have the usual inner product on `R^n`::
2188
2189 sage: set_random_seed()
2190 sage: J = JordanSpinEJA.random_instance()
2191 sage: x,y = J.random_elements(2)
2192 sage: X = x.natural_representation()
2193 sage: Y = y.natural_representation()
2194 sage: x.inner_product(y) == J.natural_inner_product(X,Y)
2195 True
2196
2197 """
2198 def __init__(self, n, field=AA, **kwargs):
2199 # This is a special case of the BilinearFormEJA with the identity
2200 # matrix as its bilinear form.
2201 super(JordanSpinEJA, self).__init__(n, field, **kwargs)
2202
2203
2204 class TrivialEJA(FiniteDimensionalEuclideanJordanAlgebra):
2205 """
2206 The trivial Euclidean Jordan algebra consisting of only a zero element.
2207
2208 SETUP::
2209
2210 sage: from mjo.eja.eja_algebra import TrivialEJA
2211
2212 EXAMPLES::
2213
2214 sage: J = TrivialEJA()
2215 sage: J.dimension()
2216 0
2217 sage: J.zero()
2218 0
2219 sage: J.one()
2220 0
2221 sage: 7*J.one()*12*J.one()
2222 0
2223 sage: J.one().inner_product(J.one())
2224 0
2225 sage: J.one().norm()
2226 0
2227 sage: J.one().subalgebra_generated_by()
2228 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2229 sage: J.rank()
2230 0
2231
2232 """
2233 def __init__(self, field=AA, **kwargs):
2234 mult_table = []
2235 super(TrivialEJA, self).__init__(field,
2236 mult_table,
2237 check=False,
2238 **kwargs)
2239 # The rank is zero using my definition, namely the dimension of the
2240 # largest subalgebra generated by any element.
2241 self.rank.set_cache(0)
2242
2243
2244 class DirectSumEJA(FiniteDimensionalEuclideanJordanAlgebra):
2245 r"""
2246 The external (orthogonal) direct sum of two other Euclidean Jordan
2247 algebras. Essentially the Cartesian product of its two factors.
2248 Every Euclidean Jordan algebra decomposes into an orthogonal
2249 direct sum of simple Euclidean Jordan algebras, so no generality
2250 is lost by providing only this construction.
2251
2252 SETUP::
2253
2254 sage: from mjo.eja.eja_algebra import (HadamardEJA,
2255 ....: RealSymmetricEJA,
2256 ....: DirectSumEJA)
2257
2258 EXAMPLES::
2259
2260 sage: J1 = HadamardEJA(2)
2261 sage: J2 = RealSymmetricEJA(3)
2262 sage: J = DirectSumEJA(J1,J2)
2263 sage: J.dimension()
2264 8
2265 sage: J.rank()
2266 5
2267
2268 """
2269 def __init__(self, J1, J2, field=AA, **kwargs):
2270 n1 = J1.dimension()
2271 n2 = J2.dimension()
2272 n = n1+n2
2273 V = VectorSpace(field, n)
2274 mult_table = [ [ V.zero() for j in range(n) ]
2275 for i in range(n) ]
2276 for i in range(n1):
2277 for j in range(n1):
2278 p = (J1.monomial(i)*J1.monomial(j)).to_vector()
2279 mult_table[i][j] = V(p.list() + [field.zero()]*n2)
2280
2281 for i in range(n2):
2282 for j in range(n2):
2283 p = (J2.monomial(i)*J2.monomial(j)).to_vector()
2284 mult_table[n1+i][n1+j] = V([field.zero()]*n1 + p.list())
2285
2286 super(DirectSumEJA, self).__init__(field,
2287 mult_table,
2288 check=False,
2289 **kwargs)
2290 self.rank.set_cache(J1.rank() + J2.rank())