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