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