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