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