]> gitweb.michael.orlitzky.com - sage.d.git/blob - mjo/eja/eja_algebra.py
eja: pass check=False for known-good constructions.
[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, gens)
749 if eigval == 0:
750 J0 = subalg
751 elif eigval == 1:
752 J1 = subalg
753 else:
754 raise ValueError("unexpected eigenvalue: %s" % eigval)
755
756 return (J0, J5, J1)
757
758
759 def random_element(self, thorough=False):
760 r"""
761 Return a random element of this algebra.
762
763 Our algebra superclass method only returns a linear
764 combination of at most two basis elements. We instead
765 want the vector space "random element" method that
766 returns a more diverse selection.
767
768 INPUT:
769
770 - ``thorough`` -- (boolean; default False) whether or not we
771 should generate irrational coefficients for the random
772 element when our base ring is irrational; this slows the
773 algebra operations to a crawl, but any truly random method
774 should include them
775
776 """
777 # For a general base ring... maybe we can trust this to do the
778 # right thing? Unlikely, but.
779 V = self.vector_space()
780 v = V.random_element()
781
782 if self.base_ring() is AA:
783 # The "random element" method of the algebraic reals is
784 # stupid at the moment, and only returns integers between
785 # -2 and 2, inclusive:
786 #
787 # https://trac.sagemath.org/ticket/30875
788 #
789 # Instead, we implement our own "random vector" method,
790 # and then coerce that into the algebra. We use the vector
791 # space degree here instead of the dimension because a
792 # subalgebra could (for example) be spanned by only two
793 # vectors, each with five coordinates. We need to
794 # generate all five coordinates.
795 if thorough:
796 v *= QQbar.random_element().real()
797 else:
798 v *= QQ.random_element()
799
800 return self.from_vector(V.coordinate_vector(v))
801
802 def random_elements(self, count, thorough=False):
803 """
804 Return ``count`` random elements as a tuple.
805
806 INPUT:
807
808 - ``thorough`` -- (boolean; default False) whether or not we
809 should generate irrational coefficients for the random
810 elements when our base ring is irrational; this slows the
811 algebra operations to a crawl, but any truly random method
812 should include them
813
814 SETUP::
815
816 sage: from mjo.eja.eja_algebra import JordanSpinEJA
817
818 EXAMPLES::
819
820 sage: J = JordanSpinEJA(3)
821 sage: x,y,z = J.random_elements(3)
822 sage: all( [ x in J, y in J, z in J ])
823 True
824 sage: len( J.random_elements(10) ) == 10
825 True
826
827 """
828 return tuple( self.random_element(thorough)
829 for idx in range(count) )
830
831 @classmethod
832 def random_instance(cls, field=AA, **kwargs):
833 """
834 Return a random instance of this type of algebra.
835
836 Beware, this will crash for "most instances" because the
837 constructor below looks wrong.
838 """
839 if cls is TrivialEJA:
840 # The TrivialEJA class doesn't take an "n" argument because
841 # there's only one.
842 return cls(field)
843
844 n = ZZ.random_element(cls._max_test_case_size() + 1)
845 return cls(n, field, **kwargs)
846
847 @cached_method
848 def _charpoly_coefficients(self):
849 r"""
850 The `r` polynomial coefficients of the "characteristic polynomial
851 of" function.
852 """
853 n = self.dimension()
854 var_names = [ "X" + str(z) for z in range(1,n+1) ]
855 R = PolynomialRing(self.base_ring(), var_names)
856 vars = R.gens()
857 F = R.fraction_field()
858
859 def L_x_i_j(i,j):
860 # From a result in my book, these are the entries of the
861 # basis representation of L_x.
862 return sum( vars[k]*self.monomial(k).operator().matrix()[i,j]
863 for k in range(n) )
864
865 L_x = matrix(F, n, n, L_x_i_j)
866
867 r = None
868 if self.rank.is_in_cache():
869 r = self.rank()
870 # There's no need to pad the system with redundant
871 # columns if we *know* they'll be redundant.
872 n = r
873
874 # Compute an extra power in case the rank is equal to
875 # the dimension (otherwise, we would stop at x^(r-1)).
876 x_powers = [ (L_x**k)*self.one().to_vector()
877 for k in range(n+1) ]
878 A = matrix.column(F, x_powers[:n])
879 AE = A.extended_echelon_form()
880 E = AE[:,n:]
881 A_rref = AE[:,:n]
882 if r is None:
883 r = A_rref.rank()
884 b = x_powers[r]
885
886 # The theory says that only the first "r" coefficients are
887 # nonzero, and they actually live in the original polynomial
888 # ring and not the fraction field. We negate them because
889 # in the actual characteristic polynomial, they get moved
890 # to the other side where x^r lives.
891 return -A_rref.solve_right(E*b).change_ring(R)[:r]
892
893 @cached_method
894 def rank(self):
895 r"""
896 Return the rank of this EJA.
897
898 This is a cached method because we know the rank a priori for
899 all of the algebras we can construct. Thus we can avoid the
900 expensive ``_charpoly_coefficients()`` call unless we truly
901 need to compute the whole characteristic polynomial.
902
903 SETUP::
904
905 sage: from mjo.eja.eja_algebra import (HadamardEJA,
906 ....: JordanSpinEJA,
907 ....: RealSymmetricEJA,
908 ....: ComplexHermitianEJA,
909 ....: QuaternionHermitianEJA,
910 ....: random_eja)
911
912 EXAMPLES:
913
914 The rank of the Jordan spin algebra is always two::
915
916 sage: JordanSpinEJA(2).rank()
917 2
918 sage: JordanSpinEJA(3).rank()
919 2
920 sage: JordanSpinEJA(4).rank()
921 2
922
923 The rank of the `n`-by-`n` Hermitian real, complex, or
924 quaternion matrices is `n`::
925
926 sage: RealSymmetricEJA(4).rank()
927 4
928 sage: ComplexHermitianEJA(3).rank()
929 3
930 sage: QuaternionHermitianEJA(2).rank()
931 2
932
933 TESTS:
934
935 Ensure that every EJA that we know how to construct has a
936 positive integer rank, unless the algebra is trivial in
937 which case its rank will be zero::
938
939 sage: set_random_seed()
940 sage: J = random_eja()
941 sage: r = J.rank()
942 sage: r in ZZ
943 True
944 sage: r > 0 or (r == 0 and J.is_trivial())
945 True
946
947 Ensure that computing the rank actually works, since the ranks
948 of all simple algebras are known and will be cached by default::
949
950 sage: J = HadamardEJA(4)
951 sage: J.rank.clear_cache()
952 sage: J.rank()
953 4
954
955 ::
956
957 sage: J = JordanSpinEJA(4)
958 sage: J.rank.clear_cache()
959 sage: J.rank()
960 2
961
962 ::
963
964 sage: J = RealSymmetricEJA(3)
965 sage: J.rank.clear_cache()
966 sage: J.rank()
967 3
968
969 ::
970
971 sage: J = ComplexHermitianEJA(2)
972 sage: J.rank.clear_cache()
973 sage: J.rank()
974 2
975
976 ::
977
978 sage: J = QuaternionHermitianEJA(2)
979 sage: J.rank.clear_cache()
980 sage: J.rank()
981 2
982 """
983 return len(self._charpoly_coefficients())
984
985
986 def vector_space(self):
987 """
988 Return the vector space that underlies this algebra.
989
990 SETUP::
991
992 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
993
994 EXAMPLES::
995
996 sage: J = RealSymmetricEJA(2)
997 sage: J.vector_space()
998 Vector space of dimension 3 over...
999
1000 """
1001 return self.zero().to_vector().parent().ambient_vector_space()
1002
1003
1004 Element = FiniteDimensionalEuclideanJordanAlgebraElement
1005
1006
1007 class HadamardEJA(FiniteDimensionalEuclideanJordanAlgebra):
1008 """
1009 Return the Euclidean Jordan Algebra corresponding to the set
1010 `R^n` under the Hadamard product.
1011
1012 Note: this is nothing more than the Cartesian product of ``n``
1013 copies of the spin algebra. Once Cartesian product algebras
1014 are implemented, this can go.
1015
1016 SETUP::
1017
1018 sage: from mjo.eja.eja_algebra import HadamardEJA
1019
1020 EXAMPLES:
1021
1022 This multiplication table can be verified by hand::
1023
1024 sage: J = HadamardEJA(3)
1025 sage: e0,e1,e2 = J.gens()
1026 sage: e0*e0
1027 e0
1028 sage: e0*e1
1029 0
1030 sage: e0*e2
1031 0
1032 sage: e1*e1
1033 e1
1034 sage: e1*e2
1035 0
1036 sage: e2*e2
1037 e2
1038
1039 TESTS:
1040
1041 We can change the generator prefix::
1042
1043 sage: HadamardEJA(3, prefix='r').gens()
1044 (r0, r1, r2)
1045
1046 """
1047 def __init__(self, n, field=AA, **kwargs):
1048 V = VectorSpace(field, n)
1049 mult_table = [ [ V.gen(i)*(i == j) for j in range(n) ]
1050 for i in range(n) ]
1051
1052 super(HadamardEJA, self).__init__(field,
1053 mult_table,
1054 check=False,
1055 **kwargs)
1056 self.rank.set_cache(n)
1057
1058 def inner_product(self, x, y):
1059 """
1060 Faster to reimplement than to use natural representations.
1061
1062 SETUP::
1063
1064 sage: from mjo.eja.eja_algebra import HadamardEJA
1065
1066 TESTS:
1067
1068 Ensure that this is the usual inner product for the algebras
1069 over `R^n`::
1070
1071 sage: set_random_seed()
1072 sage: J = HadamardEJA.random_instance()
1073 sage: x,y = J.random_elements(2)
1074 sage: X = x.natural_representation()
1075 sage: Y = y.natural_representation()
1076 sage: x.inner_product(y) == J.natural_inner_product(X,Y)
1077 True
1078
1079 """
1080 return x.to_vector().inner_product(y.to_vector())
1081
1082
1083 def random_eja(field=AA):
1084 """
1085 Return a "random" finite-dimensional Euclidean Jordan Algebra.
1086
1087 SETUP::
1088
1089 sage: from mjo.eja.eja_algebra import random_eja
1090
1091 TESTS::
1092
1093 sage: random_eja()
1094 Euclidean Jordan algebra of dimension...
1095
1096 """
1097 classname = choice([TrivialEJA,
1098 HadamardEJA,
1099 JordanSpinEJA,
1100 RealSymmetricEJA,
1101 ComplexHermitianEJA,
1102 QuaternionHermitianEJA])
1103 return classname.random_instance(field=field)
1104
1105
1106
1107
1108 class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
1109 @staticmethod
1110 def _max_test_case_size():
1111 # Play it safe, since this will be squared and the underlying
1112 # field can have dimension 4 (quaternions) too.
1113 return 2
1114
1115 def __init__(self, field, basis, normalize_basis=True, **kwargs):
1116 """
1117 Compared to the superclass constructor, we take a basis instead of
1118 a multiplication table because the latter can be computed in terms
1119 of the former when the product is known (like it is here).
1120 """
1121 # Used in this class's fast _charpoly_coefficients() override.
1122 self._basis_normalizers = None
1123
1124 # We're going to loop through this a few times, so now's a good
1125 # time to ensure that it isn't a generator expression.
1126 basis = tuple(basis)
1127
1128 if len(basis) > 1 and normalize_basis:
1129 # We'll need sqrt(2) to normalize the basis, and this
1130 # winds up in the multiplication table, so the whole
1131 # algebra needs to be over the field extension.
1132 R = PolynomialRing(field, 'z')
1133 z = R.gen()
1134 p = z**2 - 2
1135 if p.is_irreducible():
1136 field = field.extension(p, 'sqrt2', embedding=RLF(2).sqrt())
1137 basis = tuple( s.change_ring(field) for s in basis )
1138 self._basis_normalizers = tuple(
1139 ~(self.natural_inner_product(s,s).sqrt()) for s in basis )
1140 basis = tuple(s*c for (s,c) in zip(basis,self._basis_normalizers))
1141
1142 Qs = self.multiplication_table_from_matrix_basis(basis)
1143
1144 super(MatrixEuclideanJordanAlgebra, self).__init__(field,
1145 Qs,
1146 natural_basis=basis,
1147 **kwargs)
1148
1149
1150 @cached_method
1151 def _charpoly_coefficients(self):
1152 r"""
1153 Override the parent method with something that tries to compute
1154 over a faster (non-extension) field.
1155 """
1156 if self._basis_normalizers is None:
1157 # We didn't normalize, so assume that the basis we started
1158 # with had entries in a nice field.
1159 return super(MatrixEuclideanJordanAlgebra, self)._charpoly_coefficients()
1160 else:
1161 basis = ( (b/n) for (b,n) in zip(self.natural_basis(),
1162 self._basis_normalizers) )
1163
1164 # Do this over the rationals and convert back at the end.
1165 # Only works because we know the entries of the basis are
1166 # integers. The argument ``check=False`` is required
1167 # because the trace inner-product method for this
1168 # class is a stub and can't actually be checked.
1169 J = MatrixEuclideanJordanAlgebra(QQ,
1170 basis,
1171 normalize_basis=False,
1172 check=False)
1173 a = J._charpoly_coefficients()
1174
1175 # Unfortunately, changing the basis does change the
1176 # coefficients of the characteristic polynomial, but since
1177 # these are really the coefficients of the "characteristic
1178 # polynomial of" function, everything is still nice and
1179 # unevaluated. It's therefore "obvious" how scaling the
1180 # basis affects the coordinate variables X1, X2, et
1181 # cetera. Scaling the first basis vector up by "n" adds a
1182 # factor of 1/n into every "X1" term, for example. So here
1183 # we simply undo the basis_normalizer scaling that we
1184 # performed earlier.
1185 #
1186 # The a[0] access here is safe because trivial algebras
1187 # won't have any basis normalizers and therefore won't
1188 # make it to this "else" branch.
1189 XS = a[0].parent().gens()
1190 subs_dict = { XS[i]: self._basis_normalizers[i]*XS[i]
1191 for i in range(len(XS)) }
1192 return tuple( a_i.subs(subs_dict) for a_i in a )
1193
1194
1195 @staticmethod
1196 def multiplication_table_from_matrix_basis(basis):
1197 """
1198 At least three of the five simple Euclidean Jordan algebras have the
1199 symmetric multiplication (A,B) |-> (AB + BA)/2, where the
1200 multiplication on the right is matrix multiplication. Given a basis
1201 for the underlying matrix space, this function returns a
1202 multiplication table (obtained by looping through the basis
1203 elements) for an algebra of those matrices.
1204 """
1205 # In S^2, for example, we nominally have four coordinates even
1206 # though the space is of dimension three only. The vector space V
1207 # is supposed to hold the entire long vector, and the subspace W
1208 # of V will be spanned by the vectors that arise from symmetric
1209 # matrices. Thus for S^2, dim(V) == 4 and dim(W) == 3.
1210 if len(basis) == 0:
1211 return []
1212
1213 field = basis[0].base_ring()
1214 dimension = basis[0].nrows()
1215
1216 V = VectorSpace(field, dimension**2)
1217 W = V.span_of_basis( _mat2vec(s) for s in basis )
1218 n = len(basis)
1219 mult_table = [[W.zero() for j in range(n)] for i in range(n)]
1220 for i in range(n):
1221 for j in range(n):
1222 mat_entry = (basis[i]*basis[j] + basis[j]*basis[i])/2
1223 mult_table[i][j] = W.coordinate_vector(_mat2vec(mat_entry))
1224
1225 return mult_table
1226
1227
1228 @staticmethod
1229 def real_embed(M):
1230 """
1231 Embed the matrix ``M`` into a space of real matrices.
1232
1233 The matrix ``M`` can have entries in any field at the moment:
1234 the real numbers, complex numbers, or quaternions. And although
1235 they are not a field, we can probably support octonions at some
1236 point, too. This function returns a real matrix that "acts like"
1237 the original with respect to matrix multiplication; i.e.
1238
1239 real_embed(M*N) = real_embed(M)*real_embed(N)
1240
1241 """
1242 raise NotImplementedError
1243
1244
1245 @staticmethod
1246 def real_unembed(M):
1247 """
1248 The inverse of :meth:`real_embed`.
1249 """
1250 raise NotImplementedError
1251
1252
1253 @classmethod
1254 def natural_inner_product(cls,X,Y):
1255 Xu = cls.real_unembed(X)
1256 Yu = cls.real_unembed(Y)
1257 tr = (Xu*Yu).trace()
1258
1259 if tr in RLF:
1260 # It's real already.
1261 return tr
1262
1263 # Otherwise, try the thing that works for complex numbers; and
1264 # if that doesn't work, the thing that works for quaternions.
1265 try:
1266 return tr.vector()[0] # real part, imag part is index 1
1267 except AttributeError:
1268 # A quaternions doesn't have a vector() method, but does
1269 # have coefficient_tuple() method that returns the
1270 # coefficients of 1, i, j, and k -- in that order.
1271 return tr.coefficient_tuple()[0]
1272
1273
1274 class RealMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
1275 @staticmethod
1276 def real_embed(M):
1277 """
1278 The identity function, for embedding real matrices into real
1279 matrices.
1280 """
1281 return M
1282
1283 @staticmethod
1284 def real_unembed(M):
1285 """
1286 The identity function, for unembedding real matrices from real
1287 matrices.
1288 """
1289 return M
1290
1291
1292 class RealSymmetricEJA(RealMatrixEuclideanJordanAlgebra):
1293 """
1294 The rank-n simple EJA consisting of real symmetric n-by-n
1295 matrices, the usual symmetric Jordan product, and the trace inner
1296 product. It has dimension `(n^2 + n)/2` over the reals.
1297
1298 SETUP::
1299
1300 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1301
1302 EXAMPLES::
1303
1304 sage: J = RealSymmetricEJA(2)
1305 sage: e0, e1, e2 = J.gens()
1306 sage: e0*e0
1307 e0
1308 sage: e1*e1
1309 1/2*e0 + 1/2*e2
1310 sage: e2*e2
1311 e2
1312
1313 In theory, our "field" can be any subfield of the reals::
1314
1315 sage: RealSymmetricEJA(2, RDF)
1316 Euclidean Jordan algebra of dimension 3 over Real Double Field
1317 sage: RealSymmetricEJA(2, RR)
1318 Euclidean Jordan algebra of dimension 3 over Real Field with
1319 53 bits of precision
1320
1321 TESTS:
1322
1323 The dimension of this algebra is `(n^2 + n) / 2`::
1324
1325 sage: set_random_seed()
1326 sage: n_max = RealSymmetricEJA._max_test_case_size()
1327 sage: n = ZZ.random_element(1, n_max)
1328 sage: J = RealSymmetricEJA(n)
1329 sage: J.dimension() == (n^2 + n)/2
1330 True
1331
1332 The Jordan multiplication is what we think it is::
1333
1334 sage: set_random_seed()
1335 sage: J = RealSymmetricEJA.random_instance()
1336 sage: x,y = J.random_elements(2)
1337 sage: actual = (x*y).natural_representation()
1338 sage: X = x.natural_representation()
1339 sage: Y = y.natural_representation()
1340 sage: expected = (X*Y + Y*X)/2
1341 sage: actual == expected
1342 True
1343 sage: J(expected) == x*y
1344 True
1345
1346 We can change the generator prefix::
1347
1348 sage: RealSymmetricEJA(3, prefix='q').gens()
1349 (q0, q1, q2, q3, q4, q5)
1350
1351 Our natural basis is normalized with respect to the natural inner
1352 product unless we specify otherwise::
1353
1354 sage: set_random_seed()
1355 sage: J = RealSymmetricEJA.random_instance()
1356 sage: all( b.norm() == 1 for b in J.gens() )
1357 True
1358
1359 Since our natural basis is normalized with respect to the natural
1360 inner product, and since we know that this algebra is an EJA, any
1361 left-multiplication operator's matrix will be symmetric because
1362 natural->EJA basis representation is an isometry and within the EJA
1363 the operator is self-adjoint by the Jordan axiom::
1364
1365 sage: set_random_seed()
1366 sage: x = RealSymmetricEJA.random_instance().random_element()
1367 sage: x.operator().matrix().is_symmetric()
1368 True
1369
1370 We can construct the (trivial) algebra of rank zero::
1371
1372 sage: RealSymmetricEJA(0)
1373 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
1374
1375 """
1376 @classmethod
1377 def _denormalized_basis(cls, n, field):
1378 """
1379 Return a basis for the space of real symmetric n-by-n matrices.
1380
1381 SETUP::
1382
1383 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1384
1385 TESTS::
1386
1387 sage: set_random_seed()
1388 sage: n = ZZ.random_element(1,5)
1389 sage: B = RealSymmetricEJA._denormalized_basis(n,QQ)
1390 sage: all( M.is_symmetric() for M in B)
1391 True
1392
1393 """
1394 # The basis of symmetric matrices, as matrices, in their R^(n-by-n)
1395 # coordinates.
1396 S = []
1397 for i in range(n):
1398 for j in range(i+1):
1399 Eij = matrix(field, n, lambda k,l: k==i and l==j)
1400 if i == j:
1401 Sij = Eij
1402 else:
1403 Sij = Eij + Eij.transpose()
1404 S.append(Sij)
1405 return S
1406
1407
1408 @staticmethod
1409 def _max_test_case_size():
1410 return 4 # Dimension 10
1411
1412
1413 def __init__(self, n, field=AA, **kwargs):
1414 basis = self._denormalized_basis(n, field)
1415 super(RealSymmetricEJA, self).__init__(field,
1416 basis,
1417 check=False,
1418 **kwargs)
1419 self.rank.set_cache(n)
1420
1421
1422 class ComplexMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
1423 @staticmethod
1424 def real_embed(M):
1425 """
1426 Embed the n-by-n complex matrix ``M`` into the space of real
1427 matrices of size 2n-by-2n via the map the sends each entry `z = a +
1428 bi` to the block matrix ``[[a,b],[-b,a]]``.
1429
1430 SETUP::
1431
1432 sage: from mjo.eja.eja_algebra import \
1433 ....: ComplexMatrixEuclideanJordanAlgebra
1434
1435 EXAMPLES::
1436
1437 sage: F = QuadraticField(-1, 'I')
1438 sage: x1 = F(4 - 2*i)
1439 sage: x2 = F(1 + 2*i)
1440 sage: x3 = F(-i)
1441 sage: x4 = F(6)
1442 sage: M = matrix(F,2,[[x1,x2],[x3,x4]])
1443 sage: ComplexMatrixEuclideanJordanAlgebra.real_embed(M)
1444 [ 4 -2| 1 2]
1445 [ 2 4|-2 1]
1446 [-----+-----]
1447 [ 0 -1| 6 0]
1448 [ 1 0| 0 6]
1449
1450 TESTS:
1451
1452 Embedding is a homomorphism (isomorphism, in fact)::
1453
1454 sage: set_random_seed()
1455 sage: n_max = ComplexMatrixEuclideanJordanAlgebra._max_test_case_size()
1456 sage: n = ZZ.random_element(n_max)
1457 sage: F = QuadraticField(-1, 'I')
1458 sage: X = random_matrix(F, n)
1459 sage: Y = random_matrix(F, n)
1460 sage: Xe = ComplexMatrixEuclideanJordanAlgebra.real_embed(X)
1461 sage: Ye = ComplexMatrixEuclideanJordanAlgebra.real_embed(Y)
1462 sage: XYe = ComplexMatrixEuclideanJordanAlgebra.real_embed(X*Y)
1463 sage: Xe*Ye == XYe
1464 True
1465
1466 """
1467 n = M.nrows()
1468 if M.ncols() != n:
1469 raise ValueError("the matrix 'M' must be square")
1470
1471 # We don't need any adjoined elements...
1472 field = M.base_ring().base_ring()
1473
1474 blocks = []
1475 for z in M.list():
1476 a = z.list()[0] # real part, I guess
1477 b = z.list()[1] # imag part, I guess
1478 blocks.append(matrix(field, 2, [[a,b],[-b,a]]))
1479
1480 return matrix.block(field, n, blocks)
1481
1482
1483 @staticmethod
1484 def real_unembed(M):
1485 """
1486 The inverse of _embed_complex_matrix().
1487
1488 SETUP::
1489
1490 sage: from mjo.eja.eja_algebra import \
1491 ....: ComplexMatrixEuclideanJordanAlgebra
1492
1493 EXAMPLES::
1494
1495 sage: A = matrix(QQ,[ [ 1, 2, 3, 4],
1496 ....: [-2, 1, -4, 3],
1497 ....: [ 9, 10, 11, 12],
1498 ....: [-10, 9, -12, 11] ])
1499 sage: ComplexMatrixEuclideanJordanAlgebra.real_unembed(A)
1500 [ 2*I + 1 4*I + 3]
1501 [ 10*I + 9 12*I + 11]
1502
1503 TESTS:
1504
1505 Unembedding is the inverse of embedding::
1506
1507 sage: set_random_seed()
1508 sage: F = QuadraticField(-1, 'I')
1509 sage: M = random_matrix(F, 3)
1510 sage: Me = ComplexMatrixEuclideanJordanAlgebra.real_embed(M)
1511 sage: ComplexMatrixEuclideanJordanAlgebra.real_unembed(Me) == M
1512 True
1513
1514 """
1515 n = ZZ(M.nrows())
1516 if M.ncols() != n:
1517 raise ValueError("the matrix 'M' must be square")
1518 if not n.mod(2).is_zero():
1519 raise ValueError("the matrix 'M' must be a complex embedding")
1520
1521 # If "M" was normalized, its base ring might have roots
1522 # adjoined and they can stick around after unembedding.
1523 field = M.base_ring()
1524 R = PolynomialRing(field, 'z')
1525 z = R.gen()
1526 if field is AA:
1527 # Sage doesn't know how to embed AA into QQbar, i.e. how
1528 # to adjoin sqrt(-1) to AA.
1529 F = QQbar
1530 else:
1531 F = field.extension(z**2 + 1, 'I', embedding=CLF(-1).sqrt())
1532 i = F.gen()
1533
1534 # Go top-left to bottom-right (reading order), converting every
1535 # 2-by-2 block we see to a single complex element.
1536 elements = []
1537 for k in range(n/2):
1538 for j in range(n/2):
1539 submat = M[2*k:2*k+2,2*j:2*j+2]
1540 if submat[0,0] != submat[1,1]:
1541 raise ValueError('bad on-diagonal submatrix')
1542 if submat[0,1] != -submat[1,0]:
1543 raise ValueError('bad off-diagonal submatrix')
1544 z = submat[0,0] + submat[0,1]*i
1545 elements.append(z)
1546
1547 return matrix(F, n/2, elements)
1548
1549
1550 @classmethod
1551 def natural_inner_product(cls,X,Y):
1552 """
1553 Compute a natural inner product in this algebra directly from
1554 its real embedding.
1555
1556 SETUP::
1557
1558 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1559
1560 TESTS:
1561
1562 This gives the same answer as the slow, default method implemented
1563 in :class:`MatrixEuclideanJordanAlgebra`::
1564
1565 sage: set_random_seed()
1566 sage: J = ComplexHermitianEJA.random_instance()
1567 sage: x,y = J.random_elements(2)
1568 sage: Xe = x.natural_representation()
1569 sage: Ye = y.natural_representation()
1570 sage: X = ComplexHermitianEJA.real_unembed(Xe)
1571 sage: Y = ComplexHermitianEJA.real_unembed(Ye)
1572 sage: expected = (X*Y).trace().real()
1573 sage: actual = ComplexHermitianEJA.natural_inner_product(Xe,Ye)
1574 sage: actual == expected
1575 True
1576
1577 """
1578 return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/2
1579
1580
1581 class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra):
1582 """
1583 The rank-n simple EJA consisting of complex Hermitian n-by-n
1584 matrices over the real numbers, the usual symmetric Jordan product,
1585 and the real-part-of-trace inner product. It has dimension `n^2` over
1586 the reals.
1587
1588 SETUP::
1589
1590 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1591
1592 EXAMPLES:
1593
1594 In theory, our "field" can be any subfield of the reals::
1595
1596 sage: ComplexHermitianEJA(2, RDF)
1597 Euclidean Jordan algebra of dimension 4 over Real Double Field
1598 sage: ComplexHermitianEJA(2, RR)
1599 Euclidean Jordan algebra of dimension 4 over Real Field with
1600 53 bits of precision
1601
1602 TESTS:
1603
1604 The dimension of this algebra is `n^2`::
1605
1606 sage: set_random_seed()
1607 sage: n_max = ComplexHermitianEJA._max_test_case_size()
1608 sage: n = ZZ.random_element(1, n_max)
1609 sage: J = ComplexHermitianEJA(n)
1610 sage: J.dimension() == n^2
1611 True
1612
1613 The Jordan multiplication is what we think it is::
1614
1615 sage: set_random_seed()
1616 sage: J = ComplexHermitianEJA.random_instance()
1617 sage: x,y = J.random_elements(2)
1618 sage: actual = (x*y).natural_representation()
1619 sage: X = x.natural_representation()
1620 sage: Y = y.natural_representation()
1621 sage: expected = (X*Y + Y*X)/2
1622 sage: actual == expected
1623 True
1624 sage: J(expected) == x*y
1625 True
1626
1627 We can change the generator prefix::
1628
1629 sage: ComplexHermitianEJA(2, prefix='z').gens()
1630 (z0, z1, z2, z3)
1631
1632 Our natural basis is normalized with respect to the natural inner
1633 product unless we specify otherwise::
1634
1635 sage: set_random_seed()
1636 sage: J = ComplexHermitianEJA.random_instance()
1637 sage: all( b.norm() == 1 for b in J.gens() )
1638 True
1639
1640 Since our natural basis is normalized with respect to the natural
1641 inner product, and since we know that this algebra is an EJA, any
1642 left-multiplication operator's matrix will be symmetric because
1643 natural->EJA basis representation is an isometry and within the EJA
1644 the operator is self-adjoint by the Jordan axiom::
1645
1646 sage: set_random_seed()
1647 sage: x = ComplexHermitianEJA.random_instance().random_element()
1648 sage: x.operator().matrix().is_symmetric()
1649 True
1650
1651 We can construct the (trivial) algebra of rank zero::
1652
1653 sage: ComplexHermitianEJA(0)
1654 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
1655
1656 """
1657
1658 @classmethod
1659 def _denormalized_basis(cls, n, field):
1660 """
1661 Returns a basis for the space of complex Hermitian n-by-n matrices.
1662
1663 Why do we embed these? Basically, because all of numerical linear
1664 algebra assumes that you're working with vectors consisting of `n`
1665 entries from a field and scalars from the same field. There's no way
1666 to tell SageMath that (for example) the vectors contain complex
1667 numbers, while the scalar field is real.
1668
1669 SETUP::
1670
1671 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1672
1673 TESTS::
1674
1675 sage: set_random_seed()
1676 sage: n = ZZ.random_element(1,5)
1677 sage: field = QuadraticField(2, 'sqrt2')
1678 sage: B = ComplexHermitianEJA._denormalized_basis(n, field)
1679 sage: all( M.is_symmetric() for M in B)
1680 True
1681
1682 """
1683 R = PolynomialRing(field, 'z')
1684 z = R.gen()
1685 F = field.extension(z**2 + 1, 'I')
1686 I = F.gen()
1687
1688 # This is like the symmetric case, but we need to be careful:
1689 #
1690 # * We want conjugate-symmetry, not just symmetry.
1691 # * The diagonal will (as a result) be real.
1692 #
1693 S = []
1694 for i in range(n):
1695 for j in range(i+1):
1696 Eij = matrix(F, n, lambda k,l: k==i and l==j)
1697 if i == j:
1698 Sij = cls.real_embed(Eij)
1699 S.append(Sij)
1700 else:
1701 # The second one has a minus because it's conjugated.
1702 Sij_real = cls.real_embed(Eij + Eij.transpose())
1703 S.append(Sij_real)
1704 Sij_imag = cls.real_embed(I*Eij - I*Eij.transpose())
1705 S.append(Sij_imag)
1706
1707 # Since we embedded these, we can drop back to the "field" that we
1708 # started with instead of the complex extension "F".
1709 return ( s.change_ring(field) for s in S )
1710
1711
1712 def __init__(self, n, field=AA, **kwargs):
1713 basis = self._denormalized_basis(n,field)
1714 super(ComplexHermitianEJA,self).__init__(field,
1715 basis,
1716 check=False,
1717 **kwargs)
1718 self.rank.set_cache(n)
1719
1720
1721 class QuaternionMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
1722 @staticmethod
1723 def real_embed(M):
1724 """
1725 Embed the n-by-n quaternion matrix ``M`` into the space of real
1726 matrices of size 4n-by-4n by first sending each quaternion entry `z
1727 = a + bi + cj + dk` to the block-complex matrix ``[[a + bi,
1728 c+di],[-c + di, a-bi]]`, and then embedding those into a real
1729 matrix.
1730
1731 SETUP::
1732
1733 sage: from mjo.eja.eja_algebra import \
1734 ....: QuaternionMatrixEuclideanJordanAlgebra
1735
1736 EXAMPLES::
1737
1738 sage: Q = QuaternionAlgebra(QQ,-1,-1)
1739 sage: i,j,k = Q.gens()
1740 sage: x = 1 + 2*i + 3*j + 4*k
1741 sage: M = matrix(Q, 1, [[x]])
1742 sage: QuaternionMatrixEuclideanJordanAlgebra.real_embed(M)
1743 [ 1 2 3 4]
1744 [-2 1 -4 3]
1745 [-3 4 1 -2]
1746 [-4 -3 2 1]
1747
1748 Embedding is a homomorphism (isomorphism, in fact)::
1749
1750 sage: set_random_seed()
1751 sage: n_max = QuaternionMatrixEuclideanJordanAlgebra._max_test_case_size()
1752 sage: n = ZZ.random_element(n_max)
1753 sage: Q = QuaternionAlgebra(QQ,-1,-1)
1754 sage: X = random_matrix(Q, n)
1755 sage: Y = random_matrix(Q, n)
1756 sage: Xe = QuaternionMatrixEuclideanJordanAlgebra.real_embed(X)
1757 sage: Ye = QuaternionMatrixEuclideanJordanAlgebra.real_embed(Y)
1758 sage: XYe = QuaternionMatrixEuclideanJordanAlgebra.real_embed(X*Y)
1759 sage: Xe*Ye == XYe
1760 True
1761
1762 """
1763 quaternions = M.base_ring()
1764 n = M.nrows()
1765 if M.ncols() != n:
1766 raise ValueError("the matrix 'M' must be square")
1767
1768 F = QuadraticField(-1, 'I')
1769 i = F.gen()
1770
1771 blocks = []
1772 for z in M.list():
1773 t = z.coefficient_tuple()
1774 a = t[0]
1775 b = t[1]
1776 c = t[2]
1777 d = t[3]
1778 cplxM = matrix(F, 2, [[ a + b*i, c + d*i],
1779 [-c + d*i, a - b*i]])
1780 realM = ComplexMatrixEuclideanJordanAlgebra.real_embed(cplxM)
1781 blocks.append(realM)
1782
1783 # We should have real entries by now, so use the realest field
1784 # we've got for the return value.
1785 return matrix.block(quaternions.base_ring(), n, blocks)
1786
1787
1788
1789 @staticmethod
1790 def real_unembed(M):
1791 """
1792 The inverse of _embed_quaternion_matrix().
1793
1794 SETUP::
1795
1796 sage: from mjo.eja.eja_algebra import \
1797 ....: QuaternionMatrixEuclideanJordanAlgebra
1798
1799 EXAMPLES::
1800
1801 sage: M = matrix(QQ, [[ 1, 2, 3, 4],
1802 ....: [-2, 1, -4, 3],
1803 ....: [-3, 4, 1, -2],
1804 ....: [-4, -3, 2, 1]])
1805 sage: QuaternionMatrixEuclideanJordanAlgebra.real_unembed(M)
1806 [1 + 2*i + 3*j + 4*k]
1807
1808 TESTS:
1809
1810 Unembedding is the inverse of embedding::
1811
1812 sage: set_random_seed()
1813 sage: Q = QuaternionAlgebra(QQ, -1, -1)
1814 sage: M = random_matrix(Q, 3)
1815 sage: Me = QuaternionMatrixEuclideanJordanAlgebra.real_embed(M)
1816 sage: QuaternionMatrixEuclideanJordanAlgebra.real_unembed(Me) == M
1817 True
1818
1819 """
1820 n = ZZ(M.nrows())
1821 if M.ncols() != n:
1822 raise ValueError("the matrix 'M' must be square")
1823 if not n.mod(4).is_zero():
1824 raise ValueError("the matrix 'M' must be a quaternion embedding")
1825
1826 # Use the base ring of the matrix to ensure that its entries can be
1827 # multiplied by elements of the quaternion algebra.
1828 field = M.base_ring()
1829 Q = QuaternionAlgebra(field,-1,-1)
1830 i,j,k = Q.gens()
1831
1832 # Go top-left to bottom-right (reading order), converting every
1833 # 4-by-4 block we see to a 2-by-2 complex block, to a 1-by-1
1834 # quaternion block.
1835 elements = []
1836 for l in range(n/4):
1837 for m in range(n/4):
1838 submat = ComplexMatrixEuclideanJordanAlgebra.real_unembed(
1839 M[4*l:4*l+4,4*m:4*m+4] )
1840 if submat[0,0] != submat[1,1].conjugate():
1841 raise ValueError('bad on-diagonal submatrix')
1842 if submat[0,1] != -submat[1,0].conjugate():
1843 raise ValueError('bad off-diagonal submatrix')
1844 z = submat[0,0].real()
1845 z += submat[0,0].imag()*i
1846 z += submat[0,1].real()*j
1847 z += submat[0,1].imag()*k
1848 elements.append(z)
1849
1850 return matrix(Q, n/4, elements)
1851
1852
1853 @classmethod
1854 def natural_inner_product(cls,X,Y):
1855 """
1856 Compute a natural inner product in this algebra directly from
1857 its real embedding.
1858
1859 SETUP::
1860
1861 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
1862
1863 TESTS:
1864
1865 This gives the same answer as the slow, default method implemented
1866 in :class:`MatrixEuclideanJordanAlgebra`::
1867
1868 sage: set_random_seed()
1869 sage: J = QuaternionHermitianEJA.random_instance()
1870 sage: x,y = J.random_elements(2)
1871 sage: Xe = x.natural_representation()
1872 sage: Ye = y.natural_representation()
1873 sage: X = QuaternionHermitianEJA.real_unembed(Xe)
1874 sage: Y = QuaternionHermitianEJA.real_unembed(Ye)
1875 sage: expected = (X*Y).trace().coefficient_tuple()[0]
1876 sage: actual = QuaternionHermitianEJA.natural_inner_product(Xe,Ye)
1877 sage: actual == expected
1878 True
1879
1880 """
1881 return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/4
1882
1883
1884 class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra):
1885 """
1886 The rank-n simple EJA consisting of self-adjoint n-by-n quaternion
1887 matrices, the usual symmetric Jordan product, and the
1888 real-part-of-trace inner product. It has dimension `2n^2 - n` over
1889 the reals.
1890
1891 SETUP::
1892
1893 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
1894
1895 EXAMPLES:
1896
1897 In theory, our "field" can be any subfield of the reals::
1898
1899 sage: QuaternionHermitianEJA(2, RDF)
1900 Euclidean Jordan algebra of dimension 6 over Real Double Field
1901 sage: QuaternionHermitianEJA(2, RR)
1902 Euclidean Jordan algebra of dimension 6 over Real Field with
1903 53 bits of precision
1904
1905 TESTS:
1906
1907 The dimension of this algebra is `2*n^2 - n`::
1908
1909 sage: set_random_seed()
1910 sage: n_max = QuaternionHermitianEJA._max_test_case_size()
1911 sage: n = ZZ.random_element(1, n_max)
1912 sage: J = QuaternionHermitianEJA(n)
1913 sage: J.dimension() == 2*(n^2) - n
1914 True
1915
1916 The Jordan multiplication is what we think it is::
1917
1918 sage: set_random_seed()
1919 sage: J = QuaternionHermitianEJA.random_instance()
1920 sage: x,y = J.random_elements(2)
1921 sage: actual = (x*y).natural_representation()
1922 sage: X = x.natural_representation()
1923 sage: Y = y.natural_representation()
1924 sage: expected = (X*Y + Y*X)/2
1925 sage: actual == expected
1926 True
1927 sage: J(expected) == x*y
1928 True
1929
1930 We can change the generator prefix::
1931
1932 sage: QuaternionHermitianEJA(2, prefix='a').gens()
1933 (a0, a1, a2, a3, a4, a5)
1934
1935 Our natural basis is normalized with respect to the natural inner
1936 product unless we specify otherwise::
1937
1938 sage: set_random_seed()
1939 sage: J = QuaternionHermitianEJA.random_instance()
1940 sage: all( b.norm() == 1 for b in J.gens() )
1941 True
1942
1943 Since our natural basis is normalized with respect to the natural
1944 inner product, and since we know that this algebra is an EJA, any
1945 left-multiplication operator's matrix will be symmetric because
1946 natural->EJA basis representation is an isometry and within the EJA
1947 the operator is self-adjoint by the Jordan axiom::
1948
1949 sage: set_random_seed()
1950 sage: x = QuaternionHermitianEJA.random_instance().random_element()
1951 sage: x.operator().matrix().is_symmetric()
1952 True
1953
1954 We can construct the (trivial) algebra of rank zero::
1955
1956 sage: QuaternionHermitianEJA(0)
1957 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
1958
1959 """
1960 @classmethod
1961 def _denormalized_basis(cls, n, field):
1962 """
1963 Returns a basis for the space of quaternion Hermitian n-by-n matrices.
1964
1965 Why do we embed these? Basically, because all of numerical
1966 linear algebra assumes that you're working with vectors consisting
1967 of `n` entries from a field and scalars from the same field. There's
1968 no way to tell SageMath that (for example) the vectors contain
1969 complex numbers, while the scalar field is real.
1970
1971 SETUP::
1972
1973 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
1974
1975 TESTS::
1976
1977 sage: set_random_seed()
1978 sage: n = ZZ.random_element(1,5)
1979 sage: B = QuaternionHermitianEJA._denormalized_basis(n,QQ)
1980 sage: all( M.is_symmetric() for M in B )
1981 True
1982
1983 """
1984 Q = QuaternionAlgebra(QQ,-1,-1)
1985 I,J,K = Q.gens()
1986
1987 # This is like the symmetric case, but we need to be careful:
1988 #
1989 # * We want conjugate-symmetry, not just symmetry.
1990 # * The diagonal will (as a result) be real.
1991 #
1992 S = []
1993 for i in range(n):
1994 for j in range(i+1):
1995 Eij = matrix(Q, n, lambda k,l: k==i and l==j)
1996 if i == j:
1997 Sij = cls.real_embed(Eij)
1998 S.append(Sij)
1999 else:
2000 # The second, third, and fourth ones have a minus
2001 # because they're conjugated.
2002 Sij_real = cls.real_embed(Eij + Eij.transpose())
2003 S.append(Sij_real)
2004 Sij_I = cls.real_embed(I*Eij - I*Eij.transpose())
2005 S.append(Sij_I)
2006 Sij_J = cls.real_embed(J*Eij - J*Eij.transpose())
2007 S.append(Sij_J)
2008 Sij_K = cls.real_embed(K*Eij - K*Eij.transpose())
2009 S.append(Sij_K)
2010
2011 # Since we embedded these, we can drop back to the "field" that we
2012 # started with instead of the quaternion algebra "Q".
2013 return ( s.change_ring(field) for s in S )
2014
2015
2016 def __init__(self, n, field=AA, **kwargs):
2017 basis = self._denormalized_basis(n,field)
2018 super(QuaternionHermitianEJA,self).__init__(field,
2019 basis,
2020 check=False,
2021 **kwargs)
2022 self.rank.set_cache(n)
2023
2024
2025 class BilinearFormEJA(FiniteDimensionalEuclideanJordanAlgebra):
2026 r"""
2027 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
2028 with the half-trace inner product and jordan product ``x*y =
2029 (x0*y0 + <B*x_bar,y_bar>, x0*y_bar + y0*x_bar)`` where ``B`` is a
2030 symmetric positive-definite "bilinear form" matrix. It has
2031 dimension `n` over the reals, and reduces to the ``JordanSpinEJA``
2032 when ``B`` is the identity matrix of order ``n-1``.
2033
2034 SETUP::
2035
2036 sage: from mjo.eja.eja_algebra import (BilinearFormEJA,
2037 ....: JordanSpinEJA)
2038
2039 EXAMPLES:
2040
2041 When no bilinear form is specified, the identity matrix is used,
2042 and the resulting algebra is the Jordan spin algebra::
2043
2044 sage: J0 = BilinearFormEJA(3)
2045 sage: J1 = JordanSpinEJA(3)
2046 sage: J0.multiplication_table() == J0.multiplication_table()
2047 True
2048
2049 TESTS:
2050
2051 We can create a zero-dimensional algebra::
2052
2053 sage: J = BilinearFormEJA(0)
2054 sage: J.basis()
2055 Finite family {}
2056
2057 We can check the multiplication condition given in the Jordan, von
2058 Neumann, and Wigner paper (and also discussed on my "On the
2059 symmetry..." paper). Note that this relies heavily on the standard
2060 choice of basis, as does anything utilizing the bilinear form matrix::
2061
2062 sage: set_random_seed()
2063 sage: n = ZZ.random_element(5)
2064 sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular')
2065 sage: B = M.transpose()*M
2066 sage: J = BilinearFormEJA(n, B=B)
2067 sage: eis = VectorSpace(M.base_ring(), M.ncols()).basis()
2068 sage: V = J.vector_space()
2069 sage: sis = [ J.from_vector(V([0] + (M.inverse()*ei).list()))
2070 ....: for ei in eis ]
2071 sage: actual = [ sis[i]*sis[j]
2072 ....: for i in range(n-1)
2073 ....: for j in range(n-1) ]
2074 sage: expected = [ J.one() if i == j else J.zero()
2075 ....: for i in range(n-1)
2076 ....: for j in range(n-1) ]
2077 sage: actual == expected
2078 True
2079 """
2080 def __init__(self, n, field=AA, B=None, **kwargs):
2081 if B is None:
2082 self._B = matrix.identity(field, max(0,n-1))
2083 else:
2084 self._B = B
2085
2086 V = VectorSpace(field, n)
2087 mult_table = [[V.zero() for j in range(n)] for i in range(n)]
2088 for i in range(n):
2089 for j in range(n):
2090 x = V.gen(i)
2091 y = V.gen(j)
2092 x0 = x[0]
2093 xbar = x[1:]
2094 y0 = y[0]
2095 ybar = y[1:]
2096 z0 = x0*y0 + (self._B*xbar).inner_product(ybar)
2097 zbar = y0*xbar + x0*ybar
2098 z = V([z0] + zbar.list())
2099 mult_table[i][j] = z
2100
2101 # The rank of this algebra is two, unless we're in a
2102 # one-dimensional ambient space (because the rank is bounded
2103 # by the ambient dimension).
2104 super(BilinearFormEJA, self).__init__(field,
2105 mult_table,
2106 check=False,
2107 **kwargs)
2108 self.rank.set_cache(min(n,2))
2109
2110 def inner_product(self, x, y):
2111 r"""
2112 Half of the trace inner product.
2113
2114 This is defined so that the special case of the Jordan spin
2115 algebra gets the usual inner product.
2116
2117 SETUP::
2118
2119 sage: from mjo.eja.eja_algebra import BilinearFormEJA
2120
2121 TESTS:
2122
2123 Ensure that this is one-half of the trace inner-product when
2124 the algebra isn't just the reals (when ``n`` isn't one). This
2125 is in Faraut and Koranyi, and also my "On the symmetry..."
2126 paper::
2127
2128 sage: set_random_seed()
2129 sage: n = ZZ.random_element(2,5)
2130 sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular')
2131 sage: B = M.transpose()*M
2132 sage: J = BilinearFormEJA(n, B=B)
2133 sage: x = J.random_element()
2134 sage: y = J.random_element()
2135 sage: x.inner_product(y) == (x*y).trace()/2
2136 True
2137
2138 """
2139 xvec = x.to_vector()
2140 xbar = xvec[1:]
2141 yvec = y.to_vector()
2142 ybar = yvec[1:]
2143 return x[0]*y[0] + (self._B*xbar).inner_product(ybar)
2144
2145
2146 class JordanSpinEJA(BilinearFormEJA):
2147 """
2148 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
2149 with the usual inner product and jordan product ``x*y =
2150 (<x,y>, x0*y_bar + y0*x_bar)``. It has dimension `n` over
2151 the reals.
2152
2153 SETUP::
2154
2155 sage: from mjo.eja.eja_algebra import JordanSpinEJA
2156
2157 EXAMPLES:
2158
2159 This multiplication table can be verified by hand::
2160
2161 sage: J = JordanSpinEJA(4)
2162 sage: e0,e1,e2,e3 = J.gens()
2163 sage: e0*e0
2164 e0
2165 sage: e0*e1
2166 e1
2167 sage: e0*e2
2168 e2
2169 sage: e0*e3
2170 e3
2171 sage: e1*e2
2172 0
2173 sage: e1*e3
2174 0
2175 sage: e2*e3
2176 0
2177
2178 We can change the generator prefix::
2179
2180 sage: JordanSpinEJA(2, prefix='B').gens()
2181 (B0, B1)
2182
2183 TESTS:
2184
2185 Ensure that we have the usual inner product on `R^n`::
2186
2187 sage: set_random_seed()
2188 sage: J = JordanSpinEJA.random_instance()
2189 sage: x,y = J.random_elements(2)
2190 sage: X = x.natural_representation()
2191 sage: Y = y.natural_representation()
2192 sage: x.inner_product(y) == J.natural_inner_product(X,Y)
2193 True
2194
2195 """
2196 def __init__(self, n, field=AA, **kwargs):
2197 # This is a special case of the BilinearFormEJA with the identity
2198 # matrix as its bilinear form.
2199 super(JordanSpinEJA, self).__init__(n, field, **kwargs)
2200
2201
2202 class TrivialEJA(FiniteDimensionalEuclideanJordanAlgebra):
2203 """
2204 The trivial Euclidean Jordan algebra consisting of only a zero element.
2205
2206 SETUP::
2207
2208 sage: from mjo.eja.eja_algebra import TrivialEJA
2209
2210 EXAMPLES::
2211
2212 sage: J = TrivialEJA()
2213 sage: J.dimension()
2214 0
2215 sage: J.zero()
2216 0
2217 sage: J.one()
2218 0
2219 sage: 7*J.one()*12*J.one()
2220 0
2221 sage: J.one().inner_product(J.one())
2222 0
2223 sage: J.one().norm()
2224 0
2225 sage: J.one().subalgebra_generated_by()
2226 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2227 sage: J.rank()
2228 0
2229
2230 """
2231 def __init__(self, field=AA, **kwargs):
2232 mult_table = []
2233 super(TrivialEJA, self).__init__(field,
2234 mult_table,
2235 check=False,
2236 **kwargs)
2237 # The rank is zero using my definition, namely the dimension of the
2238 # largest subalgebra generated by any element.
2239 self.rank.set_cache(0)
2240
2241
2242 class DirectSumEJA(FiniteDimensionalEuclideanJordanAlgebra):
2243 r"""
2244 The external (orthogonal) direct sum of two other Euclidean Jordan
2245 algebras. Essentially the Cartesian product of its two factors.
2246 Every Euclidean Jordan algebra decomposes into an orthogonal
2247 direct sum of simple Euclidean Jordan algebras, so no generality
2248 is lost by providing only this construction.
2249
2250 SETUP::
2251
2252 sage: from mjo.eja.eja_algebra import (HadamardEJA,
2253 ....: RealSymmetricEJA,
2254 ....: DirectSumEJA)
2255
2256 EXAMPLES::
2257
2258 sage: J1 = HadamardEJA(2)
2259 sage: J2 = RealSymmetricEJA(3)
2260 sage: J = DirectSumEJA(J1,J2)
2261 sage: J.dimension()
2262 8
2263 sage: J.rank()
2264 5
2265
2266 """
2267 def __init__(self, J1, J2, field=AA, **kwargs):
2268 n1 = J1.dimension()
2269 n2 = J2.dimension()
2270 n = n1+n2
2271 V = VectorSpace(field, n)
2272 mult_table = [ [ V.zero() for j in range(n) ]
2273 for i in range(n) ]
2274 for i in range(n1):
2275 for j in range(n1):
2276 p = (J1.monomial(i)*J1.monomial(j)).to_vector()
2277 mult_table[i][j] = V(p.list() + [field.zero()]*n2)
2278
2279 for i in range(n2):
2280 for j in range(n2):
2281 p = (J2.monomial(i)*J2.monomial(j)).to_vector()
2282 mult_table[n1+i][n1+j] = V([field.zero()]*n1 + p.list())
2283
2284 super(DirectSumEJA, self).__init__(field,
2285 mult_table,
2286 check=False,
2287 **kwargs)
2288 self.rank.set_cache(J1.rank() + J2.rank())