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