]> gitweb.michael.orlitzky.com - sage.d.git/blob - mjo/eja/eja_algebra.py
eja: start experimenting with single-element Peirce decomposition.
[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 from itertools import izip, repeat
9
10 from sage.algebras.quatalg.quaternion_algebra import QuaternionAlgebra
11 from sage.categories.magmatic_algebras import MagmaticAlgebras
12 from sage.combinat.free_module import CombinatorialFreeModule
13 from sage.matrix.constructor import matrix
14 from sage.matrix.matrix_space import MatrixSpace
15 from sage.misc.cachefunc import cached_method
16 from sage.misc.lazy_import import lazy_import
17 from sage.misc.prandom import choice
18 from sage.misc.table import table
19 from sage.modules.free_module import FreeModule, VectorSpace
20 from sage.rings.all import (ZZ, QQ, RR, RLF, CLF,
21 PolynomialRing,
22 QuadraticField)
23 from mjo.eja.eja_element import FiniteDimensionalEuclideanJordanAlgebraElement
24 lazy_import('mjo.eja.eja_subalgebra',
25 'FiniteDimensionalEuclideanJordanSubalgebra')
26 from mjo.eja.eja_utils import _mat2vec
27
28 class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
29 # This is an ugly hack needed to prevent the category framework
30 # from implementing a coercion from our base ring (e.g. the
31 # rationals) into the algebra. First of all -- such a coercion is
32 # nonsense to begin with. But more importantly, it tries to do so
33 # in the category of rings, and since our algebras aren't
34 # associative they generally won't be rings.
35 _no_generic_basering_coercion = True
36
37 def __init__(self,
38 field,
39 mult_table,
40 rank,
41 prefix='e',
42 category=None,
43 natural_basis=None,
44 check=True):
45 """
46 SETUP::
47
48 sage: from mjo.eja.eja_algebra import (JordanSpinEJA, random_eja)
49
50 EXAMPLES:
51
52 By definition, Jordan multiplication commutes::
53
54 sage: set_random_seed()
55 sage: J = random_eja()
56 sage: x,y = J.random_elements(2)
57 sage: x*y == y*x
58 True
59
60 TESTS:
61
62 The ``field`` we're given must be real::
63
64 sage: JordanSpinEJA(2,QQbar)
65 Traceback (most recent call last):
66 ...
67 ValueError: field is not real
68
69 """
70 if check:
71 if not field.is_subring(RR):
72 # Note: this does return true for the real algebraic
73 # field, and any quadratic field where we've specified
74 # a real embedding.
75 raise ValueError('field is not real')
76
77 self._rank = rank
78 self._natural_basis = natural_basis
79
80 if category is None:
81 category = MagmaticAlgebras(field).FiniteDimensional()
82 category = category.WithBasis().Unital()
83
84 fda = super(FiniteDimensionalEuclideanJordanAlgebra, self)
85 fda.__init__(field,
86 range(len(mult_table)),
87 prefix=prefix,
88 category=category)
89 self.print_options(bracket='')
90
91 # The multiplication table we're given is necessarily in terms
92 # of vectors, because we don't have an algebra yet for
93 # anything to be an element of. However, it's faster in the
94 # long run to have the multiplication table be in terms of
95 # algebra elements. We do this after calling the superclass
96 # constructor so that from_vector() knows what to do.
97 self._multiplication_table = [ map(lambda x: self.from_vector(x), ls)
98 for ls in mult_table ]
99
100
101 def _element_constructor_(self, elt):
102 """
103 Construct an element of this algebra from its natural
104 representation.
105
106 This gets called only after the parent element _call_ method
107 fails to find a coercion for the argument.
108
109 SETUP::
110
111 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
112 ....: RealCartesianProductEJA,
113 ....: RealSymmetricEJA)
114
115 EXAMPLES:
116
117 The identity in `S^n` is converted to the identity in the EJA::
118
119 sage: J = RealSymmetricEJA(3)
120 sage: I = matrix.identity(QQ,3)
121 sage: J(I) == J.one()
122 True
123
124 This skew-symmetric matrix can't be represented in the EJA::
125
126 sage: J = RealSymmetricEJA(3)
127 sage: A = matrix(QQ,3, lambda i,j: i-j)
128 sage: J(A)
129 Traceback (most recent call last):
130 ...
131 ArithmeticError: vector is not in free module
132
133 TESTS:
134
135 Ensure that we can convert any element of the two non-matrix
136 simple algebras (whose natural representations are their usual
137 vector representations) back and forth faithfully::
138
139 sage: set_random_seed()
140 sage: J = RealCartesianProductEJA.random_instance()
141 sage: x = J.random_element()
142 sage: J(x.to_vector().column()) == x
143 True
144 sage: J = JordanSpinEJA.random_instance()
145 sage: x = J.random_element()
146 sage: J(x.to_vector().column()) == x
147 True
148
149 """
150 if elt == 0:
151 # The superclass implementation of random_element()
152 # needs to be able to coerce "0" into the algebra.
153 return self.zero()
154
155 natural_basis = self.natural_basis()
156 basis_space = natural_basis[0].matrix_space()
157 if elt not in basis_space:
158 raise ValueError("not a naturally-represented algebra element")
159
160 # Thanks for nothing! Matrix spaces aren't vector spaces in
161 # Sage, so we have to figure out its natural-basis coordinates
162 # ourselves. We use the basis space's ring instead of the
163 # element's ring because the basis space might be an algebraic
164 # closure whereas the base ring of the 3-by-3 identity matrix
165 # could be QQ instead of QQbar.
166 V = VectorSpace(basis_space.base_ring(), elt.nrows()*elt.ncols())
167 W = V.span_of_basis( _mat2vec(s) for s in natural_basis )
168 coords = W.coordinate_vector(_mat2vec(elt))
169 return self.from_vector(coords)
170
171
172 def _repr_(self):
173 """
174 Return a string representation of ``self``.
175
176 SETUP::
177
178 sage: from mjo.eja.eja_algebra import JordanSpinEJA
179
180 TESTS:
181
182 Ensure that it says what we think it says::
183
184 sage: JordanSpinEJA(2, field=QQ)
185 Euclidean Jordan algebra of dimension 2 over Rational Field
186 sage: JordanSpinEJA(3, field=RDF)
187 Euclidean Jordan algebra of dimension 3 over Real Double Field
188
189 """
190 fmt = "Euclidean Jordan algebra of dimension {} over {}"
191 return fmt.format(self.dimension(), self.base_ring())
192
193 def product_on_basis(self, i, j):
194 return self._multiplication_table[i][j]
195
196 def _a_regular_element(self):
197 """
198 Guess a regular element. Needed to compute the basis for our
199 characteristic polynomial coefficients.
200
201 SETUP::
202
203 sage: from mjo.eja.eja_algebra import random_eja
204
205 TESTS:
206
207 Ensure that this hacky method succeeds for every algebra that we
208 know how to construct::
209
210 sage: set_random_seed()
211 sage: J = random_eja()
212 sage: J._a_regular_element().is_regular()
213 True
214
215 """
216 gs = self.gens()
217 z = self.sum( (i+1)*gs[i] for i in range(len(gs)) )
218 if not z.is_regular():
219 raise ValueError("don't know a regular element")
220 return z
221
222
223 @cached_method
224 def _charpoly_basis_space(self):
225 """
226 Return the vector space spanned by the basis used in our
227 characteristic polynomial coefficients. This is used not only to
228 compute those coefficients, but also any time we need to
229 evaluate the coefficients (like when we compute the trace or
230 determinant).
231 """
232 z = self._a_regular_element()
233 # Don't use the parent vector space directly here in case this
234 # happens to be a subalgebra. In that case, we would be e.g.
235 # two-dimensional but span_of_basis() would expect three
236 # coordinates.
237 V = VectorSpace(self.base_ring(), self.vector_space().dimension())
238 basis = [ (z**k).to_vector() for k in range(self.rank()) ]
239 V1 = V.span_of_basis( basis )
240 b = (V1.basis() + V1.complement().basis())
241 return V.span_of_basis(b)
242
243
244
245 @cached_method
246 def _charpoly_coeff(self, i):
247 """
248 Return the coefficient polynomial "a_{i}" of this algebra's
249 general characteristic polynomial.
250
251 Having this be a separate cached method lets us compute and
252 store the trace/determinant (a_{r-1} and a_{0} respectively)
253 separate from the entire characteristic polynomial.
254 """
255 (A_of_x, x, xr, detA) = self._charpoly_matrix_system()
256 R = A_of_x.base_ring()
257
258 if i == self.rank():
259 return R.one()
260 if i > self.rank():
261 # Guaranteed by theory
262 return R.zero()
263
264 # Danger: the in-place modification is done for performance
265 # reasons (reconstructing a matrix with huge polynomial
266 # entries is slow), but I don't know how cached_method works,
267 # so it's highly possible that we're modifying some global
268 # list variable by reference, here. In other words, you
269 # probably shouldn't call this method twice on the same
270 # algebra, at the same time, in two threads
271 Ai_orig = A_of_x.column(i)
272 A_of_x.set_column(i,xr)
273 numerator = A_of_x.det()
274 A_of_x.set_column(i,Ai_orig)
275
276 # We're relying on the theory here to ensure that each a_i is
277 # indeed back in R, and the added negative signs are to make
278 # the whole charpoly expression sum to zero.
279 return R(-numerator/detA)
280
281
282 @cached_method
283 def _charpoly_matrix_system(self):
284 """
285 Compute the matrix whose entries A_ij are polynomials in
286 X1,...,XN, the vector ``x`` of variables X1,...,XN, the vector
287 corresponding to `x^r` and the determinent of the matrix A =
288 [A_ij]. In other words, all of the fixed (cachable) data needed
289 to compute the coefficients of the characteristic polynomial.
290 """
291 r = self.rank()
292 n = self.dimension()
293
294 # Turn my vector space into a module so that "vectors" can
295 # have multivatiate polynomial entries.
296 names = tuple('X' + str(i) for i in range(1,n+1))
297 R = PolynomialRing(self.base_ring(), names)
298
299 # Using change_ring() on the parent's vector space doesn't work
300 # here because, in a subalgebra, that vector space has a basis
301 # and change_ring() tries to bring the basis along with it. And
302 # that doesn't work unless the new ring is a PID, which it usually
303 # won't be.
304 V = FreeModule(R,n)
305
306 # Now let x = (X1,X2,...,Xn) be the vector whose entries are
307 # indeterminates...
308 x = V(names)
309
310 # And figure out the "left multiplication by x" matrix in
311 # that setting.
312 lmbx_cols = []
313 monomial_matrices = [ self.monomial(i).operator().matrix()
314 for i in range(n) ] # don't recompute these!
315 for k in range(n):
316 ek = self.monomial(k).to_vector()
317 lmbx_cols.append(
318 sum( x[i]*(monomial_matrices[i]*ek)
319 for i in range(n) ) )
320 Lx = matrix.column(R, lmbx_cols)
321
322 # Now we can compute powers of x "symbolically"
323 x_powers = [self.one().to_vector(), x]
324 for d in range(2, r+1):
325 x_powers.append( Lx*(x_powers[-1]) )
326
327 idmat = matrix.identity(R, n)
328
329 W = self._charpoly_basis_space()
330 W = W.change_ring(R.fraction_field())
331
332 # Starting with the standard coordinates x = (X1,X2,...,Xn)
333 # and then converting the entries to W-coordinates allows us
334 # to pass in the standard coordinates to the charpoly and get
335 # back the right answer. Specifically, with x = (X1,X2,...,Xn),
336 # we have
337 #
338 # W.coordinates(x^2) eval'd at (standard z-coords)
339 # =
340 # W-coords of (z^2)
341 # =
342 # W-coords of (standard coords of x^2 eval'd at std-coords of z)
343 #
344 # We want the middle equivalent thing in our matrix, but use
345 # the first equivalent thing instead so that we can pass in
346 # standard coordinates.
347 x_powers = [ W.coordinate_vector(xp) for xp in x_powers ]
348 l2 = [idmat.column(k-1) for k in range(r+1, n+1)]
349 A_of_x = matrix.column(R, n, (x_powers[:r] + l2))
350 return (A_of_x, x, x_powers[r], A_of_x.det())
351
352
353 @cached_method
354 def characteristic_polynomial(self):
355 """
356 Return a characteristic polynomial that works for all elements
357 of this algebra.
358
359 The resulting polynomial has `n+1` variables, where `n` is the
360 dimension of this algebra. The first `n` variables correspond to
361 the coordinates of an algebra element: when evaluated at the
362 coordinates of an algebra element with respect to a certain
363 basis, the result is a univariate polynomial (in the one
364 remaining variable ``t``), namely the characteristic polynomial
365 of that element.
366
367 SETUP::
368
369 sage: from mjo.eja.eja_algebra import JordanSpinEJA, TrivialEJA
370
371 EXAMPLES:
372
373 The characteristic polynomial in the spin algebra is given in
374 Alizadeh, Example 11.11::
375
376 sage: J = JordanSpinEJA(3)
377 sage: p = J.characteristic_polynomial(); p
378 X1^2 - X2^2 - X3^2 + (-2*t)*X1 + t^2
379 sage: xvec = J.one().to_vector()
380 sage: p(*xvec)
381 t^2 - 2*t + 1
382
383 By definition, the characteristic polynomial is a monic
384 degree-zero polynomial in a rank-zero algebra. Note that
385 Cayley-Hamilton is indeed satisfied since the polynomial
386 ``1`` evaluates to the identity element of the algebra on
387 any argument::
388
389 sage: J = TrivialEJA()
390 sage: J.characteristic_polynomial()
391 1
392
393 """
394 r = self.rank()
395 n = self.dimension()
396
397 # The list of coefficient polynomials a_0, a_1, a_2, ..., a_n.
398 a = [ self._charpoly_coeff(i) for i in range(r+1) ]
399
400 # We go to a bit of trouble here to reorder the
401 # indeterminates, so that it's easier to evaluate the
402 # characteristic polynomial at x's coordinates and get back
403 # something in terms of t, which is what we want.
404 R = a[0].parent()
405 S = PolynomialRing(self.base_ring(),'t')
406 t = S.gen(0)
407 S = PolynomialRing(S, R.variable_names())
408 t = S(t)
409
410 return sum( a[k]*(t**k) for k in xrange(len(a)) )
411
412
413 def inner_product(self, x, y):
414 """
415 The inner product associated with this Euclidean Jordan algebra.
416
417 Defaults to the trace inner product, but can be overridden by
418 subclasses if they are sure that the necessary properties are
419 satisfied.
420
421 SETUP::
422
423 sage: from mjo.eja.eja_algebra import random_eja
424
425 EXAMPLES:
426
427 Our inner product is "associative," which means the following for
428 a symmetric bilinear form::
429
430 sage: set_random_seed()
431 sage: J = random_eja()
432 sage: x,y,z = J.random_elements(3)
433 sage: (x*y).inner_product(z) == y.inner_product(x*z)
434 True
435
436 """
437 X = x.natural_representation()
438 Y = y.natural_representation()
439 return self.natural_inner_product(X,Y)
440
441
442 def is_trivial(self):
443 """
444 Return whether or not this algebra is trivial.
445
446 A trivial algebra contains only the zero element.
447
448 SETUP::
449
450 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
451 ....: TrivialEJA)
452
453 EXAMPLES::
454
455 sage: J = ComplexHermitianEJA(3)
456 sage: J.is_trivial()
457 False
458
459 ::
460
461 sage: J = TrivialEJA()
462 sage: J.is_trivial()
463 True
464
465 """
466 return self.dimension() == 0
467
468
469 def multiplication_table(self):
470 """
471 Return a visual representation of this algebra's multiplication
472 table (on basis elements).
473
474 SETUP::
475
476 sage: from mjo.eja.eja_algebra import JordanSpinEJA
477
478 EXAMPLES::
479
480 sage: J = JordanSpinEJA(4)
481 sage: J.multiplication_table()
482 +----++----+----+----+----+
483 | * || e0 | e1 | e2 | e3 |
484 +====++====+====+====+====+
485 | e0 || e0 | e1 | e2 | e3 |
486 +----++----+----+----+----+
487 | e1 || e1 | e0 | 0 | 0 |
488 +----++----+----+----+----+
489 | e2 || e2 | 0 | e0 | 0 |
490 +----++----+----+----+----+
491 | e3 || e3 | 0 | 0 | e0 |
492 +----++----+----+----+----+
493
494 """
495 M = list(self._multiplication_table) # copy
496 for i in xrange(len(M)):
497 # M had better be "square"
498 M[i] = [self.monomial(i)] + M[i]
499 M = [["*"] + list(self.gens())] + M
500 return table(M, header_row=True, header_column=True, frame=True)
501
502
503 def natural_basis(self):
504 """
505 Return a more-natural representation of this algebra's basis.
506
507 Every finite-dimensional Euclidean Jordan Algebra is a direct
508 sum of five simple algebras, four of which comprise Hermitian
509 matrices. This method returns the original "natural" basis
510 for our underlying vector space. (Typically, the natural basis
511 is used to construct the multiplication table in the first place.)
512
513 Note that this will always return a matrix. The standard basis
514 in `R^n` will be returned as `n`-by-`1` column matrices.
515
516 SETUP::
517
518 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
519 ....: RealSymmetricEJA)
520
521 EXAMPLES::
522
523 sage: J = RealSymmetricEJA(2)
524 sage: J.basis()
525 Finite family {0: e0, 1: e1, 2: e2}
526 sage: J.natural_basis()
527 (
528 [1 0] [ 0 1/2*sqrt2] [0 0]
529 [0 0], [1/2*sqrt2 0], [0 1]
530 )
531
532 ::
533
534 sage: J = JordanSpinEJA(2)
535 sage: J.basis()
536 Finite family {0: e0, 1: e1}
537 sage: J.natural_basis()
538 (
539 [1] [0]
540 [0], [1]
541 )
542
543 """
544 if self._natural_basis is None:
545 M = self.natural_basis_space()
546 return tuple( M(b.to_vector()) for b in self.basis() )
547 else:
548 return self._natural_basis
549
550
551 def natural_basis_space(self):
552 """
553 Return the matrix space in which this algebra's natural basis
554 elements live.
555 """
556 if self._natural_basis is None or len(self._natural_basis) == 0:
557 return MatrixSpace(self.base_ring(), self.dimension(), 1)
558 else:
559 return self._natural_basis[0].matrix_space()
560
561
562 @staticmethod
563 def natural_inner_product(X,Y):
564 """
565 Compute the inner product of two naturally-represented elements.
566
567 For example in the real symmetric matrix EJA, this will compute
568 the trace inner-product of two n-by-n symmetric matrices. The
569 default should work for the real cartesian product EJA, the
570 Jordan spin EJA, and the real symmetric matrices. The others
571 will have to be overridden.
572 """
573 return (X.conjugate_transpose()*Y).trace()
574
575
576 @cached_method
577 def one(self):
578 """
579 Return the unit element of this algebra.
580
581 SETUP::
582
583 sage: from mjo.eja.eja_algebra import (RealCartesianProductEJA,
584 ....: random_eja)
585
586 EXAMPLES::
587
588 sage: J = RealCartesianProductEJA(5)
589 sage: J.one()
590 e0 + e1 + e2 + e3 + e4
591
592 TESTS:
593
594 The identity element acts like the identity::
595
596 sage: set_random_seed()
597 sage: J = random_eja()
598 sage: x = J.random_element()
599 sage: J.one()*x == x and x*J.one() == x
600 True
601
602 The matrix of the unit element's operator is the identity::
603
604 sage: set_random_seed()
605 sage: J = random_eja()
606 sage: actual = J.one().operator().matrix()
607 sage: expected = matrix.identity(J.base_ring(), J.dimension())
608 sage: actual == expected
609 True
610
611 """
612 # We can brute-force compute the matrices of the operators
613 # that correspond to the basis elements of this algebra.
614 # If some linear combination of those basis elements is the
615 # algebra identity, then the same linear combination of
616 # their matrices has to be the identity matrix.
617 #
618 # Of course, matrices aren't vectors in sage, so we have to
619 # appeal to the "long vectors" isometry.
620 oper_vecs = [ _mat2vec(g.operator().matrix()) for g in self.gens() ]
621
622 # Now we use basis linear algebra to find the coefficients,
623 # of the matrices-as-vectors-linear-combination, which should
624 # work for the original algebra basis too.
625 A = matrix.column(self.base_ring(), oper_vecs)
626
627 # We used the isometry on the left-hand side already, but we
628 # still need to do it for the right-hand side. Recall that we
629 # wanted something that summed to the identity matrix.
630 b = _mat2vec( matrix.identity(self.base_ring(), self.dimension()) )
631
632 # Now if there's an identity element in the algebra, this should work.
633 coeffs = A.solve_right(b)
634 return self.linear_combination(zip(self.gens(), coeffs))
635
636
637 def peirce_decomposition(self, c):
638 """
639 The Peirce decomposition of this algebra relative to the
640 idempotent ``c``.
641
642 In the future, this can be extended to a complete system of
643 orthogonal idempotents.
644 """
645 if not c.is_idempotent():
646 raise ValueError("element is not idempotent: %s" % c)
647
648 # Default these to what they should be if they turn out to be
649 # trivial, because eigenspaces_left() won't return eigenvalues
650 # corresponding to trivial spaces (e.g. it returns only the
651 # eigenspace corresponding to lambda=1 if you take the
652 # decomposition relative to the identity element).
653 trivial = FiniteDimensionalEuclideanJordanSubalgebra(self, ())
654 J0 = trivial # eigenvalue zero
655 J2 = trivial # eigenvalue one-half
656 J1 = trivial # eigenvalue one
657
658 for (eigval, eigspace) in c.operator().matrix().left_eigenspaces():
659 gens = tuple( self.from_vector(b) for b in eigspace.basis() )
660 subalg = FiniteDimensionalEuclideanJordanSubalgebra(self, gens)
661 if eigval == 0:
662 J0 = subalg
663 elif eigval == ~(self.base_ring()(2)):
664 J2 = subalg
665 elif eigval == 1:
666 J1 = subalg
667 else:
668 raise ValueError("unexpected eigenvalue: %s" % eigval)
669
670 return (J0, J2, J1)
671
672
673 def random_elements(self, count):
674 """
675 Return ``count`` random elements as a tuple.
676
677 SETUP::
678
679 sage: from mjo.eja.eja_algebra import JordanSpinEJA
680
681 EXAMPLES::
682
683 sage: J = JordanSpinEJA(3)
684 sage: x,y,z = J.random_elements(3)
685 sage: all( [ x in J, y in J, z in J ])
686 True
687 sage: len( J.random_elements(10) ) == 10
688 True
689
690 """
691 return tuple( self.random_element() for idx in xrange(count) )
692
693
694 def rank(self):
695 """
696 Return the rank of this EJA.
697
698 ALGORITHM:
699
700 The author knows of no algorithm to compute the rank of an EJA
701 where only the multiplication table is known. In lieu of one, we
702 require the rank to be specified when the algebra is created,
703 and simply pass along that number here.
704
705 SETUP::
706
707 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
708 ....: RealSymmetricEJA,
709 ....: ComplexHermitianEJA,
710 ....: QuaternionHermitianEJA,
711 ....: random_eja)
712
713 EXAMPLES:
714
715 The rank of the Jordan spin algebra is always two::
716
717 sage: JordanSpinEJA(2).rank()
718 2
719 sage: JordanSpinEJA(3).rank()
720 2
721 sage: JordanSpinEJA(4).rank()
722 2
723
724 The rank of the `n`-by-`n` Hermitian real, complex, or
725 quaternion matrices is `n`::
726
727 sage: RealSymmetricEJA(4).rank()
728 4
729 sage: ComplexHermitianEJA(3).rank()
730 3
731 sage: QuaternionHermitianEJA(2).rank()
732 2
733
734 TESTS:
735
736 Ensure that every EJA that we know how to construct has a
737 positive integer rank, unless the algebra is trivial in
738 which case its rank will be zero::
739
740 sage: set_random_seed()
741 sage: J = random_eja()
742 sage: r = J.rank()
743 sage: r in ZZ
744 True
745 sage: r > 0 or (r == 0 and J.is_trivial())
746 True
747
748 """
749 return self._rank
750
751
752 def vector_space(self):
753 """
754 Return the vector space that underlies this algebra.
755
756 SETUP::
757
758 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
759
760 EXAMPLES::
761
762 sage: J = RealSymmetricEJA(2)
763 sage: J.vector_space()
764 Vector space of dimension 3 over...
765
766 """
767 return self.zero().to_vector().parent().ambient_vector_space()
768
769
770 Element = FiniteDimensionalEuclideanJordanAlgebraElement
771
772
773 class KnownRankEJA(object):
774 """
775 A class for algebras that we actually know we can construct. The
776 main issue is that, for most of our methods to make sense, we need
777 to know the rank of our algebra. Thus we can't simply generate a
778 "random" algebra, or even check that a given basis and product
779 satisfy the axioms; because even if everything looks OK, we wouldn't
780 know the rank we need to actuallty build the thing.
781
782 Not really a subclass of FDEJA because doing that causes method
783 resolution errors, e.g.
784
785 TypeError: Error when calling the metaclass bases
786 Cannot create a consistent method resolution
787 order (MRO) for bases FiniteDimensionalEuclideanJordanAlgebra,
788 KnownRankEJA
789
790 """
791 @staticmethod
792 def _max_test_case_size():
793 """
794 Return an integer "size" that is an upper bound on the size of
795 this algebra when it is used in a random test
796 case. Unfortunately, the term "size" is quite vague -- when
797 dealing with `R^n` under either the Hadamard or Jordan spin
798 product, the "size" refers to the dimension `n`. When dealing
799 with a matrix algebra (real symmetric or complex/quaternion
800 Hermitian), it refers to the size of the matrix, which is
801 far less than the dimension of the underlying vector space.
802
803 We default to five in this class, which is safe in `R^n`. The
804 matrix algebra subclasses (or any class where the "size" is
805 interpreted to be far less than the dimension) should override
806 with a smaller number.
807 """
808 return 5
809
810 @classmethod
811 def random_instance(cls, field=QQ, **kwargs):
812 """
813 Return a random instance of this type of algebra.
814
815 Beware, this will crash for "most instances" because the
816 constructor below looks wrong.
817 """
818 if cls is TrivialEJA:
819 # The TrivialEJA class doesn't take an "n" argument because
820 # there's only one.
821 return cls(field)
822
823 n = ZZ.random_element(cls._max_test_case_size()) + 1
824 return cls(n, field, **kwargs)
825
826
827 class RealCartesianProductEJA(FiniteDimensionalEuclideanJordanAlgebra,
828 KnownRankEJA):
829 """
830 Return the Euclidean Jordan Algebra corresponding to the set
831 `R^n` under the Hadamard product.
832
833 Note: this is nothing more than the Cartesian product of ``n``
834 copies of the spin algebra. Once Cartesian product algebras
835 are implemented, this can go.
836
837 SETUP::
838
839 sage: from mjo.eja.eja_algebra import RealCartesianProductEJA
840
841 EXAMPLES:
842
843 This multiplication table can be verified by hand::
844
845 sage: J = RealCartesianProductEJA(3)
846 sage: e0,e1,e2 = J.gens()
847 sage: e0*e0
848 e0
849 sage: e0*e1
850 0
851 sage: e0*e2
852 0
853 sage: e1*e1
854 e1
855 sage: e1*e2
856 0
857 sage: e2*e2
858 e2
859
860 TESTS:
861
862 We can change the generator prefix::
863
864 sage: RealCartesianProductEJA(3, prefix='r').gens()
865 (r0, r1, r2)
866
867 """
868 def __init__(self, n, field=QQ, **kwargs):
869 V = VectorSpace(field, n)
870 mult_table = [ [ V.gen(i)*(i == j) for j in xrange(n) ]
871 for i in xrange(n) ]
872
873 fdeja = super(RealCartesianProductEJA, self)
874 return fdeja.__init__(field, mult_table, rank=n, **kwargs)
875
876 def inner_product(self, x, y):
877 """
878 Faster to reimplement than to use natural representations.
879
880 SETUP::
881
882 sage: from mjo.eja.eja_algebra import RealCartesianProductEJA
883
884 TESTS:
885
886 Ensure that this is the usual inner product for the algebras
887 over `R^n`::
888
889 sage: set_random_seed()
890 sage: J = RealCartesianProductEJA.random_instance()
891 sage: x,y = J.random_elements(2)
892 sage: X = x.natural_representation()
893 sage: Y = y.natural_representation()
894 sage: x.inner_product(y) == J.natural_inner_product(X,Y)
895 True
896
897 """
898 return x.to_vector().inner_product(y.to_vector())
899
900
901 def random_eja(field=QQ, nontrivial=False):
902 """
903 Return a "random" finite-dimensional Euclidean Jordan Algebra.
904
905 SETUP::
906
907 sage: from mjo.eja.eja_algebra import random_eja
908
909 TESTS::
910
911 sage: random_eja()
912 Euclidean Jordan algebra of dimension...
913
914 """
915 eja_classes = KnownRankEJA.__subclasses__()
916 if nontrivial:
917 eja_classes.remove(TrivialEJA)
918 classname = choice(eja_classes)
919 return classname.random_instance(field=field)
920
921
922
923
924
925
926 class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
927 @staticmethod
928 def _max_test_case_size():
929 # Play it safe, since this will be squared and the underlying
930 # field can have dimension 4 (quaternions) too.
931 return 2
932
933 def __init__(self, field, basis, rank, normalize_basis=True, **kwargs):
934 """
935 Compared to the superclass constructor, we take a basis instead of
936 a multiplication table because the latter can be computed in terms
937 of the former when the product is known (like it is here).
938 """
939 # Used in this class's fast _charpoly_coeff() override.
940 self._basis_normalizers = None
941
942 # We're going to loop through this a few times, so now's a good
943 # time to ensure that it isn't a generator expression.
944 basis = tuple(basis)
945
946 if rank > 1 and normalize_basis:
947 # We'll need sqrt(2) to normalize the basis, and this
948 # winds up in the multiplication table, so the whole
949 # algebra needs to be over the field extension.
950 R = PolynomialRing(field, 'z')
951 z = R.gen()
952 p = z**2 - 2
953 if p.is_irreducible():
954 field = field.extension(p, 'sqrt2', embedding=RLF(2).sqrt())
955 basis = tuple( s.change_ring(field) for s in basis )
956 self._basis_normalizers = tuple(
957 ~(self.natural_inner_product(s,s).sqrt()) for s in basis )
958 basis = tuple(s*c for (s,c) in izip(basis,self._basis_normalizers))
959
960 Qs = self.multiplication_table_from_matrix_basis(basis)
961
962 fdeja = super(MatrixEuclideanJordanAlgebra, self)
963 return fdeja.__init__(field,
964 Qs,
965 rank=rank,
966 natural_basis=basis,
967 **kwargs)
968
969
970 @cached_method
971 def _charpoly_coeff(self, i):
972 """
973 Override the parent method with something that tries to compute
974 over a faster (non-extension) field.
975 """
976 if self._basis_normalizers is None:
977 # We didn't normalize, so assume that the basis we started
978 # with had entries in a nice field.
979 return super(MatrixEuclideanJordanAlgebra, self)._charpoly_coeff(i)
980 else:
981 basis = ( (b/n) for (b,n) in izip(self.natural_basis(),
982 self._basis_normalizers) )
983
984 # Do this over the rationals and convert back at the end.
985 J = MatrixEuclideanJordanAlgebra(QQ,
986 basis,
987 self.rank(),
988 normalize_basis=False)
989 (_,x,_,_) = J._charpoly_matrix_system()
990 p = J._charpoly_coeff(i)
991 # p might be missing some vars, have to substitute "optionally"
992 pairs = izip(x.base_ring().gens(), self._basis_normalizers)
993 substitutions = { v: v*c for (v,c) in pairs }
994 result = p.subs(substitutions)
995
996 # The result of "subs" can be either a coefficient-ring
997 # element or a polynomial. Gotta handle both cases.
998 if result in QQ:
999 return self.base_ring()(result)
1000 else:
1001 return result.change_ring(self.base_ring())
1002
1003
1004 @staticmethod
1005 def multiplication_table_from_matrix_basis(basis):
1006 """
1007 At least three of the five simple Euclidean Jordan algebras have the
1008 symmetric multiplication (A,B) |-> (AB + BA)/2, where the
1009 multiplication on the right is matrix multiplication. Given a basis
1010 for the underlying matrix space, this function returns a
1011 multiplication table (obtained by looping through the basis
1012 elements) for an algebra of those matrices.
1013 """
1014 # In S^2, for example, we nominally have four coordinates even
1015 # though the space is of dimension three only. The vector space V
1016 # is supposed to hold the entire long vector, and the subspace W
1017 # of V will be spanned by the vectors that arise from symmetric
1018 # matrices. Thus for S^2, dim(V) == 4 and dim(W) == 3.
1019 field = basis[0].base_ring()
1020 dimension = basis[0].nrows()
1021
1022 V = VectorSpace(field, dimension**2)
1023 W = V.span_of_basis( _mat2vec(s) for s in basis )
1024 n = len(basis)
1025 mult_table = [[W.zero() for j in xrange(n)] for i in xrange(n)]
1026 for i in xrange(n):
1027 for j in xrange(n):
1028 mat_entry = (basis[i]*basis[j] + basis[j]*basis[i])/2
1029 mult_table[i][j] = W.coordinate_vector(_mat2vec(mat_entry))
1030
1031 return mult_table
1032
1033
1034 @staticmethod
1035 def real_embed(M):
1036 """
1037 Embed the matrix ``M`` into a space of real matrices.
1038
1039 The matrix ``M`` can have entries in any field at the moment:
1040 the real numbers, complex numbers, or quaternions. And although
1041 they are not a field, we can probably support octonions at some
1042 point, too. This function returns a real matrix that "acts like"
1043 the original with respect to matrix multiplication; i.e.
1044
1045 real_embed(M*N) = real_embed(M)*real_embed(N)
1046
1047 """
1048 raise NotImplementedError
1049
1050
1051 @staticmethod
1052 def real_unembed(M):
1053 """
1054 The inverse of :meth:`real_embed`.
1055 """
1056 raise NotImplementedError
1057
1058
1059 @classmethod
1060 def natural_inner_product(cls,X,Y):
1061 Xu = cls.real_unembed(X)
1062 Yu = cls.real_unembed(Y)
1063 tr = (Xu*Yu).trace()
1064
1065 if tr in RLF:
1066 # It's real already.
1067 return tr
1068
1069 # Otherwise, try the thing that works for complex numbers; and
1070 # if that doesn't work, the thing that works for quaternions.
1071 try:
1072 return tr.vector()[0] # real part, imag part is index 1
1073 except AttributeError:
1074 # A quaternions doesn't have a vector() method, but does
1075 # have coefficient_tuple() method that returns the
1076 # coefficients of 1, i, j, and k -- in that order.
1077 return tr.coefficient_tuple()[0]
1078
1079
1080 class RealMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
1081 @staticmethod
1082 def real_embed(M):
1083 """
1084 The identity function, for embedding real matrices into real
1085 matrices.
1086 """
1087 return M
1088
1089 @staticmethod
1090 def real_unembed(M):
1091 """
1092 The identity function, for unembedding real matrices from real
1093 matrices.
1094 """
1095 return M
1096
1097
1098 class RealSymmetricEJA(RealMatrixEuclideanJordanAlgebra, KnownRankEJA):
1099 """
1100 The rank-n simple EJA consisting of real symmetric n-by-n
1101 matrices, the usual symmetric Jordan product, and the trace inner
1102 product. It has dimension `(n^2 + n)/2` over the reals.
1103
1104 SETUP::
1105
1106 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1107
1108 EXAMPLES::
1109
1110 sage: J = RealSymmetricEJA(2)
1111 sage: e0, e1, e2 = J.gens()
1112 sage: e0*e0
1113 e0
1114 sage: e1*e1
1115 1/2*e0 + 1/2*e2
1116 sage: e2*e2
1117 e2
1118
1119 In theory, our "field" can be any subfield of the reals::
1120
1121 sage: RealSymmetricEJA(2, AA)
1122 Euclidean Jordan algebra of dimension 3 over Algebraic Real Field
1123 sage: RealSymmetricEJA(2, RR)
1124 Euclidean Jordan algebra of dimension 3 over Real Field with
1125 53 bits of precision
1126
1127 TESTS:
1128
1129 The dimension of this algebra is `(n^2 + n) / 2`::
1130
1131 sage: set_random_seed()
1132 sage: n_max = RealSymmetricEJA._max_test_case_size()
1133 sage: n = ZZ.random_element(1, n_max)
1134 sage: J = RealSymmetricEJA(n)
1135 sage: J.dimension() == (n^2 + n)/2
1136 True
1137
1138 The Jordan multiplication is what we think it is::
1139
1140 sage: set_random_seed()
1141 sage: J = RealSymmetricEJA.random_instance()
1142 sage: x,y = J.random_elements(2)
1143 sage: actual = (x*y).natural_representation()
1144 sage: X = x.natural_representation()
1145 sage: Y = y.natural_representation()
1146 sage: expected = (X*Y + Y*X)/2
1147 sage: actual == expected
1148 True
1149 sage: J(expected) == x*y
1150 True
1151
1152 We can change the generator prefix::
1153
1154 sage: RealSymmetricEJA(3, prefix='q').gens()
1155 (q0, q1, q2, q3, q4, q5)
1156
1157 Our natural basis is normalized with respect to the natural inner
1158 product unless we specify otherwise::
1159
1160 sage: set_random_seed()
1161 sage: J = RealSymmetricEJA.random_instance()
1162 sage: all( b.norm() == 1 for b in J.gens() )
1163 True
1164
1165 Since our natural basis is normalized with respect to the natural
1166 inner product, and since we know that this algebra is an EJA, any
1167 left-multiplication operator's matrix will be symmetric because
1168 natural->EJA basis representation is an isometry and within the EJA
1169 the operator is self-adjoint by the Jordan axiom::
1170
1171 sage: set_random_seed()
1172 sage: x = RealSymmetricEJA.random_instance().random_element()
1173 sage: x.operator().matrix().is_symmetric()
1174 True
1175
1176 """
1177 @classmethod
1178 def _denormalized_basis(cls, n, field):
1179 """
1180 Return a basis for the space of real symmetric n-by-n matrices.
1181
1182 SETUP::
1183
1184 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1185
1186 TESTS::
1187
1188 sage: set_random_seed()
1189 sage: n = ZZ.random_element(1,5)
1190 sage: B = RealSymmetricEJA._denormalized_basis(n,QQ)
1191 sage: all( M.is_symmetric() for M in B)
1192 True
1193
1194 """
1195 # The basis of symmetric matrices, as matrices, in their R^(n-by-n)
1196 # coordinates.
1197 S = []
1198 for i in xrange(n):
1199 for j in xrange(i+1):
1200 Eij = matrix(field, n, lambda k,l: k==i and l==j)
1201 if i == j:
1202 Sij = Eij
1203 else:
1204 Sij = Eij + Eij.transpose()
1205 S.append(Sij)
1206 return S
1207
1208
1209 @staticmethod
1210 def _max_test_case_size():
1211 return 4 # Dimension 10
1212
1213
1214 def __init__(self, n, field=QQ, **kwargs):
1215 basis = self._denormalized_basis(n, field)
1216 super(RealSymmetricEJA, self).__init__(field, basis, n, **kwargs)
1217
1218
1219 class ComplexMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
1220 @staticmethod
1221 def real_embed(M):
1222 """
1223 Embed the n-by-n complex matrix ``M`` into the space of real
1224 matrices of size 2n-by-2n via the map the sends each entry `z = a +
1225 bi` to the block matrix ``[[a,b],[-b,a]]``.
1226
1227 SETUP::
1228
1229 sage: from mjo.eja.eja_algebra import \
1230 ....: ComplexMatrixEuclideanJordanAlgebra
1231
1232 EXAMPLES::
1233
1234 sage: F = QuadraticField(-1, 'i')
1235 sage: x1 = F(4 - 2*i)
1236 sage: x2 = F(1 + 2*i)
1237 sage: x3 = F(-i)
1238 sage: x4 = F(6)
1239 sage: M = matrix(F,2,[[x1,x2],[x3,x4]])
1240 sage: ComplexMatrixEuclideanJordanAlgebra.real_embed(M)
1241 [ 4 -2| 1 2]
1242 [ 2 4|-2 1]
1243 [-----+-----]
1244 [ 0 -1| 6 0]
1245 [ 1 0| 0 6]
1246
1247 TESTS:
1248
1249 Embedding is a homomorphism (isomorphism, in fact)::
1250
1251 sage: set_random_seed()
1252 sage: n_max = ComplexMatrixEuclideanJordanAlgebra._max_test_case_size()
1253 sage: n = ZZ.random_element(n_max)
1254 sage: F = QuadraticField(-1, 'i')
1255 sage: X = random_matrix(F, n)
1256 sage: Y = random_matrix(F, n)
1257 sage: Xe = ComplexMatrixEuclideanJordanAlgebra.real_embed(X)
1258 sage: Ye = ComplexMatrixEuclideanJordanAlgebra.real_embed(Y)
1259 sage: XYe = ComplexMatrixEuclideanJordanAlgebra.real_embed(X*Y)
1260 sage: Xe*Ye == XYe
1261 True
1262
1263 """
1264 n = M.nrows()
1265 if M.ncols() != n:
1266 raise ValueError("the matrix 'M' must be square")
1267
1268 # We don't need any adjoined elements...
1269 field = M.base_ring().base_ring()
1270
1271 blocks = []
1272 for z in M.list():
1273 a = z.list()[0] # real part, I guess
1274 b = z.list()[1] # imag part, I guess
1275 blocks.append(matrix(field, 2, [[a,b],[-b,a]]))
1276
1277 return matrix.block(field, n, blocks)
1278
1279
1280 @staticmethod
1281 def real_unembed(M):
1282 """
1283 The inverse of _embed_complex_matrix().
1284
1285 SETUP::
1286
1287 sage: from mjo.eja.eja_algebra import \
1288 ....: ComplexMatrixEuclideanJordanAlgebra
1289
1290 EXAMPLES::
1291
1292 sage: A = matrix(QQ,[ [ 1, 2, 3, 4],
1293 ....: [-2, 1, -4, 3],
1294 ....: [ 9, 10, 11, 12],
1295 ....: [-10, 9, -12, 11] ])
1296 sage: ComplexMatrixEuclideanJordanAlgebra.real_unembed(A)
1297 [ 2*i + 1 4*i + 3]
1298 [ 10*i + 9 12*i + 11]
1299
1300 TESTS:
1301
1302 Unembedding is the inverse of embedding::
1303
1304 sage: set_random_seed()
1305 sage: F = QuadraticField(-1, 'i')
1306 sage: M = random_matrix(F, 3)
1307 sage: Me = ComplexMatrixEuclideanJordanAlgebra.real_embed(M)
1308 sage: ComplexMatrixEuclideanJordanAlgebra.real_unembed(Me) == M
1309 True
1310
1311 """
1312 n = ZZ(M.nrows())
1313 if M.ncols() != n:
1314 raise ValueError("the matrix 'M' must be square")
1315 if not n.mod(2).is_zero():
1316 raise ValueError("the matrix 'M' must be a complex embedding")
1317
1318 # If "M" was normalized, its base ring might have roots
1319 # adjoined and they can stick around after unembedding.
1320 field = M.base_ring()
1321 R = PolynomialRing(field, 'z')
1322 z = R.gen()
1323 F = field.extension(z**2 + 1, 'i', embedding=CLF(-1).sqrt())
1324 i = F.gen()
1325
1326 # Go top-left to bottom-right (reading order), converting every
1327 # 2-by-2 block we see to a single complex element.
1328 elements = []
1329 for k in xrange(n/2):
1330 for j in xrange(n/2):
1331 submat = M[2*k:2*k+2,2*j:2*j+2]
1332 if submat[0,0] != submat[1,1]:
1333 raise ValueError('bad on-diagonal submatrix')
1334 if submat[0,1] != -submat[1,0]:
1335 raise ValueError('bad off-diagonal submatrix')
1336 z = submat[0,0] + submat[0,1]*i
1337 elements.append(z)
1338
1339 return matrix(F, n/2, elements)
1340
1341
1342 @classmethod
1343 def natural_inner_product(cls,X,Y):
1344 """
1345 Compute a natural inner product in this algebra directly from
1346 its real embedding.
1347
1348 SETUP::
1349
1350 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1351
1352 TESTS:
1353
1354 This gives the same answer as the slow, default method implemented
1355 in :class:`MatrixEuclideanJordanAlgebra`::
1356
1357 sage: set_random_seed()
1358 sage: J = ComplexHermitianEJA.random_instance()
1359 sage: x,y = J.random_elements(2)
1360 sage: Xe = x.natural_representation()
1361 sage: Ye = y.natural_representation()
1362 sage: X = ComplexHermitianEJA.real_unembed(Xe)
1363 sage: Y = ComplexHermitianEJA.real_unembed(Ye)
1364 sage: expected = (X*Y).trace().vector()[0]
1365 sage: actual = ComplexHermitianEJA.natural_inner_product(Xe,Ye)
1366 sage: actual == expected
1367 True
1368
1369 """
1370 return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/2
1371
1372
1373 class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra, KnownRankEJA):
1374 """
1375 The rank-n simple EJA consisting of complex Hermitian n-by-n
1376 matrices over the real numbers, the usual symmetric Jordan product,
1377 and the real-part-of-trace inner product. It has dimension `n^2` over
1378 the reals.
1379
1380 SETUP::
1381
1382 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1383
1384 EXAMPLES:
1385
1386 In theory, our "field" can be any subfield of the reals::
1387
1388 sage: ComplexHermitianEJA(2, AA)
1389 Euclidean Jordan algebra of dimension 4 over Algebraic Real Field
1390 sage: ComplexHermitianEJA(2, RR)
1391 Euclidean Jordan algebra of dimension 4 over Real Field with
1392 53 bits of precision
1393
1394 TESTS:
1395
1396 The dimension of this algebra is `n^2`::
1397
1398 sage: set_random_seed()
1399 sage: n_max = ComplexHermitianEJA._max_test_case_size()
1400 sage: n = ZZ.random_element(1, n_max)
1401 sage: J = ComplexHermitianEJA(n)
1402 sage: J.dimension() == n^2
1403 True
1404
1405 The Jordan multiplication is what we think it is::
1406
1407 sage: set_random_seed()
1408 sage: J = ComplexHermitianEJA.random_instance()
1409 sage: x,y = J.random_elements(2)
1410 sage: actual = (x*y).natural_representation()
1411 sage: X = x.natural_representation()
1412 sage: Y = y.natural_representation()
1413 sage: expected = (X*Y + Y*X)/2
1414 sage: actual == expected
1415 True
1416 sage: J(expected) == x*y
1417 True
1418
1419 We can change the generator prefix::
1420
1421 sage: ComplexHermitianEJA(2, prefix='z').gens()
1422 (z0, z1, z2, z3)
1423
1424 Our natural basis is normalized with respect to the natural inner
1425 product unless we specify otherwise::
1426
1427 sage: set_random_seed()
1428 sage: J = ComplexHermitianEJA.random_instance()
1429 sage: all( b.norm() == 1 for b in J.gens() )
1430 True
1431
1432 Since our natural basis is normalized with respect to the natural
1433 inner product, and since we know that this algebra is an EJA, any
1434 left-multiplication operator's matrix will be symmetric because
1435 natural->EJA basis representation is an isometry and within the EJA
1436 the operator is self-adjoint by the Jordan axiom::
1437
1438 sage: set_random_seed()
1439 sage: x = ComplexHermitianEJA.random_instance().random_element()
1440 sage: x.operator().matrix().is_symmetric()
1441 True
1442
1443 """
1444
1445 @classmethod
1446 def _denormalized_basis(cls, n, field):
1447 """
1448 Returns a basis for the space of complex Hermitian n-by-n matrices.
1449
1450 Why do we embed these? Basically, because all of numerical linear
1451 algebra assumes that you're working with vectors consisting of `n`
1452 entries from a field and scalars from the same field. There's no way
1453 to tell SageMath that (for example) the vectors contain complex
1454 numbers, while the scalar field is real.
1455
1456 SETUP::
1457
1458 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1459
1460 TESTS::
1461
1462 sage: set_random_seed()
1463 sage: n = ZZ.random_element(1,5)
1464 sage: field = QuadraticField(2, 'sqrt2')
1465 sage: B = ComplexHermitianEJA._denormalized_basis(n, field)
1466 sage: all( M.is_symmetric() for M in B)
1467 True
1468
1469 """
1470 R = PolynomialRing(field, 'z')
1471 z = R.gen()
1472 F = field.extension(z**2 + 1, 'I')
1473 I = F.gen()
1474
1475 # This is like the symmetric case, but we need to be careful:
1476 #
1477 # * We want conjugate-symmetry, not just symmetry.
1478 # * The diagonal will (as a result) be real.
1479 #
1480 S = []
1481 for i in xrange(n):
1482 for j in xrange(i+1):
1483 Eij = matrix(F, n, lambda k,l: k==i and l==j)
1484 if i == j:
1485 Sij = cls.real_embed(Eij)
1486 S.append(Sij)
1487 else:
1488 # The second one has a minus because it's conjugated.
1489 Sij_real = cls.real_embed(Eij + Eij.transpose())
1490 S.append(Sij_real)
1491 Sij_imag = cls.real_embed(I*Eij - I*Eij.transpose())
1492 S.append(Sij_imag)
1493
1494 # Since we embedded these, we can drop back to the "field" that we
1495 # started with instead of the complex extension "F".
1496 return ( s.change_ring(field) for s in S )
1497
1498
1499 def __init__(self, n, field=QQ, **kwargs):
1500 basis = self._denormalized_basis(n,field)
1501 super(ComplexHermitianEJA,self).__init__(field, basis, n, **kwargs)
1502
1503
1504 class QuaternionMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
1505 @staticmethod
1506 def real_embed(M):
1507 """
1508 Embed the n-by-n quaternion matrix ``M`` into the space of real
1509 matrices of size 4n-by-4n by first sending each quaternion entry `z
1510 = a + bi + cj + dk` to the block-complex matrix ``[[a + bi,
1511 c+di],[-c + di, a-bi]]`, and then embedding those into a real
1512 matrix.
1513
1514 SETUP::
1515
1516 sage: from mjo.eja.eja_algebra import \
1517 ....: QuaternionMatrixEuclideanJordanAlgebra
1518
1519 EXAMPLES::
1520
1521 sage: Q = QuaternionAlgebra(QQ,-1,-1)
1522 sage: i,j,k = Q.gens()
1523 sage: x = 1 + 2*i + 3*j + 4*k
1524 sage: M = matrix(Q, 1, [[x]])
1525 sage: QuaternionMatrixEuclideanJordanAlgebra.real_embed(M)
1526 [ 1 2 3 4]
1527 [-2 1 -4 3]
1528 [-3 4 1 -2]
1529 [-4 -3 2 1]
1530
1531 Embedding is a homomorphism (isomorphism, in fact)::
1532
1533 sage: set_random_seed()
1534 sage: n_max = QuaternionMatrixEuclideanJordanAlgebra._max_test_case_size()
1535 sage: n = ZZ.random_element(n_max)
1536 sage: Q = QuaternionAlgebra(QQ,-1,-1)
1537 sage: X = random_matrix(Q, n)
1538 sage: Y = random_matrix(Q, n)
1539 sage: Xe = QuaternionMatrixEuclideanJordanAlgebra.real_embed(X)
1540 sage: Ye = QuaternionMatrixEuclideanJordanAlgebra.real_embed(Y)
1541 sage: XYe = QuaternionMatrixEuclideanJordanAlgebra.real_embed(X*Y)
1542 sage: Xe*Ye == XYe
1543 True
1544
1545 """
1546 quaternions = M.base_ring()
1547 n = M.nrows()
1548 if M.ncols() != n:
1549 raise ValueError("the matrix 'M' must be square")
1550
1551 F = QuadraticField(-1, 'i')
1552 i = F.gen()
1553
1554 blocks = []
1555 for z in M.list():
1556 t = z.coefficient_tuple()
1557 a = t[0]
1558 b = t[1]
1559 c = t[2]
1560 d = t[3]
1561 cplxM = matrix(F, 2, [[ a + b*i, c + d*i],
1562 [-c + d*i, a - b*i]])
1563 realM = ComplexMatrixEuclideanJordanAlgebra.real_embed(cplxM)
1564 blocks.append(realM)
1565
1566 # We should have real entries by now, so use the realest field
1567 # we've got for the return value.
1568 return matrix.block(quaternions.base_ring(), n, blocks)
1569
1570
1571
1572 @staticmethod
1573 def real_unembed(M):
1574 """
1575 The inverse of _embed_quaternion_matrix().
1576
1577 SETUP::
1578
1579 sage: from mjo.eja.eja_algebra import \
1580 ....: QuaternionMatrixEuclideanJordanAlgebra
1581
1582 EXAMPLES::
1583
1584 sage: M = matrix(QQ, [[ 1, 2, 3, 4],
1585 ....: [-2, 1, -4, 3],
1586 ....: [-3, 4, 1, -2],
1587 ....: [-4, -3, 2, 1]])
1588 sage: QuaternionMatrixEuclideanJordanAlgebra.real_unembed(M)
1589 [1 + 2*i + 3*j + 4*k]
1590
1591 TESTS:
1592
1593 Unembedding is the inverse of embedding::
1594
1595 sage: set_random_seed()
1596 sage: Q = QuaternionAlgebra(QQ, -1, -1)
1597 sage: M = random_matrix(Q, 3)
1598 sage: Me = QuaternionMatrixEuclideanJordanAlgebra.real_embed(M)
1599 sage: QuaternionMatrixEuclideanJordanAlgebra.real_unembed(Me) == M
1600 True
1601
1602 """
1603 n = ZZ(M.nrows())
1604 if M.ncols() != n:
1605 raise ValueError("the matrix 'M' must be square")
1606 if not n.mod(4).is_zero():
1607 raise ValueError("the matrix 'M' must be a quaternion embedding")
1608
1609 # Use the base ring of the matrix to ensure that its entries can be
1610 # multiplied by elements of the quaternion algebra.
1611 field = M.base_ring()
1612 Q = QuaternionAlgebra(field,-1,-1)
1613 i,j,k = Q.gens()
1614
1615 # Go top-left to bottom-right (reading order), converting every
1616 # 4-by-4 block we see to a 2-by-2 complex block, to a 1-by-1
1617 # quaternion block.
1618 elements = []
1619 for l in xrange(n/4):
1620 for m in xrange(n/4):
1621 submat = ComplexMatrixEuclideanJordanAlgebra.real_unembed(
1622 M[4*l:4*l+4,4*m:4*m+4] )
1623 if submat[0,0] != submat[1,1].conjugate():
1624 raise ValueError('bad on-diagonal submatrix')
1625 if submat[0,1] != -submat[1,0].conjugate():
1626 raise ValueError('bad off-diagonal submatrix')
1627 z = submat[0,0].vector()[0] # real part
1628 z += submat[0,0].vector()[1]*i # imag part
1629 z += submat[0,1].vector()[0]*j # real part
1630 z += submat[0,1].vector()[1]*k # imag part
1631 elements.append(z)
1632
1633 return matrix(Q, n/4, elements)
1634
1635
1636 @classmethod
1637 def natural_inner_product(cls,X,Y):
1638 """
1639 Compute a natural inner product in this algebra directly from
1640 its real embedding.
1641
1642 SETUP::
1643
1644 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
1645
1646 TESTS:
1647
1648 This gives the same answer as the slow, default method implemented
1649 in :class:`MatrixEuclideanJordanAlgebra`::
1650
1651 sage: set_random_seed()
1652 sage: J = QuaternionHermitianEJA.random_instance()
1653 sage: x,y = J.random_elements(2)
1654 sage: Xe = x.natural_representation()
1655 sage: Ye = y.natural_representation()
1656 sage: X = QuaternionHermitianEJA.real_unembed(Xe)
1657 sage: Y = QuaternionHermitianEJA.real_unembed(Ye)
1658 sage: expected = (X*Y).trace().coefficient_tuple()[0]
1659 sage: actual = QuaternionHermitianEJA.natural_inner_product(Xe,Ye)
1660 sage: actual == expected
1661 True
1662
1663 """
1664 return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/4
1665
1666
1667 class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra,
1668 KnownRankEJA):
1669 """
1670 The rank-n simple EJA consisting of self-adjoint n-by-n quaternion
1671 matrices, the usual symmetric Jordan product, and the
1672 real-part-of-trace inner product. It has dimension `2n^2 - n` over
1673 the reals.
1674
1675 SETUP::
1676
1677 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
1678
1679 EXAMPLES:
1680
1681 In theory, our "field" can be any subfield of the reals::
1682
1683 sage: QuaternionHermitianEJA(2, AA)
1684 Euclidean Jordan algebra of dimension 6 over Algebraic Real Field
1685 sage: QuaternionHermitianEJA(2, RR)
1686 Euclidean Jordan algebra of dimension 6 over Real Field with
1687 53 bits of precision
1688
1689 TESTS:
1690
1691 The dimension of this algebra is `2*n^2 - n`::
1692
1693 sage: set_random_seed()
1694 sage: n_max = QuaternionHermitianEJA._max_test_case_size()
1695 sage: n = ZZ.random_element(1, n_max)
1696 sage: J = QuaternionHermitianEJA(n)
1697 sage: J.dimension() == 2*(n^2) - n
1698 True
1699
1700 The Jordan multiplication is what we think it is::
1701
1702 sage: set_random_seed()
1703 sage: J = QuaternionHermitianEJA.random_instance()
1704 sage: x,y = J.random_elements(2)
1705 sage: actual = (x*y).natural_representation()
1706 sage: X = x.natural_representation()
1707 sage: Y = y.natural_representation()
1708 sage: expected = (X*Y + Y*X)/2
1709 sage: actual == expected
1710 True
1711 sage: J(expected) == x*y
1712 True
1713
1714 We can change the generator prefix::
1715
1716 sage: QuaternionHermitianEJA(2, prefix='a').gens()
1717 (a0, a1, a2, a3, a4, a5)
1718
1719 Our natural basis is normalized with respect to the natural inner
1720 product unless we specify otherwise::
1721
1722 sage: set_random_seed()
1723 sage: J = QuaternionHermitianEJA.random_instance()
1724 sage: all( b.norm() == 1 for b in J.gens() )
1725 True
1726
1727 Since our natural basis is normalized with respect to the natural
1728 inner product, and since we know that this algebra is an EJA, any
1729 left-multiplication operator's matrix will be symmetric because
1730 natural->EJA basis representation is an isometry and within the EJA
1731 the operator is self-adjoint by the Jordan axiom::
1732
1733 sage: set_random_seed()
1734 sage: x = QuaternionHermitianEJA.random_instance().random_element()
1735 sage: x.operator().matrix().is_symmetric()
1736 True
1737
1738 """
1739 @classmethod
1740 def _denormalized_basis(cls, n, field):
1741 """
1742 Returns a basis for the space of quaternion Hermitian n-by-n matrices.
1743
1744 Why do we embed these? Basically, because all of numerical
1745 linear algebra assumes that you're working with vectors consisting
1746 of `n` entries from a field and scalars from the same field. There's
1747 no way to tell SageMath that (for example) the vectors contain
1748 complex numbers, while the scalar field is real.
1749
1750 SETUP::
1751
1752 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
1753
1754 TESTS::
1755
1756 sage: set_random_seed()
1757 sage: n = ZZ.random_element(1,5)
1758 sage: B = QuaternionHermitianEJA._denormalized_basis(n,QQ)
1759 sage: all( M.is_symmetric() for M in B )
1760 True
1761
1762 """
1763 Q = QuaternionAlgebra(QQ,-1,-1)
1764 I,J,K = Q.gens()
1765
1766 # This is like the symmetric case, but we need to be careful:
1767 #
1768 # * We want conjugate-symmetry, not just symmetry.
1769 # * The diagonal will (as a result) be real.
1770 #
1771 S = []
1772 for i in xrange(n):
1773 for j in xrange(i+1):
1774 Eij = matrix(Q, n, lambda k,l: k==i and l==j)
1775 if i == j:
1776 Sij = cls.real_embed(Eij)
1777 S.append(Sij)
1778 else:
1779 # The second, third, and fourth ones have a minus
1780 # because they're conjugated.
1781 Sij_real = cls.real_embed(Eij + Eij.transpose())
1782 S.append(Sij_real)
1783 Sij_I = cls.real_embed(I*Eij - I*Eij.transpose())
1784 S.append(Sij_I)
1785 Sij_J = cls.real_embed(J*Eij - J*Eij.transpose())
1786 S.append(Sij_J)
1787 Sij_K = cls.real_embed(K*Eij - K*Eij.transpose())
1788 S.append(Sij_K)
1789
1790 # Since we embedded these, we can drop back to the "field" that we
1791 # started with instead of the quaternion algebra "Q".
1792 return ( s.change_ring(field) for s in S )
1793
1794
1795 def __init__(self, n, field=QQ, **kwargs):
1796 basis = self._denormalized_basis(n,field)
1797 super(QuaternionHermitianEJA,self).__init__(field, basis, n, **kwargs)
1798
1799
1800 class JordanSpinEJA(FiniteDimensionalEuclideanJordanAlgebra, KnownRankEJA):
1801 """
1802 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
1803 with the usual inner product and jordan product ``x*y =
1804 (<x_bar,y_bar>, x0*y_bar + y0*x_bar)``. It has dimension `n` over
1805 the reals.
1806
1807 SETUP::
1808
1809 sage: from mjo.eja.eja_algebra import JordanSpinEJA
1810
1811 EXAMPLES:
1812
1813 This multiplication table can be verified by hand::
1814
1815 sage: J = JordanSpinEJA(4)
1816 sage: e0,e1,e2,e3 = J.gens()
1817 sage: e0*e0
1818 e0
1819 sage: e0*e1
1820 e1
1821 sage: e0*e2
1822 e2
1823 sage: e0*e3
1824 e3
1825 sage: e1*e2
1826 0
1827 sage: e1*e3
1828 0
1829 sage: e2*e3
1830 0
1831
1832 We can change the generator prefix::
1833
1834 sage: JordanSpinEJA(2, prefix='B').gens()
1835 (B0, B1)
1836
1837 """
1838 def __init__(self, n, field=QQ, **kwargs):
1839 V = VectorSpace(field, n)
1840 mult_table = [[V.zero() for j in xrange(n)] for i in xrange(n)]
1841 for i in xrange(n):
1842 for j in xrange(n):
1843 x = V.gen(i)
1844 y = V.gen(j)
1845 x0 = x[0]
1846 xbar = x[1:]
1847 y0 = y[0]
1848 ybar = y[1:]
1849 # z = x*y
1850 z0 = x.inner_product(y)
1851 zbar = y0*xbar + x0*ybar
1852 z = V([z0] + zbar.list())
1853 mult_table[i][j] = z
1854
1855 # The rank of the spin algebra is two, unless we're in a
1856 # one-dimensional ambient space (because the rank is bounded by
1857 # the ambient dimension).
1858 fdeja = super(JordanSpinEJA, self)
1859 return fdeja.__init__(field, mult_table, rank=min(n,2), **kwargs)
1860
1861 def inner_product(self, x, y):
1862 """
1863 Faster to reimplement than to use natural representations.
1864
1865 SETUP::
1866
1867 sage: from mjo.eja.eja_algebra import JordanSpinEJA
1868
1869 TESTS:
1870
1871 Ensure that this is the usual inner product for the algebras
1872 over `R^n`::
1873
1874 sage: set_random_seed()
1875 sage: J = JordanSpinEJA.random_instance()
1876 sage: x,y = J.random_elements(2)
1877 sage: X = x.natural_representation()
1878 sage: Y = y.natural_representation()
1879 sage: x.inner_product(y) == J.natural_inner_product(X,Y)
1880 True
1881
1882 """
1883 return x.to_vector().inner_product(y.to_vector())
1884
1885
1886 class TrivialEJA(FiniteDimensionalEuclideanJordanAlgebra, KnownRankEJA):
1887 """
1888 The trivial Euclidean Jordan algebra consisting of only a zero element.
1889
1890 SETUP::
1891
1892 sage: from mjo.eja.eja_algebra import TrivialEJA
1893
1894 EXAMPLES::
1895
1896 sage: J = TrivialEJA()
1897 sage: J.dimension()
1898 0
1899 sage: J.zero()
1900 0
1901 sage: J.one()
1902 0
1903 sage: 7*J.one()*12*J.one()
1904 0
1905 sage: J.one().inner_product(J.one())
1906 0
1907 sage: J.one().norm()
1908 0
1909 sage: J.one().subalgebra_generated_by()
1910 Euclidean Jordan algebra of dimension 0 over Rational Field
1911 sage: J.rank()
1912 0
1913
1914 """
1915 def __init__(self, field=QQ, **kwargs):
1916 mult_table = []
1917 fdeja = super(TrivialEJA, self)
1918 # The rank is zero using my definition, namely the dimension of the
1919 # largest subalgebra generated by any element.
1920 return fdeja.__init__(field, mult_table, rank=0, **kwargs)