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