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