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