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