]> gitweb.michael.orlitzky.com - sage.d.git/blob - mjo/eja/eja_algebra.py
eja: remove more vestigial charpoly stuff.
[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 # Compute an extra power in case the rank is equal to
692 # the dimension (otherwise, we would stop at x^(r-1)).
693 x_powers = [ (L_x**k)*self.one().to_vector()
694 for k in range(n+1) ]
695 A = matrix.column(F, x_powers[:n])
696 AE = A.extended_echelon_form()
697 E = AE[:,n:]
698 A_rref = AE[:,:n]
699 r = A_rref.rank()
700 b = x_powers[r]
701
702 # The theory says that only the first "r" coefficients are
703 # nonzero, and they actually live in the original polynomial
704 # ring and not the fraction field. We negate them because
705 # in the actual characteristic polynomial, they get moved
706 # to the other side where x^r lives.
707 return -A_rref.solve_right(E*b).change_ring(R)[:r]
708
709 @cached_method
710 def rank(self):
711 r"""
712 Return the rank of this EJA.
713
714 This is a cached method because we know the rank a priori for
715 all of the algebras we can construct. Thus we can avoid the
716 expensive ``_charpoly_coefficients()`` call unless we truly
717 need to compute the whole characteristic polynomial.
718
719 SETUP::
720
721 sage: from mjo.eja.eja_algebra import (HadamardEJA,
722 ....: JordanSpinEJA,
723 ....: RealSymmetricEJA,
724 ....: ComplexHermitianEJA,
725 ....: QuaternionHermitianEJA,
726 ....: random_eja)
727
728 EXAMPLES:
729
730 The rank of the Jordan spin algebra is always two::
731
732 sage: JordanSpinEJA(2).rank()
733 2
734 sage: JordanSpinEJA(3).rank()
735 2
736 sage: JordanSpinEJA(4).rank()
737 2
738
739 The rank of the `n`-by-`n` Hermitian real, complex, or
740 quaternion matrices is `n`::
741
742 sage: RealSymmetricEJA(4).rank()
743 4
744 sage: ComplexHermitianEJA(3).rank()
745 3
746 sage: QuaternionHermitianEJA(2).rank()
747 2
748
749 TESTS:
750
751 Ensure that every EJA that we know how to construct has a
752 positive integer rank, unless the algebra is trivial in
753 which case its rank will be zero::
754
755 sage: set_random_seed()
756 sage: J = random_eja()
757 sage: r = J.rank()
758 sage: r in ZZ
759 True
760 sage: r > 0 or (r == 0 and J.is_trivial())
761 True
762
763 Ensure that computing the rank actually works, since the ranks
764 of all simple algebras are known and will be cached by default::
765
766 sage: J = HadamardEJA(4)
767 sage: J.rank.clear_cache()
768 sage: J.rank()
769 4
770
771 ::
772
773 sage: J = JordanSpinEJA(4)
774 sage: J.rank.clear_cache()
775 sage: J.rank()
776 2
777
778 ::
779
780 sage: J = RealSymmetricEJA(3)
781 sage: J.rank.clear_cache()
782 sage: J.rank()
783 3
784
785 ::
786
787 sage: J = ComplexHermitianEJA(2)
788 sage: J.rank.clear_cache()
789 sage: J.rank()
790 2
791
792 ::
793
794 sage: J = QuaternionHermitianEJA(2)
795 sage: J.rank.clear_cache()
796 sage: J.rank()
797 2
798 """
799 return len(self._charpoly_coefficients())
800
801
802 def vector_space(self):
803 """
804 Return the vector space that underlies this algebra.
805
806 SETUP::
807
808 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
809
810 EXAMPLES::
811
812 sage: J = RealSymmetricEJA(2)
813 sage: J.vector_space()
814 Vector space of dimension 3 over...
815
816 """
817 return self.zero().to_vector().parent().ambient_vector_space()
818
819
820 Element = FiniteDimensionalEuclideanJordanAlgebraElement
821
822
823 class HadamardEJA(FiniteDimensionalEuclideanJordanAlgebra):
824 """
825 Return the Euclidean Jordan Algebra corresponding to the set
826 `R^n` under the Hadamard product.
827
828 Note: this is nothing more than the Cartesian product of ``n``
829 copies of the spin algebra. Once Cartesian product algebras
830 are implemented, this can go.
831
832 SETUP::
833
834 sage: from mjo.eja.eja_algebra import HadamardEJA
835
836 EXAMPLES:
837
838 This multiplication table can be verified by hand::
839
840 sage: J = HadamardEJA(3)
841 sage: e0,e1,e2 = J.gens()
842 sage: e0*e0
843 e0
844 sage: e0*e1
845 0
846 sage: e0*e2
847 0
848 sage: e1*e1
849 e1
850 sage: e1*e2
851 0
852 sage: e2*e2
853 e2
854
855 TESTS:
856
857 We can change the generator prefix::
858
859 sage: HadamardEJA(3, prefix='r').gens()
860 (r0, r1, r2)
861
862 """
863 def __init__(self, n, field=AA, **kwargs):
864 V = VectorSpace(field, n)
865 mult_table = [ [ V.gen(i)*(i == j) for j in range(n) ]
866 for i in range(n) ]
867
868 fdeja = super(HadamardEJA, self)
869 fdeja.__init__(field, mult_table, **kwargs)
870 self.rank.set_cache(n)
871
872 def inner_product(self, x, y):
873 """
874 Faster to reimplement than to use natural representations.
875
876 SETUP::
877
878 sage: from mjo.eja.eja_algebra import HadamardEJA
879
880 TESTS:
881
882 Ensure that this is the usual inner product for the algebras
883 over `R^n`::
884
885 sage: set_random_seed()
886 sage: J = HadamardEJA.random_instance()
887 sage: x,y = J.random_elements(2)
888 sage: X = x.natural_representation()
889 sage: Y = y.natural_representation()
890 sage: x.inner_product(y) == J.natural_inner_product(X,Y)
891 True
892
893 """
894 return x.to_vector().inner_product(y.to_vector())
895
896
897 def random_eja(field=AA, nontrivial=False):
898 """
899 Return a "random" finite-dimensional Euclidean Jordan Algebra.
900
901 SETUP::
902
903 sage: from mjo.eja.eja_algebra import random_eja
904
905 TESTS::
906
907 sage: random_eja()
908 Euclidean Jordan algebra of dimension...
909
910 """
911 eja_classes = [HadamardEJA,
912 JordanSpinEJA,
913 RealSymmetricEJA,
914 ComplexHermitianEJA,
915 QuaternionHermitianEJA]
916 if not nontrivial:
917 eja_classes.append(TrivialEJA)
918 classname = choice(eja_classes)
919 return classname.random_instance(field=field)
920
921
922
923
924
925
926 class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
927 @staticmethod
928 def _max_test_case_size():
929 # Play it safe, since this will be squared and the underlying
930 # field can have dimension 4 (quaternions) too.
931 return 2
932
933 def __init__(self, field, basis, normalize_basis=True, **kwargs):
934 """
935 Compared to the superclass constructor, we take a basis instead of
936 a multiplication table because the latter can be computed in terms
937 of the former when the product is known (like it is here).
938 """
939 # Used in this class's fast _charpoly_coefficients() override.
940 self._basis_normalizers = None
941
942 # We're going to loop through this a few times, so now's a good
943 # time to ensure that it isn't a generator expression.
944 basis = tuple(basis)
945
946 if len(basis) > 1 and normalize_basis:
947 # We'll need sqrt(2) to normalize the basis, and this
948 # winds up in the multiplication table, so the whole
949 # algebra needs to be over the field extension.
950 R = PolynomialRing(field, 'z')
951 z = R.gen()
952 p = z**2 - 2
953 if p.is_irreducible():
954 field = field.extension(p, 'sqrt2', embedding=RLF(2).sqrt())
955 basis = tuple( s.change_ring(field) for s in basis )
956 self._basis_normalizers = tuple(
957 ~(self.natural_inner_product(s,s).sqrt()) for s in basis )
958 basis = tuple(s*c for (s,c) in zip(basis,self._basis_normalizers))
959
960 Qs = self.multiplication_table_from_matrix_basis(basis)
961
962 fdeja = super(MatrixEuclideanJordanAlgebra, self)
963 fdeja.__init__(field, Qs, natural_basis=basis, **kwargs)
964 return
965
966
967 @cached_method
968 def _charpoly_coefficients(self):
969 r"""
970 Override the parent method with something that tries to compute
971 over a faster (non-extension) field.
972 """
973 if self._basis_normalizers is None:
974 # We didn't normalize, so assume that the basis we started
975 # with had entries in a nice field.
976 return super(MatrixEuclideanJordanAlgebra, self)._charpoly_coefficients()
977 else:
978 basis = ( (b/n) for (b,n) in zip(self.natural_basis(),
979 self._basis_normalizers) )
980
981 # Do this over the rationals and convert back at the end.
982 # Only works because we know the entries of the basis are
983 # integers.
984 J = MatrixEuclideanJordanAlgebra(QQ,
985 basis,
986 normalize_basis=False)
987 return J._charpoly_coefficients()
988
989
990 @staticmethod
991 def multiplication_table_from_matrix_basis(basis):
992 """
993 At least three of the five simple Euclidean Jordan algebras have the
994 symmetric multiplication (A,B) |-> (AB + BA)/2, where the
995 multiplication on the right is matrix multiplication. Given a basis
996 for the underlying matrix space, this function returns a
997 multiplication table (obtained by looping through the basis
998 elements) for an algebra of those matrices.
999 """
1000 # In S^2, for example, we nominally have four coordinates even
1001 # though the space is of dimension three only. The vector space V
1002 # is supposed to hold the entire long vector, and the subspace W
1003 # of V will be spanned by the vectors that arise from symmetric
1004 # matrices. Thus for S^2, dim(V) == 4 and dim(W) == 3.
1005 field = basis[0].base_ring()
1006 dimension = basis[0].nrows()
1007
1008 V = VectorSpace(field, dimension**2)
1009 W = V.span_of_basis( _mat2vec(s) for s in basis )
1010 n = len(basis)
1011 mult_table = [[W.zero() for j in range(n)] for i in range(n)]
1012 for i in range(n):
1013 for j in range(n):
1014 mat_entry = (basis[i]*basis[j] + basis[j]*basis[i])/2
1015 mult_table[i][j] = W.coordinate_vector(_mat2vec(mat_entry))
1016
1017 return mult_table
1018
1019
1020 @staticmethod
1021 def real_embed(M):
1022 """
1023 Embed the matrix ``M`` into a space of real matrices.
1024
1025 The matrix ``M`` can have entries in any field at the moment:
1026 the real numbers, complex numbers, or quaternions. And although
1027 they are not a field, we can probably support octonions at some
1028 point, too. This function returns a real matrix that "acts like"
1029 the original with respect to matrix multiplication; i.e.
1030
1031 real_embed(M*N) = real_embed(M)*real_embed(N)
1032
1033 """
1034 raise NotImplementedError
1035
1036
1037 @staticmethod
1038 def real_unembed(M):
1039 """
1040 The inverse of :meth:`real_embed`.
1041 """
1042 raise NotImplementedError
1043
1044
1045 @classmethod
1046 def natural_inner_product(cls,X,Y):
1047 Xu = cls.real_unembed(X)
1048 Yu = cls.real_unembed(Y)
1049 tr = (Xu*Yu).trace()
1050
1051 if tr in RLF:
1052 # It's real already.
1053 return tr
1054
1055 # Otherwise, try the thing that works for complex numbers; and
1056 # if that doesn't work, the thing that works for quaternions.
1057 try:
1058 return tr.vector()[0] # real part, imag part is index 1
1059 except AttributeError:
1060 # A quaternions doesn't have a vector() method, but does
1061 # have coefficient_tuple() method that returns the
1062 # coefficients of 1, i, j, and k -- in that order.
1063 return tr.coefficient_tuple()[0]
1064
1065
1066 class RealMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
1067 @staticmethod
1068 def real_embed(M):
1069 """
1070 The identity function, for embedding real matrices into real
1071 matrices.
1072 """
1073 return M
1074
1075 @staticmethod
1076 def real_unembed(M):
1077 """
1078 The identity function, for unembedding real matrices from real
1079 matrices.
1080 """
1081 return M
1082
1083
1084 class RealSymmetricEJA(RealMatrixEuclideanJordanAlgebra):
1085 """
1086 The rank-n simple EJA consisting of real symmetric n-by-n
1087 matrices, the usual symmetric Jordan product, and the trace inner
1088 product. It has dimension `(n^2 + n)/2` over the reals.
1089
1090 SETUP::
1091
1092 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1093
1094 EXAMPLES::
1095
1096 sage: J = RealSymmetricEJA(2)
1097 sage: e0, e1, e2 = J.gens()
1098 sage: e0*e0
1099 e0
1100 sage: e1*e1
1101 1/2*e0 + 1/2*e2
1102 sage: e2*e2
1103 e2
1104
1105 In theory, our "field" can be any subfield of the reals::
1106
1107 sage: RealSymmetricEJA(2, RDF)
1108 Euclidean Jordan algebra of dimension 3 over Real Double Field
1109 sage: RealSymmetricEJA(2, RR)
1110 Euclidean Jordan algebra of dimension 3 over Real Field with
1111 53 bits of precision
1112
1113 TESTS:
1114
1115 The dimension of this algebra is `(n^2 + n) / 2`::
1116
1117 sage: set_random_seed()
1118 sage: n_max = RealSymmetricEJA._max_test_case_size()
1119 sage: n = ZZ.random_element(1, n_max)
1120 sage: J = RealSymmetricEJA(n)
1121 sage: J.dimension() == (n^2 + n)/2
1122 True
1123
1124 The Jordan multiplication is what we think it is::
1125
1126 sage: set_random_seed()
1127 sage: J = RealSymmetricEJA.random_instance()
1128 sage: x,y = J.random_elements(2)
1129 sage: actual = (x*y).natural_representation()
1130 sage: X = x.natural_representation()
1131 sage: Y = y.natural_representation()
1132 sage: expected = (X*Y + Y*X)/2
1133 sage: actual == expected
1134 True
1135 sage: J(expected) == x*y
1136 True
1137
1138 We can change the generator prefix::
1139
1140 sage: RealSymmetricEJA(3, prefix='q').gens()
1141 (q0, q1, q2, q3, q4, q5)
1142
1143 Our natural basis is normalized with respect to the natural inner
1144 product unless we specify otherwise::
1145
1146 sage: set_random_seed()
1147 sage: J = RealSymmetricEJA.random_instance()
1148 sage: all( b.norm() == 1 for b in J.gens() )
1149 True
1150
1151 Since our natural basis is normalized with respect to the natural
1152 inner product, and since we know that this algebra is an EJA, any
1153 left-multiplication operator's matrix will be symmetric because
1154 natural->EJA basis representation is an isometry and within the EJA
1155 the operator is self-adjoint by the Jordan axiom::
1156
1157 sage: set_random_seed()
1158 sage: x = RealSymmetricEJA.random_instance().random_element()
1159 sage: x.operator().matrix().is_symmetric()
1160 True
1161
1162 """
1163 @classmethod
1164 def _denormalized_basis(cls, n, field):
1165 """
1166 Return a basis for the space of real symmetric n-by-n matrices.
1167
1168 SETUP::
1169
1170 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1171
1172 TESTS::
1173
1174 sage: set_random_seed()
1175 sage: n = ZZ.random_element(1,5)
1176 sage: B = RealSymmetricEJA._denormalized_basis(n,QQ)
1177 sage: all( M.is_symmetric() for M in B)
1178 True
1179
1180 """
1181 # The basis of symmetric matrices, as matrices, in their R^(n-by-n)
1182 # coordinates.
1183 S = []
1184 for i in range(n):
1185 for j in range(i+1):
1186 Eij = matrix(field, n, lambda k,l: k==i and l==j)
1187 if i == j:
1188 Sij = Eij
1189 else:
1190 Sij = Eij + Eij.transpose()
1191 S.append(Sij)
1192 return S
1193
1194
1195 @staticmethod
1196 def _max_test_case_size():
1197 return 4 # Dimension 10
1198
1199
1200 def __init__(self, n, field=AA, **kwargs):
1201 basis = self._denormalized_basis(n, field)
1202 super(RealSymmetricEJA, self).__init__(field, basis, **kwargs)
1203 self.rank.set_cache(n)
1204
1205
1206 class ComplexMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
1207 @staticmethod
1208 def real_embed(M):
1209 """
1210 Embed the n-by-n complex matrix ``M`` into the space of real
1211 matrices of size 2n-by-2n via the map the sends each entry `z = a +
1212 bi` to the block matrix ``[[a,b],[-b,a]]``.
1213
1214 SETUP::
1215
1216 sage: from mjo.eja.eja_algebra import \
1217 ....: ComplexMatrixEuclideanJordanAlgebra
1218
1219 EXAMPLES::
1220
1221 sage: F = QuadraticField(-1, 'I')
1222 sage: x1 = F(4 - 2*i)
1223 sage: x2 = F(1 + 2*i)
1224 sage: x3 = F(-i)
1225 sage: x4 = F(6)
1226 sage: M = matrix(F,2,[[x1,x2],[x3,x4]])
1227 sage: ComplexMatrixEuclideanJordanAlgebra.real_embed(M)
1228 [ 4 -2| 1 2]
1229 [ 2 4|-2 1]
1230 [-----+-----]
1231 [ 0 -1| 6 0]
1232 [ 1 0| 0 6]
1233
1234 TESTS:
1235
1236 Embedding is a homomorphism (isomorphism, in fact)::
1237
1238 sage: set_random_seed()
1239 sage: n_max = ComplexMatrixEuclideanJordanAlgebra._max_test_case_size()
1240 sage: n = ZZ.random_element(n_max)
1241 sage: F = QuadraticField(-1, 'I')
1242 sage: X = random_matrix(F, n)
1243 sage: Y = random_matrix(F, n)
1244 sage: Xe = ComplexMatrixEuclideanJordanAlgebra.real_embed(X)
1245 sage: Ye = ComplexMatrixEuclideanJordanAlgebra.real_embed(Y)
1246 sage: XYe = ComplexMatrixEuclideanJordanAlgebra.real_embed(X*Y)
1247 sage: Xe*Ye == XYe
1248 True
1249
1250 """
1251 n = M.nrows()
1252 if M.ncols() != n:
1253 raise ValueError("the matrix 'M' must be square")
1254
1255 # We don't need any adjoined elements...
1256 field = M.base_ring().base_ring()
1257
1258 blocks = []
1259 for z in M.list():
1260 a = z.list()[0] # real part, I guess
1261 b = z.list()[1] # imag part, I guess
1262 blocks.append(matrix(field, 2, [[a,b],[-b,a]]))
1263
1264 return matrix.block(field, n, blocks)
1265
1266
1267 @staticmethod
1268 def real_unembed(M):
1269 """
1270 The inverse of _embed_complex_matrix().
1271
1272 SETUP::
1273
1274 sage: from mjo.eja.eja_algebra import \
1275 ....: ComplexMatrixEuclideanJordanAlgebra
1276
1277 EXAMPLES::
1278
1279 sage: A = matrix(QQ,[ [ 1, 2, 3, 4],
1280 ....: [-2, 1, -4, 3],
1281 ....: [ 9, 10, 11, 12],
1282 ....: [-10, 9, -12, 11] ])
1283 sage: ComplexMatrixEuclideanJordanAlgebra.real_unembed(A)
1284 [ 2*I + 1 4*I + 3]
1285 [ 10*I + 9 12*I + 11]
1286
1287 TESTS:
1288
1289 Unembedding is the inverse of embedding::
1290
1291 sage: set_random_seed()
1292 sage: F = QuadraticField(-1, 'I')
1293 sage: M = random_matrix(F, 3)
1294 sage: Me = ComplexMatrixEuclideanJordanAlgebra.real_embed(M)
1295 sage: ComplexMatrixEuclideanJordanAlgebra.real_unembed(Me) == M
1296 True
1297
1298 """
1299 n = ZZ(M.nrows())
1300 if M.ncols() != n:
1301 raise ValueError("the matrix 'M' must be square")
1302 if not n.mod(2).is_zero():
1303 raise ValueError("the matrix 'M' must be a complex embedding")
1304
1305 # If "M" was normalized, its base ring might have roots
1306 # adjoined and they can stick around after unembedding.
1307 field = M.base_ring()
1308 R = PolynomialRing(field, 'z')
1309 z = R.gen()
1310 if field is AA:
1311 # Sage doesn't know how to embed AA into QQbar, i.e. how
1312 # to adjoin sqrt(-1) to AA.
1313 F = QQbar
1314 else:
1315 F = field.extension(z**2 + 1, 'I', embedding=CLF(-1).sqrt())
1316 i = F.gen()
1317
1318 # Go top-left to bottom-right (reading order), converting every
1319 # 2-by-2 block we see to a single complex element.
1320 elements = []
1321 for k in range(n/2):
1322 for j in range(n/2):
1323 submat = M[2*k:2*k+2,2*j:2*j+2]
1324 if submat[0,0] != submat[1,1]:
1325 raise ValueError('bad on-diagonal submatrix')
1326 if submat[0,1] != -submat[1,0]:
1327 raise ValueError('bad off-diagonal submatrix')
1328 z = submat[0,0] + submat[0,1]*i
1329 elements.append(z)
1330
1331 return matrix(F, n/2, elements)
1332
1333
1334 @classmethod
1335 def natural_inner_product(cls,X,Y):
1336 """
1337 Compute a natural inner product in this algebra directly from
1338 its real embedding.
1339
1340 SETUP::
1341
1342 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1343
1344 TESTS:
1345
1346 This gives the same answer as the slow, default method implemented
1347 in :class:`MatrixEuclideanJordanAlgebra`::
1348
1349 sage: set_random_seed()
1350 sage: J = ComplexHermitianEJA.random_instance()
1351 sage: x,y = J.random_elements(2)
1352 sage: Xe = x.natural_representation()
1353 sage: Ye = y.natural_representation()
1354 sage: X = ComplexHermitianEJA.real_unembed(Xe)
1355 sage: Y = ComplexHermitianEJA.real_unembed(Ye)
1356 sage: expected = (X*Y).trace().real()
1357 sage: actual = ComplexHermitianEJA.natural_inner_product(Xe,Ye)
1358 sage: actual == expected
1359 True
1360
1361 """
1362 return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/2
1363
1364
1365 class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra):
1366 """
1367 The rank-n simple EJA consisting of complex Hermitian n-by-n
1368 matrices over the real numbers, the usual symmetric Jordan product,
1369 and the real-part-of-trace inner product. It has dimension `n^2` over
1370 the reals.
1371
1372 SETUP::
1373
1374 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1375
1376 EXAMPLES:
1377
1378 In theory, our "field" can be any subfield of the reals::
1379
1380 sage: ComplexHermitianEJA(2, RDF)
1381 Euclidean Jordan algebra of dimension 4 over Real Double Field
1382 sage: ComplexHermitianEJA(2, RR)
1383 Euclidean Jordan algebra of dimension 4 over Real Field with
1384 53 bits of precision
1385
1386 TESTS:
1387
1388 The dimension of this algebra is `n^2`::
1389
1390 sage: set_random_seed()
1391 sage: n_max = ComplexHermitianEJA._max_test_case_size()
1392 sage: n = ZZ.random_element(1, n_max)
1393 sage: J = ComplexHermitianEJA(n)
1394 sage: J.dimension() == n^2
1395 True
1396
1397 The Jordan multiplication is what we think it is::
1398
1399 sage: set_random_seed()
1400 sage: J = ComplexHermitianEJA.random_instance()
1401 sage: x,y = J.random_elements(2)
1402 sage: actual = (x*y).natural_representation()
1403 sage: X = x.natural_representation()
1404 sage: Y = y.natural_representation()
1405 sage: expected = (X*Y + Y*X)/2
1406 sage: actual == expected
1407 True
1408 sage: J(expected) == x*y
1409 True
1410
1411 We can change the generator prefix::
1412
1413 sage: ComplexHermitianEJA(2, prefix='z').gens()
1414 (z0, z1, z2, z3)
1415
1416 Our natural basis is normalized with respect to the natural inner
1417 product unless we specify otherwise::
1418
1419 sage: set_random_seed()
1420 sage: J = ComplexHermitianEJA.random_instance()
1421 sage: all( b.norm() == 1 for b in J.gens() )
1422 True
1423
1424 Since our natural basis is normalized with respect to the natural
1425 inner product, and since we know that this algebra is an EJA, any
1426 left-multiplication operator's matrix will be symmetric because
1427 natural->EJA basis representation is an isometry and within the EJA
1428 the operator is self-adjoint by the Jordan axiom::
1429
1430 sage: set_random_seed()
1431 sage: x = ComplexHermitianEJA.random_instance().random_element()
1432 sage: x.operator().matrix().is_symmetric()
1433 True
1434
1435 """
1436
1437 @classmethod
1438 def _denormalized_basis(cls, n, field):
1439 """
1440 Returns a basis for the space of complex Hermitian n-by-n matrices.
1441
1442 Why do we embed these? Basically, because all of numerical linear
1443 algebra assumes that you're working with vectors consisting of `n`
1444 entries from a field and scalars from the same field. There's no way
1445 to tell SageMath that (for example) the vectors contain complex
1446 numbers, while the scalar field is real.
1447
1448 SETUP::
1449
1450 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1451
1452 TESTS::
1453
1454 sage: set_random_seed()
1455 sage: n = ZZ.random_element(1,5)
1456 sage: field = QuadraticField(2, 'sqrt2')
1457 sage: B = ComplexHermitianEJA._denormalized_basis(n, field)
1458 sage: all( M.is_symmetric() for M in B)
1459 True
1460
1461 """
1462 R = PolynomialRing(field, 'z')
1463 z = R.gen()
1464 F = field.extension(z**2 + 1, 'I')
1465 I = F.gen()
1466
1467 # This is like the symmetric case, but we need to be careful:
1468 #
1469 # * We want conjugate-symmetry, not just symmetry.
1470 # * The diagonal will (as a result) be real.
1471 #
1472 S = []
1473 for i in range(n):
1474 for j in range(i+1):
1475 Eij = matrix(F, n, lambda k,l: k==i and l==j)
1476 if i == j:
1477 Sij = cls.real_embed(Eij)
1478 S.append(Sij)
1479 else:
1480 # The second one has a minus because it's conjugated.
1481 Sij_real = cls.real_embed(Eij + Eij.transpose())
1482 S.append(Sij_real)
1483 Sij_imag = cls.real_embed(I*Eij - I*Eij.transpose())
1484 S.append(Sij_imag)
1485
1486 # Since we embedded these, we can drop back to the "field" that we
1487 # started with instead of the complex extension "F".
1488 return ( s.change_ring(field) for s in S )
1489
1490
1491 def __init__(self, n, field=AA, **kwargs):
1492 basis = self._denormalized_basis(n,field)
1493 super(ComplexHermitianEJA,self).__init__(field, basis, **kwargs)
1494 self.rank.set_cache(n)
1495
1496
1497 class QuaternionMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
1498 @staticmethod
1499 def real_embed(M):
1500 """
1501 Embed the n-by-n quaternion matrix ``M`` into the space of real
1502 matrices of size 4n-by-4n by first sending each quaternion entry `z
1503 = a + bi + cj + dk` to the block-complex matrix ``[[a + bi,
1504 c+di],[-c + di, a-bi]]`, and then embedding those into a real
1505 matrix.
1506
1507 SETUP::
1508
1509 sage: from mjo.eja.eja_algebra import \
1510 ....: QuaternionMatrixEuclideanJordanAlgebra
1511
1512 EXAMPLES::
1513
1514 sage: Q = QuaternionAlgebra(QQ,-1,-1)
1515 sage: i,j,k = Q.gens()
1516 sage: x = 1 + 2*i + 3*j + 4*k
1517 sage: M = matrix(Q, 1, [[x]])
1518 sage: QuaternionMatrixEuclideanJordanAlgebra.real_embed(M)
1519 [ 1 2 3 4]
1520 [-2 1 -4 3]
1521 [-3 4 1 -2]
1522 [-4 -3 2 1]
1523
1524 Embedding is a homomorphism (isomorphism, in fact)::
1525
1526 sage: set_random_seed()
1527 sage: n_max = QuaternionMatrixEuclideanJordanAlgebra._max_test_case_size()
1528 sage: n = ZZ.random_element(n_max)
1529 sage: Q = QuaternionAlgebra(QQ,-1,-1)
1530 sage: X = random_matrix(Q, n)
1531 sage: Y = random_matrix(Q, n)
1532 sage: Xe = QuaternionMatrixEuclideanJordanAlgebra.real_embed(X)
1533 sage: Ye = QuaternionMatrixEuclideanJordanAlgebra.real_embed(Y)
1534 sage: XYe = QuaternionMatrixEuclideanJordanAlgebra.real_embed(X*Y)
1535 sage: Xe*Ye == XYe
1536 True
1537
1538 """
1539 quaternions = M.base_ring()
1540 n = M.nrows()
1541 if M.ncols() != n:
1542 raise ValueError("the matrix 'M' must be square")
1543
1544 F = QuadraticField(-1, 'I')
1545 i = F.gen()
1546
1547 blocks = []
1548 for z in M.list():
1549 t = z.coefficient_tuple()
1550 a = t[0]
1551 b = t[1]
1552 c = t[2]
1553 d = t[3]
1554 cplxM = matrix(F, 2, [[ a + b*i, c + d*i],
1555 [-c + d*i, a - b*i]])
1556 realM = ComplexMatrixEuclideanJordanAlgebra.real_embed(cplxM)
1557 blocks.append(realM)
1558
1559 # We should have real entries by now, so use the realest field
1560 # we've got for the return value.
1561 return matrix.block(quaternions.base_ring(), n, blocks)
1562
1563
1564
1565 @staticmethod
1566 def real_unembed(M):
1567 """
1568 The inverse of _embed_quaternion_matrix().
1569
1570 SETUP::
1571
1572 sage: from mjo.eja.eja_algebra import \
1573 ....: QuaternionMatrixEuclideanJordanAlgebra
1574
1575 EXAMPLES::
1576
1577 sage: M = matrix(QQ, [[ 1, 2, 3, 4],
1578 ....: [-2, 1, -4, 3],
1579 ....: [-3, 4, 1, -2],
1580 ....: [-4, -3, 2, 1]])
1581 sage: QuaternionMatrixEuclideanJordanAlgebra.real_unembed(M)
1582 [1 + 2*i + 3*j + 4*k]
1583
1584 TESTS:
1585
1586 Unembedding is the inverse of embedding::
1587
1588 sage: set_random_seed()
1589 sage: Q = QuaternionAlgebra(QQ, -1, -1)
1590 sage: M = random_matrix(Q, 3)
1591 sage: Me = QuaternionMatrixEuclideanJordanAlgebra.real_embed(M)
1592 sage: QuaternionMatrixEuclideanJordanAlgebra.real_unembed(Me) == M
1593 True
1594
1595 """
1596 n = ZZ(M.nrows())
1597 if M.ncols() != n:
1598 raise ValueError("the matrix 'M' must be square")
1599 if not n.mod(4).is_zero():
1600 raise ValueError("the matrix 'M' must be a quaternion embedding")
1601
1602 # Use the base ring of the matrix to ensure that its entries can be
1603 # multiplied by elements of the quaternion algebra.
1604 field = M.base_ring()
1605 Q = QuaternionAlgebra(field,-1,-1)
1606 i,j,k = Q.gens()
1607
1608 # Go top-left to bottom-right (reading order), converting every
1609 # 4-by-4 block we see to a 2-by-2 complex block, to a 1-by-1
1610 # quaternion block.
1611 elements = []
1612 for l in range(n/4):
1613 for m in range(n/4):
1614 submat = ComplexMatrixEuclideanJordanAlgebra.real_unembed(
1615 M[4*l:4*l+4,4*m:4*m+4] )
1616 if submat[0,0] != submat[1,1].conjugate():
1617 raise ValueError('bad on-diagonal submatrix')
1618 if submat[0,1] != -submat[1,0].conjugate():
1619 raise ValueError('bad off-diagonal submatrix')
1620 z = submat[0,0].real()
1621 z += submat[0,0].imag()*i
1622 z += submat[0,1].real()*j
1623 z += submat[0,1].imag()*k
1624 elements.append(z)
1625
1626 return matrix(Q, n/4, elements)
1627
1628
1629 @classmethod
1630 def natural_inner_product(cls,X,Y):
1631 """
1632 Compute a natural inner product in this algebra directly from
1633 its real embedding.
1634
1635 SETUP::
1636
1637 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
1638
1639 TESTS:
1640
1641 This gives the same answer as the slow, default method implemented
1642 in :class:`MatrixEuclideanJordanAlgebra`::
1643
1644 sage: set_random_seed()
1645 sage: J = QuaternionHermitianEJA.random_instance()
1646 sage: x,y = J.random_elements(2)
1647 sage: Xe = x.natural_representation()
1648 sage: Ye = y.natural_representation()
1649 sage: X = QuaternionHermitianEJA.real_unembed(Xe)
1650 sage: Y = QuaternionHermitianEJA.real_unembed(Ye)
1651 sage: expected = (X*Y).trace().coefficient_tuple()[0]
1652 sage: actual = QuaternionHermitianEJA.natural_inner_product(Xe,Ye)
1653 sage: actual == expected
1654 True
1655
1656 """
1657 return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/4
1658
1659
1660 class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra):
1661 """
1662 The rank-n simple EJA consisting of self-adjoint n-by-n quaternion
1663 matrices, the usual symmetric Jordan product, and the
1664 real-part-of-trace inner product. It has dimension `2n^2 - n` over
1665 the reals.
1666
1667 SETUP::
1668
1669 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
1670
1671 EXAMPLES:
1672
1673 In theory, our "field" can be any subfield of the reals::
1674
1675 sage: QuaternionHermitianEJA(2, RDF)
1676 Euclidean Jordan algebra of dimension 6 over Real Double Field
1677 sage: QuaternionHermitianEJA(2, RR)
1678 Euclidean Jordan algebra of dimension 6 over Real Field with
1679 53 bits of precision
1680
1681 TESTS:
1682
1683 The dimension of this algebra is `2*n^2 - n`::
1684
1685 sage: set_random_seed()
1686 sage: n_max = QuaternionHermitianEJA._max_test_case_size()
1687 sage: n = ZZ.random_element(1, n_max)
1688 sage: J = QuaternionHermitianEJA(n)
1689 sage: J.dimension() == 2*(n^2) - n
1690 True
1691
1692 The Jordan multiplication is what we think it is::
1693
1694 sage: set_random_seed()
1695 sage: J = QuaternionHermitianEJA.random_instance()
1696 sage: x,y = J.random_elements(2)
1697 sage: actual = (x*y).natural_representation()
1698 sage: X = x.natural_representation()
1699 sage: Y = y.natural_representation()
1700 sage: expected = (X*Y + Y*X)/2
1701 sage: actual == expected
1702 True
1703 sage: J(expected) == x*y
1704 True
1705
1706 We can change the generator prefix::
1707
1708 sage: QuaternionHermitianEJA(2, prefix='a').gens()
1709 (a0, a1, a2, a3, a4, a5)
1710
1711 Our natural basis is normalized with respect to the natural inner
1712 product unless we specify otherwise::
1713
1714 sage: set_random_seed()
1715 sage: J = QuaternionHermitianEJA.random_instance()
1716 sage: all( b.norm() == 1 for b in J.gens() )
1717 True
1718
1719 Since our natural basis is normalized with respect to the natural
1720 inner product, and since we know that this algebra is an EJA, any
1721 left-multiplication operator's matrix will be symmetric because
1722 natural->EJA basis representation is an isometry and within the EJA
1723 the operator is self-adjoint by the Jordan axiom::
1724
1725 sage: set_random_seed()
1726 sage: x = QuaternionHermitianEJA.random_instance().random_element()
1727 sage: x.operator().matrix().is_symmetric()
1728 True
1729
1730 """
1731 @classmethod
1732 def _denormalized_basis(cls, n, field):
1733 """
1734 Returns a basis for the space of quaternion Hermitian n-by-n matrices.
1735
1736 Why do we embed these? Basically, because all of numerical
1737 linear algebra assumes that you're working with vectors consisting
1738 of `n` entries from a field and scalars from the same field. There's
1739 no way to tell SageMath that (for example) the vectors contain
1740 complex numbers, while the scalar field is real.
1741
1742 SETUP::
1743
1744 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
1745
1746 TESTS::
1747
1748 sage: set_random_seed()
1749 sage: n = ZZ.random_element(1,5)
1750 sage: B = QuaternionHermitianEJA._denormalized_basis(n,QQ)
1751 sage: all( M.is_symmetric() for M in B )
1752 True
1753
1754 """
1755 Q = QuaternionAlgebra(QQ,-1,-1)
1756 I,J,K = Q.gens()
1757
1758 # This is like the symmetric case, but we need to be careful:
1759 #
1760 # * We want conjugate-symmetry, not just symmetry.
1761 # * The diagonal will (as a result) be real.
1762 #
1763 S = []
1764 for i in range(n):
1765 for j in range(i+1):
1766 Eij = matrix(Q, n, lambda k,l: k==i and l==j)
1767 if i == j:
1768 Sij = cls.real_embed(Eij)
1769 S.append(Sij)
1770 else:
1771 # The second, third, and fourth ones have a minus
1772 # because they're conjugated.
1773 Sij_real = cls.real_embed(Eij + Eij.transpose())
1774 S.append(Sij_real)
1775 Sij_I = cls.real_embed(I*Eij - I*Eij.transpose())
1776 S.append(Sij_I)
1777 Sij_J = cls.real_embed(J*Eij - J*Eij.transpose())
1778 S.append(Sij_J)
1779 Sij_K = cls.real_embed(K*Eij - K*Eij.transpose())
1780 S.append(Sij_K)
1781
1782 # Since we embedded these, we can drop back to the "field" that we
1783 # started with instead of the quaternion algebra "Q".
1784 return ( s.change_ring(field) for s in S )
1785
1786
1787 def __init__(self, n, field=AA, **kwargs):
1788 basis = self._denormalized_basis(n,field)
1789 super(QuaternionHermitianEJA,self).__init__(field, basis, **kwargs)
1790 self.rank.set_cache(n)
1791
1792
1793 class BilinearFormEJA(FiniteDimensionalEuclideanJordanAlgebra):
1794 r"""
1795 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
1796 with the half-trace inner product and jordan product ``x*y =
1797 (x0*y0 + <B*x_bar,y_bar>, x0*y_bar + y0*x_bar)`` where ``B`` is a
1798 symmetric positive-definite "bilinear form" matrix. It has
1799 dimension `n` over the reals, and reduces to the ``JordanSpinEJA``
1800 when ``B`` is the identity matrix of order ``n-1``.
1801
1802 SETUP::
1803
1804 sage: from mjo.eja.eja_algebra import (BilinearFormEJA,
1805 ....: JordanSpinEJA)
1806
1807 EXAMPLES:
1808
1809 When no bilinear form is specified, the identity matrix is used,
1810 and the resulting algebra is the Jordan spin algebra::
1811
1812 sage: J0 = BilinearFormEJA(3)
1813 sage: J1 = JordanSpinEJA(3)
1814 sage: J0.multiplication_table() == J0.multiplication_table()
1815 True
1816
1817 TESTS:
1818
1819 We can create a zero-dimensional algebra::
1820
1821 sage: J = BilinearFormEJA(0)
1822 sage: J.basis()
1823 Finite family {}
1824
1825 We can check the multiplication condition given in the Jordan, von
1826 Neumann, and Wigner paper (and also discussed on my "On the
1827 symmetry..." paper). Note that this relies heavily on the standard
1828 choice of basis, as does anything utilizing the bilinear form matrix::
1829
1830 sage: set_random_seed()
1831 sage: n = ZZ.random_element(5)
1832 sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular')
1833 sage: B = M.transpose()*M
1834 sage: J = BilinearFormEJA(n, B=B)
1835 sage: eis = VectorSpace(M.base_ring(), M.ncols()).basis()
1836 sage: V = J.vector_space()
1837 sage: sis = [ J.from_vector(V([0] + (M.inverse()*ei).list()))
1838 ....: for ei in eis ]
1839 sage: actual = [ sis[i]*sis[j]
1840 ....: for i in range(n-1)
1841 ....: for j in range(n-1) ]
1842 sage: expected = [ J.one() if i == j else J.zero()
1843 ....: for i in range(n-1)
1844 ....: for j in range(n-1) ]
1845 sage: actual == expected
1846 True
1847 """
1848 def __init__(self, n, field=AA, B=None, **kwargs):
1849 if B is None:
1850 self._B = matrix.identity(field, max(0,n-1))
1851 else:
1852 self._B = B
1853
1854 V = VectorSpace(field, n)
1855 mult_table = [[V.zero() for j in range(n)] for i in range(n)]
1856 for i in range(n):
1857 for j in range(n):
1858 x = V.gen(i)
1859 y = V.gen(j)
1860 x0 = x[0]
1861 xbar = x[1:]
1862 y0 = y[0]
1863 ybar = y[1:]
1864 z0 = x0*y0 + (self._B*xbar).inner_product(ybar)
1865 zbar = y0*xbar + x0*ybar
1866 z = V([z0] + zbar.list())
1867 mult_table[i][j] = z
1868
1869 # The rank of this algebra is two, unless we're in a
1870 # one-dimensional ambient space (because the rank is bounded
1871 # by the ambient dimension).
1872 fdeja = super(BilinearFormEJA, self)
1873 fdeja.__init__(field, mult_table, **kwargs)
1874 self.rank.set_cache(min(n,2))
1875
1876 def inner_product(self, x, y):
1877 r"""
1878 Half of the trace inner product.
1879
1880 This is defined so that the special case of the Jordan spin
1881 algebra gets the usual inner product.
1882
1883 SETUP::
1884
1885 sage: from mjo.eja.eja_algebra import BilinearFormEJA
1886
1887 TESTS:
1888
1889 Ensure that this is one-half of the trace inner-product when
1890 the algebra isn't just the reals (when ``n`` isn't one). This
1891 is in Faraut and Koranyi, and also my "On the symmetry..."
1892 paper::
1893
1894 sage: set_random_seed()
1895 sage: n = ZZ.random_element(2,5)
1896 sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular')
1897 sage: B = M.transpose()*M
1898 sage: J = BilinearFormEJA(n, B=B)
1899 sage: x = J.random_element()
1900 sage: y = J.random_element()
1901 sage: x.inner_product(y) == (x*y).trace()/2
1902 True
1903
1904 """
1905 xvec = x.to_vector()
1906 xbar = xvec[1:]
1907 yvec = y.to_vector()
1908 ybar = yvec[1:]
1909 return x[0]*y[0] + (self._B*xbar).inner_product(ybar)
1910
1911
1912 class JordanSpinEJA(BilinearFormEJA):
1913 """
1914 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
1915 with the usual inner product and jordan product ``x*y =
1916 (<x,y>, x0*y_bar + y0*x_bar)``. It has dimension `n` over
1917 the reals.
1918
1919 SETUP::
1920
1921 sage: from mjo.eja.eja_algebra import JordanSpinEJA
1922
1923 EXAMPLES:
1924
1925 This multiplication table can be verified by hand::
1926
1927 sage: J = JordanSpinEJA(4)
1928 sage: e0,e1,e2,e3 = J.gens()
1929 sage: e0*e0
1930 e0
1931 sage: e0*e1
1932 e1
1933 sage: e0*e2
1934 e2
1935 sage: e0*e3
1936 e3
1937 sage: e1*e2
1938 0
1939 sage: e1*e3
1940 0
1941 sage: e2*e3
1942 0
1943
1944 We can change the generator prefix::
1945
1946 sage: JordanSpinEJA(2, prefix='B').gens()
1947 (B0, B1)
1948
1949 TESTS:
1950
1951 Ensure that we have the usual inner product on `R^n`::
1952
1953 sage: set_random_seed()
1954 sage: J = JordanSpinEJA.random_instance()
1955 sage: x,y = J.random_elements(2)
1956 sage: X = x.natural_representation()
1957 sage: Y = y.natural_representation()
1958 sage: x.inner_product(y) == J.natural_inner_product(X,Y)
1959 True
1960
1961 """
1962 def __init__(self, n, field=AA, **kwargs):
1963 # This is a special case of the BilinearFormEJA with the identity
1964 # matrix as its bilinear form.
1965 return super(JordanSpinEJA, self).__init__(n, field, **kwargs)
1966
1967
1968 class TrivialEJA(FiniteDimensionalEuclideanJordanAlgebra):
1969 """
1970 The trivial Euclidean Jordan algebra consisting of only a zero element.
1971
1972 SETUP::
1973
1974 sage: from mjo.eja.eja_algebra import TrivialEJA
1975
1976 EXAMPLES::
1977
1978 sage: J = TrivialEJA()
1979 sage: J.dimension()
1980 0
1981 sage: J.zero()
1982 0
1983 sage: J.one()
1984 0
1985 sage: 7*J.one()*12*J.one()
1986 0
1987 sage: J.one().inner_product(J.one())
1988 0
1989 sage: J.one().norm()
1990 0
1991 sage: J.one().subalgebra_generated_by()
1992 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
1993 sage: J.rank()
1994 0
1995
1996 """
1997 def __init__(self, field=AA, **kwargs):
1998 mult_table = []
1999 fdeja = super(TrivialEJA, self)
2000 # The rank is zero using my definition, namely the dimension of the
2001 # largest subalgebra generated by any element.
2002 fdeja.__init__(field, mult_table, **kwargs)
2003 self.rank.set_cache(0)