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