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