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