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