]> gitweb.michael.orlitzky.com - sage.d.git/blob - mjo/eja/eja_algebra.py
eja: document why an a[0] access is safe.
[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 from itertools import repeat
9
10 from sage.algebras.quatalg.quaternion_algebra import QuaternionAlgebra
11 from sage.categories.magmatic_algebras import MagmaticAlgebras
12 from sage.combinat.free_module import CombinatorialFreeModule
13 from sage.matrix.constructor import matrix
14 from sage.matrix.matrix_space import MatrixSpace
15 from sage.misc.cachefunc import cached_method
16 from sage.misc.lazy_import import lazy_import
17 from sage.misc.prandom import choice
18 from sage.misc.table import table
19 from sage.modules.free_module import FreeModule, VectorSpace
20 from sage.rings.all import (ZZ, QQ, AA, QQbar, RR, RLF, CLF,
21 PolynomialRing,
22 QuadraticField)
23 from mjo.eja.eja_element import FiniteDimensionalEuclideanJordanAlgebraElement
24 lazy_import('mjo.eja.eja_subalgebra',
25 'FiniteDimensionalEuclideanJordanSubalgebra')
26 from mjo.eja.eja_utils import _mat2vec
27
28 class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
29
30 def _coerce_map_from_base_ring(self):
31 """
32 Disable the map from the base ring into the algebra.
33
34 Performing a nonsense conversion like this automatically
35 is counterpedagogical. The fallback is to try the usual
36 element constructor, which should also fail.
37
38 SETUP::
39
40 sage: from mjo.eja.eja_algebra import random_eja
41
42 TESTS::
43
44 sage: set_random_seed()
45 sage: J = random_eja()
46 sage: J(1)
47 Traceback (most recent call last):
48 ...
49 ValueError: not a naturally-represented algebra element
50
51 """
52 return None
53
54 def __init__(self,
55 field,
56 mult_table,
57 prefix='e',
58 category=None,
59 natural_basis=None,
60 check=True):
61 """
62 SETUP::
63
64 sage: from mjo.eja.eja_algebra import (JordanSpinEJA, random_eja)
65
66 EXAMPLES:
67
68 By definition, Jordan multiplication commutes::
69
70 sage: set_random_seed()
71 sage: J = random_eja()
72 sage: x,y = J.random_elements(2)
73 sage: x*y == y*x
74 True
75
76 TESTS:
77
78 The ``field`` we're given must be real::
79
80 sage: JordanSpinEJA(2,QQbar)
81 Traceback (most recent call last):
82 ...
83 ValueError: field is not real
84
85 """
86 if check:
87 if not field.is_subring(RR):
88 # Note: this does return true for the real algebraic
89 # field, and any quadratic field where we've specified
90 # a real embedding.
91 raise ValueError('field is not real')
92
93 self._natural_basis = natural_basis
94
95 if category is None:
96 category = MagmaticAlgebras(field).FiniteDimensional()
97 category = category.WithBasis().Unital()
98
99 fda = super(FiniteDimensionalEuclideanJordanAlgebra, self)
100 fda.__init__(field,
101 range(len(mult_table)),
102 prefix=prefix,
103 category=category)
104 self.print_options(bracket='')
105
106 # The multiplication table we're given is necessarily in terms
107 # of vectors, because we don't have an algebra yet for
108 # anything to be an element of. However, it's faster in the
109 # long run to have the multiplication table be in terms of
110 # algebra elements. We do this after calling the superclass
111 # constructor so that from_vector() knows what to do.
112 self._multiplication_table = [
113 list(map(lambda x: self.from_vector(x), ls))
114 for ls in mult_table
115 ]
116
117
118 def _element_constructor_(self, elt):
119 """
120 Construct an element of this algebra from its natural
121 representation.
122
123 This gets called only after the parent element _call_ method
124 fails to find a coercion for the argument.
125
126 SETUP::
127
128 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
129 ....: HadamardEJA,
130 ....: RealSymmetricEJA)
131
132 EXAMPLES:
133
134 The identity in `S^n` is converted to the identity in the EJA::
135
136 sage: J = RealSymmetricEJA(3)
137 sage: I = matrix.identity(QQ,3)
138 sage: J(I) == J.one()
139 True
140
141 This skew-symmetric matrix can't be represented in the EJA::
142
143 sage: J = RealSymmetricEJA(3)
144 sage: A = matrix(QQ,3, lambda i,j: i-j)
145 sage: J(A)
146 Traceback (most recent call last):
147 ...
148 ArithmeticError: vector is not in free module
149
150 TESTS:
151
152 Ensure that we can convert any element of the two non-matrix
153 simple algebras (whose natural representations are their usual
154 vector representations) back and forth faithfully::
155
156 sage: set_random_seed()
157 sage: J = HadamardEJA.random_instance()
158 sage: x = J.random_element()
159 sage: J(x.to_vector().column()) == x
160 True
161 sage: J = JordanSpinEJA.random_instance()
162 sage: x = J.random_element()
163 sage: J(x.to_vector().column()) == x
164 True
165
166 """
167 msg = "not a naturally-represented algebra element"
168 if elt == 0:
169 # The superclass implementation of random_element()
170 # needs to be able to coerce "0" into the algebra.
171 return self.zero()
172 elif elt in self.base_ring():
173 # Ensure that no base ring -> algebra coercion is performed
174 # by this method. There's some stupidity in sage that would
175 # otherwise propagate to this method; for example, sage thinks
176 # that the integer 3 belongs to the space of 2-by-2 matrices.
177 raise ValueError(msg)
178
179 natural_basis = self.natural_basis()
180 basis_space = natural_basis[0].matrix_space()
181 if elt not in basis_space:
182 raise ValueError(msg)
183
184 # Thanks for nothing! Matrix spaces aren't vector spaces in
185 # Sage, so we have to figure out its natural-basis coordinates
186 # ourselves. We use the basis space's ring instead of the
187 # element's ring because the basis space might be an algebraic
188 # closure whereas the base ring of the 3-by-3 identity matrix
189 # could be QQ instead of QQbar.
190 V = VectorSpace(basis_space.base_ring(), elt.nrows()*elt.ncols())
191 W = V.span_of_basis( _mat2vec(s) for s in natural_basis )
192 coords = W.coordinate_vector(_mat2vec(elt))
193 return self.from_vector(coords)
194
195 @staticmethod
196 def _max_test_case_size():
197 """
198 Return an integer "size" that is an upper bound on the size of
199 this algebra when it is used in a random test
200 case. Unfortunately, the term "size" is quite vague -- when
201 dealing with `R^n` under either the Hadamard or Jordan spin
202 product, the "size" refers to the dimension `n`. When dealing
203 with a matrix algebra (real symmetric or complex/quaternion
204 Hermitian), it refers to the size of the matrix, which is
205 far less than the dimension of the underlying vector space.
206
207 We default to five in this class, which is safe in `R^n`. The
208 matrix algebra subclasses (or any class where the "size" is
209 interpreted to be far less than the dimension) should override
210 with a smaller number.
211 """
212 return 5
213
214 def _repr_(self):
215 """
216 Return a string representation of ``self``.
217
218 SETUP::
219
220 sage: from mjo.eja.eja_algebra import JordanSpinEJA
221
222 TESTS:
223
224 Ensure that it says what we think it says::
225
226 sage: JordanSpinEJA(2, field=AA)
227 Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
228 sage: JordanSpinEJA(3, field=RDF)
229 Euclidean Jordan algebra of dimension 3 over Real Double Field
230
231 """
232 fmt = "Euclidean Jordan algebra of dimension {} over {}"
233 return fmt.format(self.dimension(), self.base_ring())
234
235 def product_on_basis(self, i, j):
236 return self._multiplication_table[i][j]
237
238 @cached_method
239 def characteristic_polynomial(self):
240 """
241 Return a characteristic polynomial that works for all elements
242 of this algebra.
243
244 The resulting polynomial has `n+1` variables, where `n` is the
245 dimension of this algebra. The first `n` variables correspond to
246 the coordinates of an algebra element: when evaluated at the
247 coordinates of an algebra element with respect to a certain
248 basis, the result is a univariate polynomial (in the one
249 remaining variable ``t``), namely the characteristic polynomial
250 of that element.
251
252 SETUP::
253
254 sage: from mjo.eja.eja_algebra import JordanSpinEJA, TrivialEJA
255
256 EXAMPLES:
257
258 The characteristic polynomial in the spin algebra is given in
259 Alizadeh, Example 11.11::
260
261 sage: J = JordanSpinEJA(3)
262 sage: p = J.characteristic_polynomial(); p
263 X1^2 - X2^2 - X3^2 + (-2*t)*X1 + t^2
264 sage: xvec = J.one().to_vector()
265 sage: p(*xvec)
266 t^2 - 2*t + 1
267
268 By definition, the characteristic polynomial is a monic
269 degree-zero polynomial in a rank-zero algebra. Note that
270 Cayley-Hamilton is indeed satisfied since the polynomial
271 ``1`` evaluates to the identity element of the algebra on
272 any argument::
273
274 sage: J = TrivialEJA()
275 sage: J.characteristic_polynomial()
276 1
277
278 """
279 r = self.rank()
280 n = self.dimension()
281
282 # The list of coefficient polynomials a_0, a_1, a_2, ..., a_(r-1).
283 a = self._charpoly_coefficients()
284
285 # We go to a bit of trouble here to reorder the
286 # indeterminates, so that it's easier to evaluate the
287 # characteristic polynomial at x's coordinates and get back
288 # something in terms of t, which is what we want.
289 S = PolynomialRing(self.base_ring(),'t')
290 t = S.gen(0)
291 if r > 0:
292 R = a[0].parent()
293 S = PolynomialRing(S, R.variable_names())
294 t = S(t)
295
296 return (t**r + sum( a[k]*(t**k) for k in range(r) ))
297
298
299 def inner_product(self, x, y):
300 """
301 The inner product associated with this Euclidean Jordan algebra.
302
303 Defaults to the trace inner product, but can be overridden by
304 subclasses if they are sure that the necessary properties are
305 satisfied.
306
307 SETUP::
308
309 sage: from mjo.eja.eja_algebra import random_eja
310
311 EXAMPLES:
312
313 Our inner product is "associative," which means the following for
314 a symmetric bilinear form::
315
316 sage: set_random_seed()
317 sage: J = random_eja()
318 sage: x,y,z = J.random_elements(3)
319 sage: (x*y).inner_product(z) == y.inner_product(x*z)
320 True
321
322 """
323 X = x.natural_representation()
324 Y = y.natural_representation()
325 return self.natural_inner_product(X,Y)
326
327
328 def is_trivial(self):
329 """
330 Return whether or not this algebra is trivial.
331
332 A trivial algebra contains only the zero element.
333
334 SETUP::
335
336 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
337 ....: TrivialEJA)
338
339 EXAMPLES::
340
341 sage: J = ComplexHermitianEJA(3)
342 sage: J.is_trivial()
343 False
344
345 ::
346
347 sage: J = TrivialEJA()
348 sage: J.is_trivial()
349 True
350
351 """
352 return self.dimension() == 0
353
354
355 def multiplication_table(self):
356 """
357 Return a visual representation of this algebra's multiplication
358 table (on basis elements).
359
360 SETUP::
361
362 sage: from mjo.eja.eja_algebra import JordanSpinEJA
363
364 EXAMPLES::
365
366 sage: J = JordanSpinEJA(4)
367 sage: J.multiplication_table()
368 +----++----+----+----+----+
369 | * || e0 | e1 | e2 | e3 |
370 +====++====+====+====+====+
371 | e0 || e0 | e1 | e2 | e3 |
372 +----++----+----+----+----+
373 | e1 || e1 | e0 | 0 | 0 |
374 +----++----+----+----+----+
375 | e2 || e2 | 0 | e0 | 0 |
376 +----++----+----+----+----+
377 | e3 || e3 | 0 | 0 | e0 |
378 +----++----+----+----+----+
379
380 """
381 M = list(self._multiplication_table) # copy
382 for i in range(len(M)):
383 # M had better be "square"
384 M[i] = [self.monomial(i)] + M[i]
385 M = [["*"] + list(self.gens())] + M
386 return table(M, header_row=True, header_column=True, frame=True)
387
388
389 def natural_basis(self):
390 """
391 Return a more-natural representation of this algebra's basis.
392
393 Every finite-dimensional Euclidean Jordan Algebra is a direct
394 sum of five simple algebras, four of which comprise Hermitian
395 matrices. This method returns the original "natural" basis
396 for our underlying vector space. (Typically, the natural basis
397 is used to construct the multiplication table in the first place.)
398
399 Note that this will always return a matrix. The standard basis
400 in `R^n` will be returned as `n`-by-`1` column matrices.
401
402 SETUP::
403
404 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
405 ....: RealSymmetricEJA)
406
407 EXAMPLES::
408
409 sage: J = RealSymmetricEJA(2)
410 sage: J.basis()
411 Finite family {0: e0, 1: e1, 2: e2}
412 sage: J.natural_basis()
413 (
414 [1 0] [ 0 0.7071067811865475?] [0 0]
415 [0 0], [0.7071067811865475? 0], [0 1]
416 )
417
418 ::
419
420 sage: J = JordanSpinEJA(2)
421 sage: J.basis()
422 Finite family {0: e0, 1: e1}
423 sage: J.natural_basis()
424 (
425 [1] [0]
426 [0], [1]
427 )
428
429 """
430 if self._natural_basis is None:
431 M = self.natural_basis_space()
432 return tuple( M(b.to_vector()) for b in self.basis() )
433 else:
434 return self._natural_basis
435
436
437 def natural_basis_space(self):
438 """
439 Return the matrix space in which this algebra's natural basis
440 elements live.
441 """
442 if self._natural_basis is None or len(self._natural_basis) == 0:
443 return MatrixSpace(self.base_ring(), self.dimension(), 1)
444 else:
445 return self._natural_basis[0].matrix_space()
446
447
448 @staticmethod
449 def natural_inner_product(X,Y):
450 """
451 Compute the inner product of two naturally-represented elements.
452
453 For example in the real symmetric matrix EJA, this will compute
454 the trace inner-product of two n-by-n symmetric matrices. The
455 default should work for the real cartesian product EJA, the
456 Jordan spin EJA, and the real symmetric matrices. The others
457 will have to be overridden.
458 """
459 return (X.conjugate_transpose()*Y).trace()
460
461
462 @cached_method
463 def one(self):
464 """
465 Return the unit element of this algebra.
466
467 SETUP::
468
469 sage: from mjo.eja.eja_algebra import (HadamardEJA,
470 ....: random_eja)
471
472 EXAMPLES::
473
474 sage: J = HadamardEJA(5)
475 sage: J.one()
476 e0 + e1 + e2 + e3 + e4
477
478 TESTS:
479
480 The identity element acts like the identity::
481
482 sage: set_random_seed()
483 sage: J = random_eja()
484 sage: x = J.random_element()
485 sage: J.one()*x == x and x*J.one() == x
486 True
487
488 The matrix of the unit element's operator is the identity::
489
490 sage: set_random_seed()
491 sage: J = random_eja()
492 sage: actual = J.one().operator().matrix()
493 sage: expected = matrix.identity(J.base_ring(), J.dimension())
494 sage: actual == expected
495 True
496
497 """
498 # We can brute-force compute the matrices of the operators
499 # that correspond to the basis elements of this algebra.
500 # If some linear combination of those basis elements is the
501 # algebra identity, then the same linear combination of
502 # their matrices has to be the identity matrix.
503 #
504 # Of course, matrices aren't vectors in sage, so we have to
505 # appeal to the "long vectors" isometry.
506 oper_vecs = [ _mat2vec(g.operator().matrix()) for g in self.gens() ]
507
508 # Now we use basis linear algebra to find the coefficients,
509 # of the matrices-as-vectors-linear-combination, which should
510 # work for the original algebra basis too.
511 A = matrix.column(self.base_ring(), oper_vecs)
512
513 # We used the isometry on the left-hand side already, but we
514 # still need to do it for the right-hand side. Recall that we
515 # wanted something that summed to the identity matrix.
516 b = _mat2vec( matrix.identity(self.base_ring(), self.dimension()) )
517
518 # Now if there's an identity element in the algebra, this should work.
519 coeffs = A.solve_right(b)
520 return self.linear_combination(zip(self.gens(), coeffs))
521
522
523 def peirce_decomposition(self, c):
524 """
525 The Peirce decomposition of this algebra relative to the
526 idempotent ``c``.
527
528 In the future, this can be extended to a complete system of
529 orthogonal idempotents.
530
531 INPUT:
532
533 - ``c`` -- an idempotent of this algebra.
534
535 OUTPUT:
536
537 A triple (J0, J5, J1) containing two subalgebras and one subspace
538 of this algebra,
539
540 - ``J0`` -- the algebra on the eigenspace of ``c.operator()``
541 corresponding to the eigenvalue zero.
542
543 - ``J5`` -- the eigenspace (NOT a subalgebra) of ``c.operator()``
544 corresponding to the eigenvalue one-half.
545
546 - ``J1`` -- the algebra on the eigenspace of ``c.operator()``
547 corresponding to the eigenvalue one.
548
549 These are the only possible eigenspaces for that operator, and this
550 algebra is a direct sum of them. The spaces ``J0`` and ``J1`` are
551 orthogonal, and are subalgebras of this algebra with the appropriate
552 restrictions.
553
554 SETUP::
555
556 sage: from mjo.eja.eja_algebra import random_eja, RealSymmetricEJA
557
558 EXAMPLES:
559
560 The canonical example comes from the symmetric matrices, which
561 decompose into diagonal and off-diagonal parts::
562
563 sage: J = RealSymmetricEJA(3)
564 sage: C = matrix(QQ, [ [1,0,0],
565 ....: [0,1,0],
566 ....: [0,0,0] ])
567 sage: c = J(C)
568 sage: J0,J5,J1 = J.peirce_decomposition(c)
569 sage: J0
570 Euclidean Jordan algebra of dimension 1...
571 sage: J5
572 Vector space of degree 6 and dimension 2...
573 sage: J1
574 Euclidean Jordan algebra of dimension 3...
575
576 TESTS:
577
578 Every algebra decomposes trivially with respect to its identity
579 element::
580
581 sage: set_random_seed()
582 sage: J = random_eja()
583 sage: J0,J5,J1 = J.peirce_decomposition(J.one())
584 sage: J0.dimension() == 0 and J5.dimension() == 0
585 True
586 sage: J1.superalgebra() == J and J1.dimension() == J.dimension()
587 True
588
589 The identity elements in the two subalgebras are the
590 projections onto their respective subspaces of the
591 superalgebra's identity element::
592
593 sage: set_random_seed()
594 sage: J = random_eja()
595 sage: x = J.random_element()
596 sage: if not J.is_trivial():
597 ....: while x.is_nilpotent():
598 ....: x = J.random_element()
599 sage: c = x.subalgebra_idempotent()
600 sage: J0,J5,J1 = J.peirce_decomposition(c)
601 sage: J1(c) == J1.one()
602 True
603 sage: J0(J.one() - c) == J0.one()
604 True
605
606 """
607 if not c.is_idempotent():
608 raise ValueError("element is not idempotent: %s" % c)
609
610 # Default these to what they should be if they turn out to be
611 # trivial, because eigenspaces_left() won't return eigenvalues
612 # corresponding to trivial spaces (e.g. it returns only the
613 # eigenspace corresponding to lambda=1 if you take the
614 # decomposition relative to the identity element).
615 trivial = FiniteDimensionalEuclideanJordanSubalgebra(self, ())
616 J0 = trivial # eigenvalue zero
617 J5 = VectorSpace(self.base_ring(), 0) # eigenvalue one-half
618 J1 = trivial # eigenvalue one
619
620 for (eigval, eigspace) in c.operator().matrix().right_eigenspaces():
621 if eigval == ~(self.base_ring()(2)):
622 J5 = eigspace
623 else:
624 gens = tuple( self.from_vector(b) for b in eigspace.basis() )
625 subalg = FiniteDimensionalEuclideanJordanSubalgebra(self, gens)
626 if eigval == 0:
627 J0 = subalg
628 elif eigval == 1:
629 J1 = subalg
630 else:
631 raise ValueError("unexpected eigenvalue: %s" % eigval)
632
633 return (J0, J5, J1)
634
635
636 def random_elements(self, count):
637 """
638 Return ``count`` random elements as a tuple.
639
640 SETUP::
641
642 sage: from mjo.eja.eja_algebra import JordanSpinEJA
643
644 EXAMPLES::
645
646 sage: J = JordanSpinEJA(3)
647 sage: x,y,z = J.random_elements(3)
648 sage: all( [ x in J, y in J, z in J ])
649 True
650 sage: len( J.random_elements(10) ) == 10
651 True
652
653 """
654 return tuple( self.random_element() for idx in range(count) )
655
656 @classmethod
657 def random_instance(cls, field=AA, **kwargs):
658 """
659 Return a random instance of this type of algebra.
660
661 Beware, this will crash for "most instances" because the
662 constructor below looks wrong.
663 """
664 if cls is TrivialEJA:
665 # The TrivialEJA class doesn't take an "n" argument because
666 # there's only one.
667 return cls(field)
668
669 n = ZZ.random_element(cls._max_test_case_size()) + 1
670 return cls(n, field, **kwargs)
671
672 @cached_method
673 def _charpoly_coefficients(self):
674 r"""
675 The `r` polynomial coefficients of the "characteristic polynomial
676 of" function.
677 """
678 n = self.dimension()
679 var_names = [ "X" + str(z) for z in range(1,n+1) ]
680 R = PolynomialRing(self.base_ring(), var_names)
681 vars = R.gens()
682 F = R.fraction_field()
683
684 def L_x_i_j(i,j):
685 # From a result in my book, these are the entries of the
686 # basis representation of L_x.
687 return sum( vars[k]*self.monomial(k).operator().matrix()[i,j]
688 for k in range(n) )
689
690 L_x = matrix(F, n, n, L_x_i_j)
691
692 r = None
693 if self.rank.is_in_cache():
694 r = self.rank()
695 # There's no need to pad the system with redundant
696 # columns if we *know* they'll be redundant.
697 n = r
698
699 # Compute an extra power in case the rank is equal to
700 # the dimension (otherwise, we would stop at x^(r-1)).
701 x_powers = [ (L_x**k)*self.one().to_vector()
702 for k in range(n+1) ]
703 A = matrix.column(F, x_powers[:n])
704 AE = A.extended_echelon_form()
705 E = AE[:,n:]
706 A_rref = AE[:,:n]
707 if r is None:
708 r = A_rref.rank()
709 b = x_powers[r]
710
711 # The theory says that only the first "r" coefficients are
712 # nonzero, and they actually live in the original polynomial
713 # ring and not the fraction field. We negate them because
714 # in the actual characteristic polynomial, they get moved
715 # to the other side where x^r lives.
716 return -A_rref.solve_right(E*b).change_ring(R)[:r]
717
718 @cached_method
719 def rank(self):
720 r"""
721 Return the rank of this EJA.
722
723 This is a cached method because we know the rank a priori for
724 all of the algebras we can construct. Thus we can avoid the
725 expensive ``_charpoly_coefficients()`` call unless we truly
726 need to compute the whole characteristic polynomial.
727
728 SETUP::
729
730 sage: from mjo.eja.eja_algebra import (HadamardEJA,
731 ....: JordanSpinEJA,
732 ....: RealSymmetricEJA,
733 ....: ComplexHermitianEJA,
734 ....: QuaternionHermitianEJA,
735 ....: random_eja)
736
737 EXAMPLES:
738
739 The rank of the Jordan spin algebra is always two::
740
741 sage: JordanSpinEJA(2).rank()
742 2
743 sage: JordanSpinEJA(3).rank()
744 2
745 sage: JordanSpinEJA(4).rank()
746 2
747
748 The rank of the `n`-by-`n` Hermitian real, complex, or
749 quaternion matrices is `n`::
750
751 sage: RealSymmetricEJA(4).rank()
752 4
753 sage: ComplexHermitianEJA(3).rank()
754 3
755 sage: QuaternionHermitianEJA(2).rank()
756 2
757
758 TESTS:
759
760 Ensure that every EJA that we know how to construct has a
761 positive integer rank, unless the algebra is trivial in
762 which case its rank will be zero::
763
764 sage: set_random_seed()
765 sage: J = random_eja()
766 sage: r = J.rank()
767 sage: r in ZZ
768 True
769 sage: r > 0 or (r == 0 and J.is_trivial())
770 True
771
772 Ensure that computing the rank actually works, since the ranks
773 of all simple algebras are known and will be cached by default::
774
775 sage: J = HadamardEJA(4)
776 sage: J.rank.clear_cache()
777 sage: J.rank()
778 4
779
780 ::
781
782 sage: J = JordanSpinEJA(4)
783 sage: J.rank.clear_cache()
784 sage: J.rank()
785 2
786
787 ::
788
789 sage: J = RealSymmetricEJA(3)
790 sage: J.rank.clear_cache()
791 sage: J.rank()
792 3
793
794 ::
795
796 sage: J = ComplexHermitianEJA(2)
797 sage: J.rank.clear_cache()
798 sage: J.rank()
799 2
800
801 ::
802
803 sage: J = QuaternionHermitianEJA(2)
804 sage: J.rank.clear_cache()
805 sage: J.rank()
806 2
807 """
808 return len(self._charpoly_coefficients())
809
810
811 def vector_space(self):
812 """
813 Return the vector space that underlies this algebra.
814
815 SETUP::
816
817 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
818
819 EXAMPLES::
820
821 sage: J = RealSymmetricEJA(2)
822 sage: J.vector_space()
823 Vector space of dimension 3 over...
824
825 """
826 return self.zero().to_vector().parent().ambient_vector_space()
827
828
829 Element = FiniteDimensionalEuclideanJordanAlgebraElement
830
831
832 class HadamardEJA(FiniteDimensionalEuclideanJordanAlgebra):
833 """
834 Return the Euclidean Jordan Algebra corresponding to the set
835 `R^n` under the Hadamard product.
836
837 Note: this is nothing more than the Cartesian product of ``n``
838 copies of the spin algebra. Once Cartesian product algebras
839 are implemented, this can go.
840
841 SETUP::
842
843 sage: from mjo.eja.eja_algebra import HadamardEJA
844
845 EXAMPLES:
846
847 This multiplication table can be verified by hand::
848
849 sage: J = HadamardEJA(3)
850 sage: e0,e1,e2 = J.gens()
851 sage: e0*e0
852 e0
853 sage: e0*e1
854 0
855 sage: e0*e2
856 0
857 sage: e1*e1
858 e1
859 sage: e1*e2
860 0
861 sage: e2*e2
862 e2
863
864 TESTS:
865
866 We can change the generator prefix::
867
868 sage: HadamardEJA(3, prefix='r').gens()
869 (r0, r1, r2)
870
871 """
872 def __init__(self, n, field=AA, **kwargs):
873 V = VectorSpace(field, n)
874 mult_table = [ [ V.gen(i)*(i == j) for j in range(n) ]
875 for i in range(n) ]
876
877 fdeja = super(HadamardEJA, self)
878 fdeja.__init__(field, mult_table, **kwargs)
879 self.rank.set_cache(n)
880
881 def inner_product(self, x, y):
882 """
883 Faster to reimplement than to use natural representations.
884
885 SETUP::
886
887 sage: from mjo.eja.eja_algebra import HadamardEJA
888
889 TESTS:
890
891 Ensure that this is the usual inner product for the algebras
892 over `R^n`::
893
894 sage: set_random_seed()
895 sage: J = HadamardEJA.random_instance()
896 sage: x,y = J.random_elements(2)
897 sage: X = x.natural_representation()
898 sage: Y = y.natural_representation()
899 sage: x.inner_product(y) == J.natural_inner_product(X,Y)
900 True
901
902 """
903 return x.to_vector().inner_product(y.to_vector())
904
905
906 def random_eja(field=AA, nontrivial=False):
907 """
908 Return a "random" finite-dimensional Euclidean Jordan Algebra.
909
910 SETUP::
911
912 sage: from mjo.eja.eja_algebra import random_eja
913
914 TESTS::
915
916 sage: random_eja()
917 Euclidean Jordan algebra of dimension...
918
919 """
920 eja_classes = [HadamardEJA,
921 JordanSpinEJA,
922 RealSymmetricEJA,
923 ComplexHermitianEJA,
924 QuaternionHermitianEJA]
925 if not nontrivial:
926 eja_classes.append(TrivialEJA)
927 classname = choice(eja_classes)
928 return classname.random_instance(field=field)
929
930
931
932
933
934
935 class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
936 @staticmethod
937 def _max_test_case_size():
938 # Play it safe, since this will be squared and the underlying
939 # field can have dimension 4 (quaternions) too.
940 return 2
941
942 def __init__(self, field, basis, normalize_basis=True, **kwargs):
943 """
944 Compared to the superclass constructor, we take a basis instead of
945 a multiplication table because the latter can be computed in terms
946 of the former when the product is known (like it is here).
947 """
948 # Used in this class's fast _charpoly_coefficients() override.
949 self._basis_normalizers = None
950
951 # We're going to loop through this a few times, so now's a good
952 # time to ensure that it isn't a generator expression.
953 basis = tuple(basis)
954
955 if len(basis) > 1 and normalize_basis:
956 # We'll need sqrt(2) to normalize the basis, and this
957 # winds up in the multiplication table, so the whole
958 # algebra needs to be over the field extension.
959 R = PolynomialRing(field, 'z')
960 z = R.gen()
961 p = z**2 - 2
962 if p.is_irreducible():
963 field = field.extension(p, 'sqrt2', embedding=RLF(2).sqrt())
964 basis = tuple( s.change_ring(field) for s in basis )
965 self._basis_normalizers = tuple(
966 ~(self.natural_inner_product(s,s).sqrt()) for s in basis )
967 basis = tuple(s*c for (s,c) in zip(basis,self._basis_normalizers))
968
969 Qs = self.multiplication_table_from_matrix_basis(basis)
970
971 fdeja = super(MatrixEuclideanJordanAlgebra, self)
972 fdeja.__init__(field, Qs, natural_basis=basis, **kwargs)
973 return
974
975
976 @cached_method
977 def _charpoly_coefficients(self):
978 r"""
979 Override the parent method with something that tries to compute
980 over a faster (non-extension) field.
981 """
982 if self._basis_normalizers is None:
983 # We didn't normalize, so assume that the basis we started
984 # with had entries in a nice field.
985 return super(MatrixEuclideanJordanAlgebra, self)._charpoly_coefficients()
986 else:
987 basis = ( (b/n) for (b,n) in zip(self.natural_basis(),
988 self._basis_normalizers) )
989
990 # Do this over the rationals and convert back at the end.
991 # Only works because we know the entries of the basis are
992 # integers.
993 J = MatrixEuclideanJordanAlgebra(QQ,
994 basis,
995 normalize_basis=False)
996 a = J._charpoly_coefficients()
997
998 # Unfortunately, changing the basis does change the
999 # coefficients of the characteristic polynomial, but since
1000 # these are really the coefficients of the "characteristic
1001 # polynomial of" function, everything is still nice and
1002 # unevaluated. It's therefore "obvious" how scaling the
1003 # basis affects the coordinate variables X1, X2, et
1004 # cetera. Scaling the first basis vector up by "n" adds a
1005 # factor of 1/n into every "X1" term, for example. So here
1006 # we simply undo the basis_normalizer scaling that we
1007 # performed earlier.
1008 #
1009 # The a[0] access here is safe because trivial algebras
1010 # won't have any basis normalizers and therefore won't
1011 # make it to this "else" branch.
1012 XS = a[0].parent().gens()
1013 subs_dict = { XS[i]: self._basis_normalizers[i]*XS[i]
1014 for i in range(len(XS)) }
1015 return tuple( a_i.subs(subs_dict) for a_i in a )
1016
1017
1018 @staticmethod
1019 def multiplication_table_from_matrix_basis(basis):
1020 """
1021 At least three of the five simple Euclidean Jordan algebras have the
1022 symmetric multiplication (A,B) |-> (AB + BA)/2, where the
1023 multiplication on the right is matrix multiplication. Given a basis
1024 for the underlying matrix space, this function returns a
1025 multiplication table (obtained by looping through the basis
1026 elements) for an algebra of those matrices.
1027 """
1028 # In S^2, for example, we nominally have four coordinates even
1029 # though the space is of dimension three only. The vector space V
1030 # is supposed to hold the entire long vector, and the subspace W
1031 # of V will be spanned by the vectors that arise from symmetric
1032 # matrices. Thus for S^2, dim(V) == 4 and dim(W) == 3.
1033 if len(basis) == 0:
1034 return []
1035
1036 field = basis[0].base_ring()
1037 dimension = basis[0].nrows()
1038
1039 V = VectorSpace(field, dimension**2)
1040 W = V.span_of_basis( _mat2vec(s) for s in basis )
1041 n = len(basis)
1042 mult_table = [[W.zero() for j in range(n)] for i in range(n)]
1043 for i in range(n):
1044 for j in range(n):
1045 mat_entry = (basis[i]*basis[j] + basis[j]*basis[i])/2
1046 mult_table[i][j] = W.coordinate_vector(_mat2vec(mat_entry))
1047
1048 return mult_table
1049
1050
1051 @staticmethod
1052 def real_embed(M):
1053 """
1054 Embed the matrix ``M`` into a space of real matrices.
1055
1056 The matrix ``M`` can have entries in any field at the moment:
1057 the real numbers, complex numbers, or quaternions. And although
1058 they are not a field, we can probably support octonions at some
1059 point, too. This function returns a real matrix that "acts like"
1060 the original with respect to matrix multiplication; i.e.
1061
1062 real_embed(M*N) = real_embed(M)*real_embed(N)
1063
1064 """
1065 raise NotImplementedError
1066
1067
1068 @staticmethod
1069 def real_unembed(M):
1070 """
1071 The inverse of :meth:`real_embed`.
1072 """
1073 raise NotImplementedError
1074
1075
1076 @classmethod
1077 def natural_inner_product(cls,X,Y):
1078 Xu = cls.real_unembed(X)
1079 Yu = cls.real_unembed(Y)
1080 tr = (Xu*Yu).trace()
1081
1082 if tr in RLF:
1083 # It's real already.
1084 return tr
1085
1086 # Otherwise, try the thing that works for complex numbers; and
1087 # if that doesn't work, the thing that works for quaternions.
1088 try:
1089 return tr.vector()[0] # real part, imag part is index 1
1090 except AttributeError:
1091 # A quaternions doesn't have a vector() method, but does
1092 # have coefficient_tuple() method that returns the
1093 # coefficients of 1, i, j, and k -- in that order.
1094 return tr.coefficient_tuple()[0]
1095
1096
1097 class RealMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
1098 @staticmethod
1099 def real_embed(M):
1100 """
1101 The identity function, for embedding real matrices into real
1102 matrices.
1103 """
1104 return M
1105
1106 @staticmethod
1107 def real_unembed(M):
1108 """
1109 The identity function, for unembedding real matrices from real
1110 matrices.
1111 """
1112 return M
1113
1114
1115 class RealSymmetricEJA(RealMatrixEuclideanJordanAlgebra):
1116 """
1117 The rank-n simple EJA consisting of real symmetric n-by-n
1118 matrices, the usual symmetric Jordan product, and the trace inner
1119 product. It has dimension `(n^2 + n)/2` over the reals.
1120
1121 SETUP::
1122
1123 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1124
1125 EXAMPLES::
1126
1127 sage: J = RealSymmetricEJA(2)
1128 sage: e0, e1, e2 = J.gens()
1129 sage: e0*e0
1130 e0
1131 sage: e1*e1
1132 1/2*e0 + 1/2*e2
1133 sage: e2*e2
1134 e2
1135
1136 In theory, our "field" can be any subfield of the reals::
1137
1138 sage: RealSymmetricEJA(2, RDF)
1139 Euclidean Jordan algebra of dimension 3 over Real Double Field
1140 sage: RealSymmetricEJA(2, RR)
1141 Euclidean Jordan algebra of dimension 3 over Real Field with
1142 53 bits of precision
1143
1144 TESTS:
1145
1146 The dimension of this algebra is `(n^2 + n) / 2`::
1147
1148 sage: set_random_seed()
1149 sage: n_max = RealSymmetricEJA._max_test_case_size()
1150 sage: n = ZZ.random_element(1, n_max)
1151 sage: J = RealSymmetricEJA(n)
1152 sage: J.dimension() == (n^2 + n)/2
1153 True
1154
1155 The Jordan multiplication is what we think it is::
1156
1157 sage: set_random_seed()
1158 sage: J = RealSymmetricEJA.random_instance()
1159 sage: x,y = J.random_elements(2)
1160 sage: actual = (x*y).natural_representation()
1161 sage: X = x.natural_representation()
1162 sage: Y = y.natural_representation()
1163 sage: expected = (X*Y + Y*X)/2
1164 sage: actual == expected
1165 True
1166 sage: J(expected) == x*y
1167 True
1168
1169 We can change the generator prefix::
1170
1171 sage: RealSymmetricEJA(3, prefix='q').gens()
1172 (q0, q1, q2, q3, q4, q5)
1173
1174 Our natural basis is normalized with respect to the natural inner
1175 product unless we specify otherwise::
1176
1177 sage: set_random_seed()
1178 sage: J = RealSymmetricEJA.random_instance()
1179 sage: all( b.norm() == 1 for b in J.gens() )
1180 True
1181
1182 Since our natural basis is normalized with respect to the natural
1183 inner product, and since we know that this algebra is an EJA, any
1184 left-multiplication operator's matrix will be symmetric because
1185 natural->EJA basis representation is an isometry and within the EJA
1186 the operator is self-adjoint by the Jordan axiom::
1187
1188 sage: set_random_seed()
1189 sage: x = RealSymmetricEJA.random_instance().random_element()
1190 sage: x.operator().matrix().is_symmetric()
1191 True
1192
1193 We can construct the (trivial) algebra of rank zero::
1194
1195 sage: RealSymmetricEJA(0)
1196 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
1197
1198 """
1199 @classmethod
1200 def _denormalized_basis(cls, n, field):
1201 """
1202 Return a basis for the space of real symmetric n-by-n matrices.
1203
1204 SETUP::
1205
1206 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1207
1208 TESTS::
1209
1210 sage: set_random_seed()
1211 sage: n = ZZ.random_element(1,5)
1212 sage: B = RealSymmetricEJA._denormalized_basis(n,QQ)
1213 sage: all( M.is_symmetric() for M in B)
1214 True
1215
1216 """
1217 # The basis of symmetric matrices, as matrices, in their R^(n-by-n)
1218 # coordinates.
1219 S = []
1220 for i in range(n):
1221 for j in range(i+1):
1222 Eij = matrix(field, n, lambda k,l: k==i and l==j)
1223 if i == j:
1224 Sij = Eij
1225 else:
1226 Sij = Eij + Eij.transpose()
1227 S.append(Sij)
1228 return S
1229
1230
1231 @staticmethod
1232 def _max_test_case_size():
1233 return 4 # Dimension 10
1234
1235
1236 def __init__(self, n, field=AA, **kwargs):
1237 basis = self._denormalized_basis(n, field)
1238 super(RealSymmetricEJA, self).__init__(field, basis, **kwargs)
1239 self.rank.set_cache(n)
1240
1241
1242 class ComplexMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
1243 @staticmethod
1244 def real_embed(M):
1245 """
1246 Embed the n-by-n complex matrix ``M`` into the space of real
1247 matrices of size 2n-by-2n via the map the sends each entry `z = a +
1248 bi` to the block matrix ``[[a,b],[-b,a]]``.
1249
1250 SETUP::
1251
1252 sage: from mjo.eja.eja_algebra import \
1253 ....: ComplexMatrixEuclideanJordanAlgebra
1254
1255 EXAMPLES::
1256
1257 sage: F = QuadraticField(-1, 'I')
1258 sage: x1 = F(4 - 2*i)
1259 sage: x2 = F(1 + 2*i)
1260 sage: x3 = F(-i)
1261 sage: x4 = F(6)
1262 sage: M = matrix(F,2,[[x1,x2],[x3,x4]])
1263 sage: ComplexMatrixEuclideanJordanAlgebra.real_embed(M)
1264 [ 4 -2| 1 2]
1265 [ 2 4|-2 1]
1266 [-----+-----]
1267 [ 0 -1| 6 0]
1268 [ 1 0| 0 6]
1269
1270 TESTS:
1271
1272 Embedding is a homomorphism (isomorphism, in fact)::
1273
1274 sage: set_random_seed()
1275 sage: n_max = ComplexMatrixEuclideanJordanAlgebra._max_test_case_size()
1276 sage: n = ZZ.random_element(n_max)
1277 sage: F = QuadraticField(-1, 'I')
1278 sage: X = random_matrix(F, n)
1279 sage: Y = random_matrix(F, n)
1280 sage: Xe = ComplexMatrixEuclideanJordanAlgebra.real_embed(X)
1281 sage: Ye = ComplexMatrixEuclideanJordanAlgebra.real_embed(Y)
1282 sage: XYe = ComplexMatrixEuclideanJordanAlgebra.real_embed(X*Y)
1283 sage: Xe*Ye == XYe
1284 True
1285
1286 """
1287 n = M.nrows()
1288 if M.ncols() != n:
1289 raise ValueError("the matrix 'M' must be square")
1290
1291 # We don't need any adjoined elements...
1292 field = M.base_ring().base_ring()
1293
1294 blocks = []
1295 for z in M.list():
1296 a = z.list()[0] # real part, I guess
1297 b = z.list()[1] # imag part, I guess
1298 blocks.append(matrix(field, 2, [[a,b],[-b,a]]))
1299
1300 return matrix.block(field, n, blocks)
1301
1302
1303 @staticmethod
1304 def real_unembed(M):
1305 """
1306 The inverse of _embed_complex_matrix().
1307
1308 SETUP::
1309
1310 sage: from mjo.eja.eja_algebra import \
1311 ....: ComplexMatrixEuclideanJordanAlgebra
1312
1313 EXAMPLES::
1314
1315 sage: A = matrix(QQ,[ [ 1, 2, 3, 4],
1316 ....: [-2, 1, -4, 3],
1317 ....: [ 9, 10, 11, 12],
1318 ....: [-10, 9, -12, 11] ])
1319 sage: ComplexMatrixEuclideanJordanAlgebra.real_unembed(A)
1320 [ 2*I + 1 4*I + 3]
1321 [ 10*I + 9 12*I + 11]
1322
1323 TESTS:
1324
1325 Unembedding is the inverse of embedding::
1326
1327 sage: set_random_seed()
1328 sage: F = QuadraticField(-1, 'I')
1329 sage: M = random_matrix(F, 3)
1330 sage: Me = ComplexMatrixEuclideanJordanAlgebra.real_embed(M)
1331 sage: ComplexMatrixEuclideanJordanAlgebra.real_unembed(Me) == M
1332 True
1333
1334 """
1335 n = ZZ(M.nrows())
1336 if M.ncols() != n:
1337 raise ValueError("the matrix 'M' must be square")
1338 if not n.mod(2).is_zero():
1339 raise ValueError("the matrix 'M' must be a complex embedding")
1340
1341 # If "M" was normalized, its base ring might have roots
1342 # adjoined and they can stick around after unembedding.
1343 field = M.base_ring()
1344 R = PolynomialRing(field, 'z')
1345 z = R.gen()
1346 if field is AA:
1347 # Sage doesn't know how to embed AA into QQbar, i.e. how
1348 # to adjoin sqrt(-1) to AA.
1349 F = QQbar
1350 else:
1351 F = field.extension(z**2 + 1, 'I', embedding=CLF(-1).sqrt())
1352 i = F.gen()
1353
1354 # Go top-left to bottom-right (reading order), converting every
1355 # 2-by-2 block we see to a single complex element.
1356 elements = []
1357 for k in range(n/2):
1358 for j in range(n/2):
1359 submat = M[2*k:2*k+2,2*j:2*j+2]
1360 if submat[0,0] != submat[1,1]:
1361 raise ValueError('bad on-diagonal submatrix')
1362 if submat[0,1] != -submat[1,0]:
1363 raise ValueError('bad off-diagonal submatrix')
1364 z = submat[0,0] + submat[0,1]*i
1365 elements.append(z)
1366
1367 return matrix(F, n/2, elements)
1368
1369
1370 @classmethod
1371 def natural_inner_product(cls,X,Y):
1372 """
1373 Compute a natural inner product in this algebra directly from
1374 its real embedding.
1375
1376 SETUP::
1377
1378 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1379
1380 TESTS:
1381
1382 This gives the same answer as the slow, default method implemented
1383 in :class:`MatrixEuclideanJordanAlgebra`::
1384
1385 sage: set_random_seed()
1386 sage: J = ComplexHermitianEJA.random_instance()
1387 sage: x,y = J.random_elements(2)
1388 sage: Xe = x.natural_representation()
1389 sage: Ye = y.natural_representation()
1390 sage: X = ComplexHermitianEJA.real_unembed(Xe)
1391 sage: Y = ComplexHermitianEJA.real_unembed(Ye)
1392 sage: expected = (X*Y).trace().real()
1393 sage: actual = ComplexHermitianEJA.natural_inner_product(Xe,Ye)
1394 sage: actual == expected
1395 True
1396
1397 """
1398 return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/2
1399
1400
1401 class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra):
1402 """
1403 The rank-n simple EJA consisting of complex Hermitian n-by-n
1404 matrices over the real numbers, the usual symmetric Jordan product,
1405 and the real-part-of-trace inner product. It has dimension `n^2` over
1406 the reals.
1407
1408 SETUP::
1409
1410 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1411
1412 EXAMPLES:
1413
1414 In theory, our "field" can be any subfield of the reals::
1415
1416 sage: ComplexHermitianEJA(2, RDF)
1417 Euclidean Jordan algebra of dimension 4 over Real Double Field
1418 sage: ComplexHermitianEJA(2, RR)
1419 Euclidean Jordan algebra of dimension 4 over Real Field with
1420 53 bits of precision
1421
1422 TESTS:
1423
1424 The dimension of this algebra is `n^2`::
1425
1426 sage: set_random_seed()
1427 sage: n_max = ComplexHermitianEJA._max_test_case_size()
1428 sage: n = ZZ.random_element(1, n_max)
1429 sage: J = ComplexHermitianEJA(n)
1430 sage: J.dimension() == n^2
1431 True
1432
1433 The Jordan multiplication is what we think it is::
1434
1435 sage: set_random_seed()
1436 sage: J = ComplexHermitianEJA.random_instance()
1437 sage: x,y = J.random_elements(2)
1438 sage: actual = (x*y).natural_representation()
1439 sage: X = x.natural_representation()
1440 sage: Y = y.natural_representation()
1441 sage: expected = (X*Y + Y*X)/2
1442 sage: actual == expected
1443 True
1444 sage: J(expected) == x*y
1445 True
1446
1447 We can change the generator prefix::
1448
1449 sage: ComplexHermitianEJA(2, prefix='z').gens()
1450 (z0, z1, z2, z3)
1451
1452 Our natural basis is normalized with respect to the natural inner
1453 product unless we specify otherwise::
1454
1455 sage: set_random_seed()
1456 sage: J = ComplexHermitianEJA.random_instance()
1457 sage: all( b.norm() == 1 for b in J.gens() )
1458 True
1459
1460 Since our natural basis is normalized with respect to the natural
1461 inner product, and since we know that this algebra is an EJA, any
1462 left-multiplication operator's matrix will be symmetric because
1463 natural->EJA basis representation is an isometry and within the EJA
1464 the operator is self-adjoint by the Jordan axiom::
1465
1466 sage: set_random_seed()
1467 sage: x = ComplexHermitianEJA.random_instance().random_element()
1468 sage: x.operator().matrix().is_symmetric()
1469 True
1470
1471 We can construct the (trivial) algebra of rank zero::
1472
1473 sage: ComplexHermitianEJA(0)
1474 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
1475
1476 """
1477
1478 @classmethod
1479 def _denormalized_basis(cls, n, field):
1480 """
1481 Returns a basis for the space of complex Hermitian n-by-n matrices.
1482
1483 Why do we embed these? Basically, because all of numerical linear
1484 algebra assumes that you're working with vectors consisting of `n`
1485 entries from a field and scalars from the same field. There's no way
1486 to tell SageMath that (for example) the vectors contain complex
1487 numbers, while the scalar field is real.
1488
1489 SETUP::
1490
1491 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1492
1493 TESTS::
1494
1495 sage: set_random_seed()
1496 sage: n = ZZ.random_element(1,5)
1497 sage: field = QuadraticField(2, 'sqrt2')
1498 sage: B = ComplexHermitianEJA._denormalized_basis(n, field)
1499 sage: all( M.is_symmetric() for M in B)
1500 True
1501
1502 """
1503 R = PolynomialRing(field, 'z')
1504 z = R.gen()
1505 F = field.extension(z**2 + 1, 'I')
1506 I = F.gen()
1507
1508 # This is like the symmetric case, but we need to be careful:
1509 #
1510 # * We want conjugate-symmetry, not just symmetry.
1511 # * The diagonal will (as a result) be real.
1512 #
1513 S = []
1514 for i in range(n):
1515 for j in range(i+1):
1516 Eij = matrix(F, n, lambda k,l: k==i and l==j)
1517 if i == j:
1518 Sij = cls.real_embed(Eij)
1519 S.append(Sij)
1520 else:
1521 # The second one has a minus because it's conjugated.
1522 Sij_real = cls.real_embed(Eij + Eij.transpose())
1523 S.append(Sij_real)
1524 Sij_imag = cls.real_embed(I*Eij - I*Eij.transpose())
1525 S.append(Sij_imag)
1526
1527 # Since we embedded these, we can drop back to the "field" that we
1528 # started with instead of the complex extension "F".
1529 return ( s.change_ring(field) for s in S )
1530
1531
1532 def __init__(self, n, field=AA, **kwargs):
1533 basis = self._denormalized_basis(n,field)
1534 super(ComplexHermitianEJA,self).__init__(field, basis, **kwargs)
1535 self.rank.set_cache(n)
1536
1537
1538 class QuaternionMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
1539 @staticmethod
1540 def real_embed(M):
1541 """
1542 Embed the n-by-n quaternion matrix ``M`` into the space of real
1543 matrices of size 4n-by-4n by first sending each quaternion entry `z
1544 = a + bi + cj + dk` to the block-complex matrix ``[[a + bi,
1545 c+di],[-c + di, a-bi]]`, and then embedding those into a real
1546 matrix.
1547
1548 SETUP::
1549
1550 sage: from mjo.eja.eja_algebra import \
1551 ....: QuaternionMatrixEuclideanJordanAlgebra
1552
1553 EXAMPLES::
1554
1555 sage: Q = QuaternionAlgebra(QQ,-1,-1)
1556 sage: i,j,k = Q.gens()
1557 sage: x = 1 + 2*i + 3*j + 4*k
1558 sage: M = matrix(Q, 1, [[x]])
1559 sage: QuaternionMatrixEuclideanJordanAlgebra.real_embed(M)
1560 [ 1 2 3 4]
1561 [-2 1 -4 3]
1562 [-3 4 1 -2]
1563 [-4 -3 2 1]
1564
1565 Embedding is a homomorphism (isomorphism, in fact)::
1566
1567 sage: set_random_seed()
1568 sage: n_max = QuaternionMatrixEuclideanJordanAlgebra._max_test_case_size()
1569 sage: n = ZZ.random_element(n_max)
1570 sage: Q = QuaternionAlgebra(QQ,-1,-1)
1571 sage: X = random_matrix(Q, n)
1572 sage: Y = random_matrix(Q, n)
1573 sage: Xe = QuaternionMatrixEuclideanJordanAlgebra.real_embed(X)
1574 sage: Ye = QuaternionMatrixEuclideanJordanAlgebra.real_embed(Y)
1575 sage: XYe = QuaternionMatrixEuclideanJordanAlgebra.real_embed(X*Y)
1576 sage: Xe*Ye == XYe
1577 True
1578
1579 """
1580 quaternions = M.base_ring()
1581 n = M.nrows()
1582 if M.ncols() != n:
1583 raise ValueError("the matrix 'M' must be square")
1584
1585 F = QuadraticField(-1, 'I')
1586 i = F.gen()
1587
1588 blocks = []
1589 for z in M.list():
1590 t = z.coefficient_tuple()
1591 a = t[0]
1592 b = t[1]
1593 c = t[2]
1594 d = t[3]
1595 cplxM = matrix(F, 2, [[ a + b*i, c + d*i],
1596 [-c + d*i, a - b*i]])
1597 realM = ComplexMatrixEuclideanJordanAlgebra.real_embed(cplxM)
1598 blocks.append(realM)
1599
1600 # We should have real entries by now, so use the realest field
1601 # we've got for the return value.
1602 return matrix.block(quaternions.base_ring(), n, blocks)
1603
1604
1605
1606 @staticmethod
1607 def real_unembed(M):
1608 """
1609 The inverse of _embed_quaternion_matrix().
1610
1611 SETUP::
1612
1613 sage: from mjo.eja.eja_algebra import \
1614 ....: QuaternionMatrixEuclideanJordanAlgebra
1615
1616 EXAMPLES::
1617
1618 sage: M = matrix(QQ, [[ 1, 2, 3, 4],
1619 ....: [-2, 1, -4, 3],
1620 ....: [-3, 4, 1, -2],
1621 ....: [-4, -3, 2, 1]])
1622 sage: QuaternionMatrixEuclideanJordanAlgebra.real_unembed(M)
1623 [1 + 2*i + 3*j + 4*k]
1624
1625 TESTS:
1626
1627 Unembedding is the inverse of embedding::
1628
1629 sage: set_random_seed()
1630 sage: Q = QuaternionAlgebra(QQ, -1, -1)
1631 sage: M = random_matrix(Q, 3)
1632 sage: Me = QuaternionMatrixEuclideanJordanAlgebra.real_embed(M)
1633 sage: QuaternionMatrixEuclideanJordanAlgebra.real_unembed(Me) == M
1634 True
1635
1636 """
1637 n = ZZ(M.nrows())
1638 if M.ncols() != n:
1639 raise ValueError("the matrix 'M' must be square")
1640 if not n.mod(4).is_zero():
1641 raise ValueError("the matrix 'M' must be a quaternion embedding")
1642
1643 # Use the base ring of the matrix to ensure that its entries can be
1644 # multiplied by elements of the quaternion algebra.
1645 field = M.base_ring()
1646 Q = QuaternionAlgebra(field,-1,-1)
1647 i,j,k = Q.gens()
1648
1649 # Go top-left to bottom-right (reading order), converting every
1650 # 4-by-4 block we see to a 2-by-2 complex block, to a 1-by-1
1651 # quaternion block.
1652 elements = []
1653 for l in range(n/4):
1654 for m in range(n/4):
1655 submat = ComplexMatrixEuclideanJordanAlgebra.real_unembed(
1656 M[4*l:4*l+4,4*m:4*m+4] )
1657 if submat[0,0] != submat[1,1].conjugate():
1658 raise ValueError('bad on-diagonal submatrix')
1659 if submat[0,1] != -submat[1,0].conjugate():
1660 raise ValueError('bad off-diagonal submatrix')
1661 z = submat[0,0].real()
1662 z += submat[0,0].imag()*i
1663 z += submat[0,1].real()*j
1664 z += submat[0,1].imag()*k
1665 elements.append(z)
1666
1667 return matrix(Q, n/4, elements)
1668
1669
1670 @classmethod
1671 def natural_inner_product(cls,X,Y):
1672 """
1673 Compute a natural inner product in this algebra directly from
1674 its real embedding.
1675
1676 SETUP::
1677
1678 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
1679
1680 TESTS:
1681
1682 This gives the same answer as the slow, default method implemented
1683 in :class:`MatrixEuclideanJordanAlgebra`::
1684
1685 sage: set_random_seed()
1686 sage: J = QuaternionHermitianEJA.random_instance()
1687 sage: x,y = J.random_elements(2)
1688 sage: Xe = x.natural_representation()
1689 sage: Ye = y.natural_representation()
1690 sage: X = QuaternionHermitianEJA.real_unembed(Xe)
1691 sage: Y = QuaternionHermitianEJA.real_unembed(Ye)
1692 sage: expected = (X*Y).trace().coefficient_tuple()[0]
1693 sage: actual = QuaternionHermitianEJA.natural_inner_product(Xe,Ye)
1694 sage: actual == expected
1695 True
1696
1697 """
1698 return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/4
1699
1700
1701 class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra):
1702 """
1703 The rank-n simple EJA consisting of self-adjoint n-by-n quaternion
1704 matrices, the usual symmetric Jordan product, and the
1705 real-part-of-trace inner product. It has dimension `2n^2 - n` over
1706 the reals.
1707
1708 SETUP::
1709
1710 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
1711
1712 EXAMPLES:
1713
1714 In theory, our "field" can be any subfield of the reals::
1715
1716 sage: QuaternionHermitianEJA(2, RDF)
1717 Euclidean Jordan algebra of dimension 6 over Real Double Field
1718 sage: QuaternionHermitianEJA(2, RR)
1719 Euclidean Jordan algebra of dimension 6 over Real Field with
1720 53 bits of precision
1721
1722 TESTS:
1723
1724 The dimension of this algebra is `2*n^2 - n`::
1725
1726 sage: set_random_seed()
1727 sage: n_max = QuaternionHermitianEJA._max_test_case_size()
1728 sage: n = ZZ.random_element(1, n_max)
1729 sage: J = QuaternionHermitianEJA(n)
1730 sage: J.dimension() == 2*(n^2) - n
1731 True
1732
1733 The Jordan multiplication is what we think it is::
1734
1735 sage: set_random_seed()
1736 sage: J = QuaternionHermitianEJA.random_instance()
1737 sage: x,y = J.random_elements(2)
1738 sage: actual = (x*y).natural_representation()
1739 sage: X = x.natural_representation()
1740 sage: Y = y.natural_representation()
1741 sage: expected = (X*Y + Y*X)/2
1742 sage: actual == expected
1743 True
1744 sage: J(expected) == x*y
1745 True
1746
1747 We can change the generator prefix::
1748
1749 sage: QuaternionHermitianEJA(2, prefix='a').gens()
1750 (a0, a1, a2, a3, a4, a5)
1751
1752 Our natural basis is normalized with respect to the natural inner
1753 product unless we specify otherwise::
1754
1755 sage: set_random_seed()
1756 sage: J = QuaternionHermitianEJA.random_instance()
1757 sage: all( b.norm() == 1 for b in J.gens() )
1758 True
1759
1760 Since our natural basis is normalized with respect to the natural
1761 inner product, and since we know that this algebra is an EJA, any
1762 left-multiplication operator's matrix will be symmetric because
1763 natural->EJA basis representation is an isometry and within the EJA
1764 the operator is self-adjoint by the Jordan axiom::
1765
1766 sage: set_random_seed()
1767 sage: x = QuaternionHermitianEJA.random_instance().random_element()
1768 sage: x.operator().matrix().is_symmetric()
1769 True
1770
1771 We can construct the (trivial) algebra of rank zero::
1772
1773 sage: QuaternionHermitianEJA(0)
1774 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
1775
1776 """
1777 @classmethod
1778 def _denormalized_basis(cls, n, field):
1779 """
1780 Returns a basis for the space of quaternion Hermitian n-by-n matrices.
1781
1782 Why do we embed these? Basically, because all of numerical
1783 linear algebra assumes that you're working with vectors consisting
1784 of `n` entries from a field and scalars from the same field. There's
1785 no way to tell SageMath that (for example) the vectors contain
1786 complex numbers, while the scalar field is real.
1787
1788 SETUP::
1789
1790 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
1791
1792 TESTS::
1793
1794 sage: set_random_seed()
1795 sage: n = ZZ.random_element(1,5)
1796 sage: B = QuaternionHermitianEJA._denormalized_basis(n,QQ)
1797 sage: all( M.is_symmetric() for M in B )
1798 True
1799
1800 """
1801 Q = QuaternionAlgebra(QQ,-1,-1)
1802 I,J,K = Q.gens()
1803
1804 # This is like the symmetric case, but we need to be careful:
1805 #
1806 # * We want conjugate-symmetry, not just symmetry.
1807 # * The diagonal will (as a result) be real.
1808 #
1809 S = []
1810 for i in range(n):
1811 for j in range(i+1):
1812 Eij = matrix(Q, n, lambda k,l: k==i and l==j)
1813 if i == j:
1814 Sij = cls.real_embed(Eij)
1815 S.append(Sij)
1816 else:
1817 # The second, third, and fourth ones have a minus
1818 # because they're conjugated.
1819 Sij_real = cls.real_embed(Eij + Eij.transpose())
1820 S.append(Sij_real)
1821 Sij_I = cls.real_embed(I*Eij - I*Eij.transpose())
1822 S.append(Sij_I)
1823 Sij_J = cls.real_embed(J*Eij - J*Eij.transpose())
1824 S.append(Sij_J)
1825 Sij_K = cls.real_embed(K*Eij - K*Eij.transpose())
1826 S.append(Sij_K)
1827
1828 # Since we embedded these, we can drop back to the "field" that we
1829 # started with instead of the quaternion algebra "Q".
1830 return ( s.change_ring(field) for s in S )
1831
1832
1833 def __init__(self, n, field=AA, **kwargs):
1834 basis = self._denormalized_basis(n,field)
1835 super(QuaternionHermitianEJA,self).__init__(field, basis, **kwargs)
1836 self.rank.set_cache(n)
1837
1838
1839 class BilinearFormEJA(FiniteDimensionalEuclideanJordanAlgebra):
1840 r"""
1841 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
1842 with the half-trace inner product and jordan product ``x*y =
1843 (x0*y0 + <B*x_bar,y_bar>, x0*y_bar + y0*x_bar)`` where ``B`` is a
1844 symmetric positive-definite "bilinear form" matrix. It has
1845 dimension `n` over the reals, and reduces to the ``JordanSpinEJA``
1846 when ``B`` is the identity matrix of order ``n-1``.
1847
1848 SETUP::
1849
1850 sage: from mjo.eja.eja_algebra import (BilinearFormEJA,
1851 ....: JordanSpinEJA)
1852
1853 EXAMPLES:
1854
1855 When no bilinear form is specified, the identity matrix is used,
1856 and the resulting algebra is the Jordan spin algebra::
1857
1858 sage: J0 = BilinearFormEJA(3)
1859 sage: J1 = JordanSpinEJA(3)
1860 sage: J0.multiplication_table() == J0.multiplication_table()
1861 True
1862
1863 TESTS:
1864
1865 We can create a zero-dimensional algebra::
1866
1867 sage: J = BilinearFormEJA(0)
1868 sage: J.basis()
1869 Finite family {}
1870
1871 We can check the multiplication condition given in the Jordan, von
1872 Neumann, and Wigner paper (and also discussed on my "On the
1873 symmetry..." paper). Note that this relies heavily on the standard
1874 choice of basis, as does anything utilizing the bilinear form matrix::
1875
1876 sage: set_random_seed()
1877 sage: n = ZZ.random_element(5)
1878 sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular')
1879 sage: B = M.transpose()*M
1880 sage: J = BilinearFormEJA(n, B=B)
1881 sage: eis = VectorSpace(M.base_ring(), M.ncols()).basis()
1882 sage: V = J.vector_space()
1883 sage: sis = [ J.from_vector(V([0] + (M.inverse()*ei).list()))
1884 ....: for ei in eis ]
1885 sage: actual = [ sis[i]*sis[j]
1886 ....: for i in range(n-1)
1887 ....: for j in range(n-1) ]
1888 sage: expected = [ J.one() if i == j else J.zero()
1889 ....: for i in range(n-1)
1890 ....: for j in range(n-1) ]
1891 sage: actual == expected
1892 True
1893 """
1894 def __init__(self, n, field=AA, B=None, **kwargs):
1895 if B is None:
1896 self._B = matrix.identity(field, max(0,n-1))
1897 else:
1898 self._B = B
1899
1900 V = VectorSpace(field, n)
1901 mult_table = [[V.zero() for j in range(n)] for i in range(n)]
1902 for i in range(n):
1903 for j in range(n):
1904 x = V.gen(i)
1905 y = V.gen(j)
1906 x0 = x[0]
1907 xbar = x[1:]
1908 y0 = y[0]
1909 ybar = y[1:]
1910 z0 = x0*y0 + (self._B*xbar).inner_product(ybar)
1911 zbar = y0*xbar + x0*ybar
1912 z = V([z0] + zbar.list())
1913 mult_table[i][j] = z
1914
1915 # The rank of this algebra is two, unless we're in a
1916 # one-dimensional ambient space (because the rank is bounded
1917 # by the ambient dimension).
1918 fdeja = super(BilinearFormEJA, self)
1919 fdeja.__init__(field, mult_table, **kwargs)
1920 self.rank.set_cache(min(n,2))
1921
1922 def inner_product(self, x, y):
1923 r"""
1924 Half of the trace inner product.
1925
1926 This is defined so that the special case of the Jordan spin
1927 algebra gets the usual inner product.
1928
1929 SETUP::
1930
1931 sage: from mjo.eja.eja_algebra import BilinearFormEJA
1932
1933 TESTS:
1934
1935 Ensure that this is one-half of the trace inner-product when
1936 the algebra isn't just the reals (when ``n`` isn't one). This
1937 is in Faraut and Koranyi, and also my "On the symmetry..."
1938 paper::
1939
1940 sage: set_random_seed()
1941 sage: n = ZZ.random_element(2,5)
1942 sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular')
1943 sage: B = M.transpose()*M
1944 sage: J = BilinearFormEJA(n, B=B)
1945 sage: x = J.random_element()
1946 sage: y = J.random_element()
1947 sage: x.inner_product(y) == (x*y).trace()/2
1948 True
1949
1950 """
1951 xvec = x.to_vector()
1952 xbar = xvec[1:]
1953 yvec = y.to_vector()
1954 ybar = yvec[1:]
1955 return x[0]*y[0] + (self._B*xbar).inner_product(ybar)
1956
1957
1958 class JordanSpinEJA(BilinearFormEJA):
1959 """
1960 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
1961 with the usual inner product and jordan product ``x*y =
1962 (<x,y>, x0*y_bar + y0*x_bar)``. It has dimension `n` over
1963 the reals.
1964
1965 SETUP::
1966
1967 sage: from mjo.eja.eja_algebra import JordanSpinEJA
1968
1969 EXAMPLES:
1970
1971 This multiplication table can be verified by hand::
1972
1973 sage: J = JordanSpinEJA(4)
1974 sage: e0,e1,e2,e3 = J.gens()
1975 sage: e0*e0
1976 e0
1977 sage: e0*e1
1978 e1
1979 sage: e0*e2
1980 e2
1981 sage: e0*e3
1982 e3
1983 sage: e1*e2
1984 0
1985 sage: e1*e3
1986 0
1987 sage: e2*e3
1988 0
1989
1990 We can change the generator prefix::
1991
1992 sage: JordanSpinEJA(2, prefix='B').gens()
1993 (B0, B1)
1994
1995 TESTS:
1996
1997 Ensure that we have the usual inner product on `R^n`::
1998
1999 sage: set_random_seed()
2000 sage: J = JordanSpinEJA.random_instance()
2001 sage: x,y = J.random_elements(2)
2002 sage: X = x.natural_representation()
2003 sage: Y = y.natural_representation()
2004 sage: x.inner_product(y) == J.natural_inner_product(X,Y)
2005 True
2006
2007 """
2008 def __init__(self, n, field=AA, **kwargs):
2009 # This is a special case of the BilinearFormEJA with the identity
2010 # matrix as its bilinear form.
2011 return super(JordanSpinEJA, self).__init__(n, field, **kwargs)
2012
2013
2014 class TrivialEJA(FiniteDimensionalEuclideanJordanAlgebra):
2015 """
2016 The trivial Euclidean Jordan algebra consisting of only a zero element.
2017
2018 SETUP::
2019
2020 sage: from mjo.eja.eja_algebra import TrivialEJA
2021
2022 EXAMPLES::
2023
2024 sage: J = TrivialEJA()
2025 sage: J.dimension()
2026 0
2027 sage: J.zero()
2028 0
2029 sage: J.one()
2030 0
2031 sage: 7*J.one()*12*J.one()
2032 0
2033 sage: J.one().inner_product(J.one())
2034 0
2035 sage: J.one().norm()
2036 0
2037 sage: J.one().subalgebra_generated_by()
2038 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2039 sage: J.rank()
2040 0
2041
2042 """
2043 def __init__(self, field=AA, **kwargs):
2044 mult_table = []
2045 fdeja = super(TrivialEJA, self)
2046 # The rank is zero using my definition, namely the dimension of the
2047 # largest subalgebra generated by any element.
2048 fdeja.__init__(field, mult_table, **kwargs)
2049 self.rank.set_cache(0)