]> gitweb.michael.orlitzky.com - sage.d.git/blob - mjo/eja/eja_algebra.py
eja: use zip() instead of izip(), which doesn't exist in python-3.x.
[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 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 INPUT:
646
647 - ``c`` -- an idempotent of this algebra.
648
649 OUTPUT:
650
651 A triple (J0, J5, J1) containing two subalgebras and one subspace
652 of this algebra,
653
654 - ``J0`` -- the algebra on the eigenspace of ``c.operator()``
655 corresponding to the eigenvalue zero.
656
657 - ``J5`` -- the eigenspace (NOT a subalgebra) of ``c.operator()``
658 corresponding to the eigenvalue one-half.
659
660 - ``J1`` -- the algebra on the eigenspace of ``c.operator()``
661 corresponding to the eigenvalue one.
662
663 These are the only possible eigenspaces for that operator, and this
664 algebra is a direct sum of them. The spaces ``J0`` and ``J1`` are
665 orthogonal, and are subalgebras of this algebra with the appropriate
666 restrictions.
667
668 SETUP::
669
670 sage: from mjo.eja.eja_algebra import random_eja, RealSymmetricEJA
671
672 EXAMPLES:
673
674 The canonical example comes from the symmetric matrices, which
675 decompose into diagonal and off-diagonal parts::
676
677 sage: J = RealSymmetricEJA(3)
678 sage: C = matrix(QQ, [ [1,0,0],
679 ....: [0,1,0],
680 ....: [0,0,0] ])
681 sage: c = J(C)
682 sage: J0,J5,J1 = J.peirce_decomposition(c)
683 sage: J0
684 Euclidean Jordan algebra of dimension 1...
685 sage: J5
686 Vector space of degree 6 and dimension 2...
687 sage: J1
688 Euclidean Jordan algebra of dimension 3...
689
690 TESTS:
691
692 Every algebra decomposes trivially with respect to its identity
693 element::
694
695 sage: set_random_seed()
696 sage: J = random_eja()
697 sage: J0,J5,J1 = J.peirce_decomposition(J.one())
698 sage: J0.dimension() == 0 and J5.dimension() == 0
699 True
700 sage: J1.superalgebra() == J and J1.dimension() == J.dimension()
701 True
702
703 The identity elements in the two subalgebras are the
704 projections onto their respective subspaces of the
705 superalgebra's identity element::
706
707 sage: set_random_seed()
708 sage: J = random_eja()
709 sage: x = J.random_element()
710 sage: if not J.is_trivial():
711 ....: while x.is_nilpotent():
712 ....: x = J.random_element()
713 sage: c = x.subalgebra_idempotent()
714 sage: J0,J5,J1 = J.peirce_decomposition(c)
715 sage: J1(c) == J1.one()
716 True
717 sage: J0(J.one() - c) == J0.one()
718 True
719
720 """
721 if not c.is_idempotent():
722 raise ValueError("element is not idempotent: %s" % c)
723
724 # Default these to what they should be if they turn out to be
725 # trivial, because eigenspaces_left() won't return eigenvalues
726 # corresponding to trivial spaces (e.g. it returns only the
727 # eigenspace corresponding to lambda=1 if you take the
728 # decomposition relative to the identity element).
729 trivial = FiniteDimensionalEuclideanJordanSubalgebra(self, ())
730 J0 = trivial # eigenvalue zero
731 J5 = VectorSpace(self.base_ring(), 0) # eigenvalue one-half
732 J1 = trivial # eigenvalue one
733
734 for (eigval, eigspace) in c.operator().matrix().left_eigenspaces():
735 if eigval == ~(self.base_ring()(2)):
736 J5 = eigspace
737 else:
738 gens = tuple( self.from_vector(b) for b in eigspace.basis() )
739 subalg = FiniteDimensionalEuclideanJordanSubalgebra(self, gens)
740 if eigval == 0:
741 J0 = subalg
742 elif eigval == 1:
743 J1 = subalg
744 else:
745 raise ValueError("unexpected eigenvalue: %s" % eigval)
746
747 return (J0, J5, J1)
748
749
750 def random_elements(self, count):
751 """
752 Return ``count`` random elements as a tuple.
753
754 SETUP::
755
756 sage: from mjo.eja.eja_algebra import JordanSpinEJA
757
758 EXAMPLES::
759
760 sage: J = JordanSpinEJA(3)
761 sage: x,y,z = J.random_elements(3)
762 sage: all( [ x in J, y in J, z in J ])
763 True
764 sage: len( J.random_elements(10) ) == 10
765 True
766
767 """
768 return tuple( self.random_element() for idx in xrange(count) )
769
770
771 def rank(self):
772 """
773 Return the rank of this EJA.
774
775 ALGORITHM:
776
777 The author knows of no algorithm to compute the rank of an EJA
778 where only the multiplication table is known. In lieu of one, we
779 require the rank to be specified when the algebra is created,
780 and simply pass along that number here.
781
782 SETUP::
783
784 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
785 ....: RealSymmetricEJA,
786 ....: ComplexHermitianEJA,
787 ....: QuaternionHermitianEJA,
788 ....: random_eja)
789
790 EXAMPLES:
791
792 The rank of the Jordan spin algebra is always two::
793
794 sage: JordanSpinEJA(2).rank()
795 2
796 sage: JordanSpinEJA(3).rank()
797 2
798 sage: JordanSpinEJA(4).rank()
799 2
800
801 The rank of the `n`-by-`n` Hermitian real, complex, or
802 quaternion matrices is `n`::
803
804 sage: RealSymmetricEJA(4).rank()
805 4
806 sage: ComplexHermitianEJA(3).rank()
807 3
808 sage: QuaternionHermitianEJA(2).rank()
809 2
810
811 TESTS:
812
813 Ensure that every EJA that we know how to construct has a
814 positive integer rank, unless the algebra is trivial in
815 which case its rank will be zero::
816
817 sage: set_random_seed()
818 sage: J = random_eja()
819 sage: r = J.rank()
820 sage: r in ZZ
821 True
822 sage: r > 0 or (r == 0 and J.is_trivial())
823 True
824
825 """
826 return self._rank
827
828
829 def vector_space(self):
830 """
831 Return the vector space that underlies this algebra.
832
833 SETUP::
834
835 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
836
837 EXAMPLES::
838
839 sage: J = RealSymmetricEJA(2)
840 sage: J.vector_space()
841 Vector space of dimension 3 over...
842
843 """
844 return self.zero().to_vector().parent().ambient_vector_space()
845
846
847 Element = FiniteDimensionalEuclideanJordanAlgebraElement
848
849
850 class KnownRankEJA(object):
851 """
852 A class for algebras that we actually know we can construct. The
853 main issue is that, for most of our methods to make sense, we need
854 to know the rank of our algebra. Thus we can't simply generate a
855 "random" algebra, or even check that a given basis and product
856 satisfy the axioms; because even if everything looks OK, we wouldn't
857 know the rank we need to actuallty build the thing.
858
859 Not really a subclass of FDEJA because doing that causes method
860 resolution errors, e.g.
861
862 TypeError: Error when calling the metaclass bases
863 Cannot create a consistent method resolution
864 order (MRO) for bases FiniteDimensionalEuclideanJordanAlgebra,
865 KnownRankEJA
866
867 """
868 @staticmethod
869 def _max_test_case_size():
870 """
871 Return an integer "size" that is an upper bound on the size of
872 this algebra when it is used in a random test
873 case. Unfortunately, the term "size" is quite vague -- when
874 dealing with `R^n` under either the Hadamard or Jordan spin
875 product, the "size" refers to the dimension `n`. When dealing
876 with a matrix algebra (real symmetric or complex/quaternion
877 Hermitian), it refers to the size of the matrix, which is
878 far less than the dimension of the underlying vector space.
879
880 We default to five in this class, which is safe in `R^n`. The
881 matrix algebra subclasses (or any class where the "size" is
882 interpreted to be far less than the dimension) should override
883 with a smaller number.
884 """
885 return 5
886
887 @classmethod
888 def random_instance(cls, field=QQ, **kwargs):
889 """
890 Return a random instance of this type of algebra.
891
892 Beware, this will crash for "most instances" because the
893 constructor below looks wrong.
894 """
895 if cls is TrivialEJA:
896 # The TrivialEJA class doesn't take an "n" argument because
897 # there's only one.
898 return cls(field)
899
900 n = ZZ.random_element(cls._max_test_case_size()) + 1
901 return cls(n, field, **kwargs)
902
903
904 class RealCartesianProductEJA(FiniteDimensionalEuclideanJordanAlgebra,
905 KnownRankEJA):
906 """
907 Return the Euclidean Jordan Algebra corresponding to the set
908 `R^n` under the Hadamard product.
909
910 Note: this is nothing more than the Cartesian product of ``n``
911 copies of the spin algebra. Once Cartesian product algebras
912 are implemented, this can go.
913
914 SETUP::
915
916 sage: from mjo.eja.eja_algebra import RealCartesianProductEJA
917
918 EXAMPLES:
919
920 This multiplication table can be verified by hand::
921
922 sage: J = RealCartesianProductEJA(3)
923 sage: e0,e1,e2 = J.gens()
924 sage: e0*e0
925 e0
926 sage: e0*e1
927 0
928 sage: e0*e2
929 0
930 sage: e1*e1
931 e1
932 sage: e1*e2
933 0
934 sage: e2*e2
935 e2
936
937 TESTS:
938
939 We can change the generator prefix::
940
941 sage: RealCartesianProductEJA(3, prefix='r').gens()
942 (r0, r1, r2)
943
944 """
945 def __init__(self, n, field=QQ, **kwargs):
946 V = VectorSpace(field, n)
947 mult_table = [ [ V.gen(i)*(i == j) for j in xrange(n) ]
948 for i in xrange(n) ]
949
950 fdeja = super(RealCartesianProductEJA, self)
951 return fdeja.__init__(field, mult_table, rank=n, **kwargs)
952
953 def inner_product(self, x, y):
954 """
955 Faster to reimplement than to use natural representations.
956
957 SETUP::
958
959 sage: from mjo.eja.eja_algebra import RealCartesianProductEJA
960
961 TESTS:
962
963 Ensure that this is the usual inner product for the algebras
964 over `R^n`::
965
966 sage: set_random_seed()
967 sage: J = RealCartesianProductEJA.random_instance()
968 sage: x,y = J.random_elements(2)
969 sage: X = x.natural_representation()
970 sage: Y = y.natural_representation()
971 sage: x.inner_product(y) == J.natural_inner_product(X,Y)
972 True
973
974 """
975 return x.to_vector().inner_product(y.to_vector())
976
977
978 def random_eja(field=QQ, nontrivial=False):
979 """
980 Return a "random" finite-dimensional Euclidean Jordan Algebra.
981
982 SETUP::
983
984 sage: from mjo.eja.eja_algebra import random_eja
985
986 TESTS::
987
988 sage: random_eja()
989 Euclidean Jordan algebra of dimension...
990
991 """
992 eja_classes = KnownRankEJA.__subclasses__()
993 if nontrivial:
994 eja_classes.remove(TrivialEJA)
995 classname = choice(eja_classes)
996 return classname.random_instance(field=field)
997
998
999
1000
1001
1002
1003 class MatrixEuclideanJordanAlgebra(FiniteDimensionalEuclideanJordanAlgebra):
1004 @staticmethod
1005 def _max_test_case_size():
1006 # Play it safe, since this will be squared and the underlying
1007 # field can have dimension 4 (quaternions) too.
1008 return 2
1009
1010 def __init__(self, field, basis, rank, normalize_basis=True, **kwargs):
1011 """
1012 Compared to the superclass constructor, we take a basis instead of
1013 a multiplication table because the latter can be computed in terms
1014 of the former when the product is known (like it is here).
1015 """
1016 # Used in this class's fast _charpoly_coeff() override.
1017 self._basis_normalizers = None
1018
1019 # We're going to loop through this a few times, so now's a good
1020 # time to ensure that it isn't a generator expression.
1021 basis = tuple(basis)
1022
1023 if rank > 1 and normalize_basis:
1024 # We'll need sqrt(2) to normalize the basis, and this
1025 # winds up in the multiplication table, so the whole
1026 # algebra needs to be over the field extension.
1027 R = PolynomialRing(field, 'z')
1028 z = R.gen()
1029 p = z**2 - 2
1030 if p.is_irreducible():
1031 field = field.extension(p, 'sqrt2', embedding=RLF(2).sqrt())
1032 basis = tuple( s.change_ring(field) for s in basis )
1033 self._basis_normalizers = tuple(
1034 ~(self.natural_inner_product(s,s).sqrt()) for s in basis )
1035 basis = tuple(s*c for (s,c) in zip(basis,self._basis_normalizers))
1036
1037 Qs = self.multiplication_table_from_matrix_basis(basis)
1038
1039 fdeja = super(MatrixEuclideanJordanAlgebra, self)
1040 return fdeja.__init__(field,
1041 Qs,
1042 rank=rank,
1043 natural_basis=basis,
1044 **kwargs)
1045
1046
1047 @cached_method
1048 def _charpoly_coeff(self, i):
1049 """
1050 Override the parent method with something that tries to compute
1051 over a faster (non-extension) field.
1052 """
1053 if self._basis_normalizers is None:
1054 # We didn't normalize, so assume that the basis we started
1055 # with had entries in a nice field.
1056 return super(MatrixEuclideanJordanAlgebra, self)._charpoly_coeff(i)
1057 else:
1058 basis = ( (b/n) for (b,n) in zip(self.natural_basis(),
1059 self._basis_normalizers) )
1060
1061 # Do this over the rationals and convert back at the end.
1062 J = MatrixEuclideanJordanAlgebra(QQ,
1063 basis,
1064 self.rank(),
1065 normalize_basis=False)
1066 (_,x,_,_) = J._charpoly_matrix_system()
1067 p = J._charpoly_coeff(i)
1068 # p might be missing some vars, have to substitute "optionally"
1069 pairs = zip(x.base_ring().gens(), self._basis_normalizers)
1070 substitutions = { v: v*c for (v,c) in pairs }
1071 result = p.subs(substitutions)
1072
1073 # The result of "subs" can be either a coefficient-ring
1074 # element or a polynomial. Gotta handle both cases.
1075 if result in QQ:
1076 return self.base_ring()(result)
1077 else:
1078 return result.change_ring(self.base_ring())
1079
1080
1081 @staticmethod
1082 def multiplication_table_from_matrix_basis(basis):
1083 """
1084 At least three of the five simple Euclidean Jordan algebras have the
1085 symmetric multiplication (A,B) |-> (AB + BA)/2, where the
1086 multiplication on the right is matrix multiplication. Given a basis
1087 for the underlying matrix space, this function returns a
1088 multiplication table (obtained by looping through the basis
1089 elements) for an algebra of those matrices.
1090 """
1091 # In S^2, for example, we nominally have four coordinates even
1092 # though the space is of dimension three only. The vector space V
1093 # is supposed to hold the entire long vector, and the subspace W
1094 # of V will be spanned by the vectors that arise from symmetric
1095 # matrices. Thus for S^2, dim(V) == 4 and dim(W) == 3.
1096 field = basis[0].base_ring()
1097 dimension = basis[0].nrows()
1098
1099 V = VectorSpace(field, dimension**2)
1100 W = V.span_of_basis( _mat2vec(s) for s in basis )
1101 n = len(basis)
1102 mult_table = [[W.zero() for j in xrange(n)] for i in xrange(n)]
1103 for i in xrange(n):
1104 for j in xrange(n):
1105 mat_entry = (basis[i]*basis[j] + basis[j]*basis[i])/2
1106 mult_table[i][j] = W.coordinate_vector(_mat2vec(mat_entry))
1107
1108 return mult_table
1109
1110
1111 @staticmethod
1112 def real_embed(M):
1113 """
1114 Embed the matrix ``M`` into a space of real matrices.
1115
1116 The matrix ``M`` can have entries in any field at the moment:
1117 the real numbers, complex numbers, or quaternions. And although
1118 they are not a field, we can probably support octonions at some
1119 point, too. This function returns a real matrix that "acts like"
1120 the original with respect to matrix multiplication; i.e.
1121
1122 real_embed(M*N) = real_embed(M)*real_embed(N)
1123
1124 """
1125 raise NotImplementedError
1126
1127
1128 @staticmethod
1129 def real_unembed(M):
1130 """
1131 The inverse of :meth:`real_embed`.
1132 """
1133 raise NotImplementedError
1134
1135
1136 @classmethod
1137 def natural_inner_product(cls,X,Y):
1138 Xu = cls.real_unembed(X)
1139 Yu = cls.real_unembed(Y)
1140 tr = (Xu*Yu).trace()
1141
1142 if tr in RLF:
1143 # It's real already.
1144 return tr
1145
1146 # Otherwise, try the thing that works for complex numbers; and
1147 # if that doesn't work, the thing that works for quaternions.
1148 try:
1149 return tr.vector()[0] # real part, imag part is index 1
1150 except AttributeError:
1151 # A quaternions doesn't have a vector() method, but does
1152 # have coefficient_tuple() method that returns the
1153 # coefficients of 1, i, j, and k -- in that order.
1154 return tr.coefficient_tuple()[0]
1155
1156
1157 class RealMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
1158 @staticmethod
1159 def real_embed(M):
1160 """
1161 The identity function, for embedding real matrices into real
1162 matrices.
1163 """
1164 return M
1165
1166 @staticmethod
1167 def real_unembed(M):
1168 """
1169 The identity function, for unembedding real matrices from real
1170 matrices.
1171 """
1172 return M
1173
1174
1175 class RealSymmetricEJA(RealMatrixEuclideanJordanAlgebra, KnownRankEJA):
1176 """
1177 The rank-n simple EJA consisting of real symmetric n-by-n
1178 matrices, the usual symmetric Jordan product, and the trace inner
1179 product. It has dimension `(n^2 + n)/2` over the reals.
1180
1181 SETUP::
1182
1183 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1184
1185 EXAMPLES::
1186
1187 sage: J = RealSymmetricEJA(2)
1188 sage: e0, e1, e2 = J.gens()
1189 sage: e0*e0
1190 e0
1191 sage: e1*e1
1192 1/2*e0 + 1/2*e2
1193 sage: e2*e2
1194 e2
1195
1196 In theory, our "field" can be any subfield of the reals::
1197
1198 sage: RealSymmetricEJA(2, AA)
1199 Euclidean Jordan algebra of dimension 3 over Algebraic Real Field
1200 sage: RealSymmetricEJA(2, RR)
1201 Euclidean Jordan algebra of dimension 3 over Real Field with
1202 53 bits of precision
1203
1204 TESTS:
1205
1206 The dimension of this algebra is `(n^2 + n) / 2`::
1207
1208 sage: set_random_seed()
1209 sage: n_max = RealSymmetricEJA._max_test_case_size()
1210 sage: n = ZZ.random_element(1, n_max)
1211 sage: J = RealSymmetricEJA(n)
1212 sage: J.dimension() == (n^2 + n)/2
1213 True
1214
1215 The Jordan multiplication is what we think it is::
1216
1217 sage: set_random_seed()
1218 sage: J = RealSymmetricEJA.random_instance()
1219 sage: x,y = J.random_elements(2)
1220 sage: actual = (x*y).natural_representation()
1221 sage: X = x.natural_representation()
1222 sage: Y = y.natural_representation()
1223 sage: expected = (X*Y + Y*X)/2
1224 sage: actual == expected
1225 True
1226 sage: J(expected) == x*y
1227 True
1228
1229 We can change the generator prefix::
1230
1231 sage: RealSymmetricEJA(3, prefix='q').gens()
1232 (q0, q1, q2, q3, q4, q5)
1233
1234 Our natural basis is normalized with respect to the natural inner
1235 product unless we specify otherwise::
1236
1237 sage: set_random_seed()
1238 sage: J = RealSymmetricEJA.random_instance()
1239 sage: all( b.norm() == 1 for b in J.gens() )
1240 True
1241
1242 Since our natural basis is normalized with respect to the natural
1243 inner product, and since we know that this algebra is an EJA, any
1244 left-multiplication operator's matrix will be symmetric because
1245 natural->EJA basis representation is an isometry and within the EJA
1246 the operator is self-adjoint by the Jordan axiom::
1247
1248 sage: set_random_seed()
1249 sage: x = RealSymmetricEJA.random_instance().random_element()
1250 sage: x.operator().matrix().is_symmetric()
1251 True
1252
1253 """
1254 @classmethod
1255 def _denormalized_basis(cls, n, field):
1256 """
1257 Return a basis for the space of real symmetric n-by-n matrices.
1258
1259 SETUP::
1260
1261 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1262
1263 TESTS::
1264
1265 sage: set_random_seed()
1266 sage: n = ZZ.random_element(1,5)
1267 sage: B = RealSymmetricEJA._denormalized_basis(n,QQ)
1268 sage: all( M.is_symmetric() for M in B)
1269 True
1270
1271 """
1272 # The basis of symmetric matrices, as matrices, in their R^(n-by-n)
1273 # coordinates.
1274 S = []
1275 for i in xrange(n):
1276 for j in xrange(i+1):
1277 Eij = matrix(field, n, lambda k,l: k==i and l==j)
1278 if i == j:
1279 Sij = Eij
1280 else:
1281 Sij = Eij + Eij.transpose()
1282 S.append(Sij)
1283 return S
1284
1285
1286 @staticmethod
1287 def _max_test_case_size():
1288 return 4 # Dimension 10
1289
1290
1291 def __init__(self, n, field=QQ, **kwargs):
1292 basis = self._denormalized_basis(n, field)
1293 super(RealSymmetricEJA, self).__init__(field, basis, n, **kwargs)
1294
1295
1296 class ComplexMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
1297 @staticmethod
1298 def real_embed(M):
1299 """
1300 Embed the n-by-n complex matrix ``M`` into the space of real
1301 matrices of size 2n-by-2n via the map the sends each entry `z = a +
1302 bi` to the block matrix ``[[a,b],[-b,a]]``.
1303
1304 SETUP::
1305
1306 sage: from mjo.eja.eja_algebra import \
1307 ....: ComplexMatrixEuclideanJordanAlgebra
1308
1309 EXAMPLES::
1310
1311 sage: F = QuadraticField(-1, 'i')
1312 sage: x1 = F(4 - 2*i)
1313 sage: x2 = F(1 + 2*i)
1314 sage: x3 = F(-i)
1315 sage: x4 = F(6)
1316 sage: M = matrix(F,2,[[x1,x2],[x3,x4]])
1317 sage: ComplexMatrixEuclideanJordanAlgebra.real_embed(M)
1318 [ 4 -2| 1 2]
1319 [ 2 4|-2 1]
1320 [-----+-----]
1321 [ 0 -1| 6 0]
1322 [ 1 0| 0 6]
1323
1324 TESTS:
1325
1326 Embedding is a homomorphism (isomorphism, in fact)::
1327
1328 sage: set_random_seed()
1329 sage: n_max = ComplexMatrixEuclideanJordanAlgebra._max_test_case_size()
1330 sage: n = ZZ.random_element(n_max)
1331 sage: F = QuadraticField(-1, 'i')
1332 sage: X = random_matrix(F, n)
1333 sage: Y = random_matrix(F, n)
1334 sage: Xe = ComplexMatrixEuclideanJordanAlgebra.real_embed(X)
1335 sage: Ye = ComplexMatrixEuclideanJordanAlgebra.real_embed(Y)
1336 sage: XYe = ComplexMatrixEuclideanJordanAlgebra.real_embed(X*Y)
1337 sage: Xe*Ye == XYe
1338 True
1339
1340 """
1341 n = M.nrows()
1342 if M.ncols() != n:
1343 raise ValueError("the matrix 'M' must be square")
1344
1345 # We don't need any adjoined elements...
1346 field = M.base_ring().base_ring()
1347
1348 blocks = []
1349 for z in M.list():
1350 a = z.list()[0] # real part, I guess
1351 b = z.list()[1] # imag part, I guess
1352 blocks.append(matrix(field, 2, [[a,b],[-b,a]]))
1353
1354 return matrix.block(field, n, blocks)
1355
1356
1357 @staticmethod
1358 def real_unembed(M):
1359 """
1360 The inverse of _embed_complex_matrix().
1361
1362 SETUP::
1363
1364 sage: from mjo.eja.eja_algebra import \
1365 ....: ComplexMatrixEuclideanJordanAlgebra
1366
1367 EXAMPLES::
1368
1369 sage: A = matrix(QQ,[ [ 1, 2, 3, 4],
1370 ....: [-2, 1, -4, 3],
1371 ....: [ 9, 10, 11, 12],
1372 ....: [-10, 9, -12, 11] ])
1373 sage: ComplexMatrixEuclideanJordanAlgebra.real_unembed(A)
1374 [ 2*i + 1 4*i + 3]
1375 [ 10*i + 9 12*i + 11]
1376
1377 TESTS:
1378
1379 Unembedding is the inverse of embedding::
1380
1381 sage: set_random_seed()
1382 sage: F = QuadraticField(-1, 'i')
1383 sage: M = random_matrix(F, 3)
1384 sage: Me = ComplexMatrixEuclideanJordanAlgebra.real_embed(M)
1385 sage: ComplexMatrixEuclideanJordanAlgebra.real_unembed(Me) == M
1386 True
1387
1388 """
1389 n = ZZ(M.nrows())
1390 if M.ncols() != n:
1391 raise ValueError("the matrix 'M' must be square")
1392 if not n.mod(2).is_zero():
1393 raise ValueError("the matrix 'M' must be a complex embedding")
1394
1395 # If "M" was normalized, its base ring might have roots
1396 # adjoined and they can stick around after unembedding.
1397 field = M.base_ring()
1398 R = PolynomialRing(field, 'z')
1399 z = R.gen()
1400 F = field.extension(z**2 + 1, 'i', embedding=CLF(-1).sqrt())
1401 i = F.gen()
1402
1403 # Go top-left to bottom-right (reading order), converting every
1404 # 2-by-2 block we see to a single complex element.
1405 elements = []
1406 for k in xrange(n/2):
1407 for j in xrange(n/2):
1408 submat = M[2*k:2*k+2,2*j:2*j+2]
1409 if submat[0,0] != submat[1,1]:
1410 raise ValueError('bad on-diagonal submatrix')
1411 if submat[0,1] != -submat[1,0]:
1412 raise ValueError('bad off-diagonal submatrix')
1413 z = submat[0,0] + submat[0,1]*i
1414 elements.append(z)
1415
1416 return matrix(F, n/2, elements)
1417
1418
1419 @classmethod
1420 def natural_inner_product(cls,X,Y):
1421 """
1422 Compute a natural inner product in this algebra directly from
1423 its real embedding.
1424
1425 SETUP::
1426
1427 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1428
1429 TESTS:
1430
1431 This gives the same answer as the slow, default method implemented
1432 in :class:`MatrixEuclideanJordanAlgebra`::
1433
1434 sage: set_random_seed()
1435 sage: J = ComplexHermitianEJA.random_instance()
1436 sage: x,y = J.random_elements(2)
1437 sage: Xe = x.natural_representation()
1438 sage: Ye = y.natural_representation()
1439 sage: X = ComplexHermitianEJA.real_unembed(Xe)
1440 sage: Y = ComplexHermitianEJA.real_unembed(Ye)
1441 sage: expected = (X*Y).trace().vector()[0]
1442 sage: actual = ComplexHermitianEJA.natural_inner_product(Xe,Ye)
1443 sage: actual == expected
1444 True
1445
1446 """
1447 return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/2
1448
1449
1450 class ComplexHermitianEJA(ComplexMatrixEuclideanJordanAlgebra, KnownRankEJA):
1451 """
1452 The rank-n simple EJA consisting of complex Hermitian n-by-n
1453 matrices over the real numbers, the usual symmetric Jordan product,
1454 and the real-part-of-trace inner product. It has dimension `n^2` over
1455 the reals.
1456
1457 SETUP::
1458
1459 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1460
1461 EXAMPLES:
1462
1463 In theory, our "field" can be any subfield of the reals::
1464
1465 sage: ComplexHermitianEJA(2, AA)
1466 Euclidean Jordan algebra of dimension 4 over Algebraic Real Field
1467 sage: ComplexHermitianEJA(2, RR)
1468 Euclidean Jordan algebra of dimension 4 over Real Field with
1469 53 bits of precision
1470
1471 TESTS:
1472
1473 The dimension of this algebra is `n^2`::
1474
1475 sage: set_random_seed()
1476 sage: n_max = ComplexHermitianEJA._max_test_case_size()
1477 sage: n = ZZ.random_element(1, n_max)
1478 sage: J = ComplexHermitianEJA(n)
1479 sage: J.dimension() == n^2
1480 True
1481
1482 The Jordan multiplication is what we think it is::
1483
1484 sage: set_random_seed()
1485 sage: J = ComplexHermitianEJA.random_instance()
1486 sage: x,y = J.random_elements(2)
1487 sage: actual = (x*y).natural_representation()
1488 sage: X = x.natural_representation()
1489 sage: Y = y.natural_representation()
1490 sage: expected = (X*Y + Y*X)/2
1491 sage: actual == expected
1492 True
1493 sage: J(expected) == x*y
1494 True
1495
1496 We can change the generator prefix::
1497
1498 sage: ComplexHermitianEJA(2, prefix='z').gens()
1499 (z0, z1, z2, z3)
1500
1501 Our natural basis is normalized with respect to the natural inner
1502 product unless we specify otherwise::
1503
1504 sage: set_random_seed()
1505 sage: J = ComplexHermitianEJA.random_instance()
1506 sage: all( b.norm() == 1 for b in J.gens() )
1507 True
1508
1509 Since our natural basis is normalized with respect to the natural
1510 inner product, and since we know that this algebra is an EJA, any
1511 left-multiplication operator's matrix will be symmetric because
1512 natural->EJA basis representation is an isometry and within the EJA
1513 the operator is self-adjoint by the Jordan axiom::
1514
1515 sage: set_random_seed()
1516 sage: x = ComplexHermitianEJA.random_instance().random_element()
1517 sage: x.operator().matrix().is_symmetric()
1518 True
1519
1520 """
1521
1522 @classmethod
1523 def _denormalized_basis(cls, n, field):
1524 """
1525 Returns a basis for the space of complex Hermitian n-by-n matrices.
1526
1527 Why do we embed these? Basically, because all of numerical linear
1528 algebra assumes that you're working with vectors consisting of `n`
1529 entries from a field and scalars from the same field. There's no way
1530 to tell SageMath that (for example) the vectors contain complex
1531 numbers, while the scalar field is real.
1532
1533 SETUP::
1534
1535 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1536
1537 TESTS::
1538
1539 sage: set_random_seed()
1540 sage: n = ZZ.random_element(1,5)
1541 sage: field = QuadraticField(2, 'sqrt2')
1542 sage: B = ComplexHermitianEJA._denormalized_basis(n, field)
1543 sage: all( M.is_symmetric() for M in B)
1544 True
1545
1546 """
1547 R = PolynomialRing(field, 'z')
1548 z = R.gen()
1549 F = field.extension(z**2 + 1, 'I')
1550 I = F.gen()
1551
1552 # This is like the symmetric case, but we need to be careful:
1553 #
1554 # * We want conjugate-symmetry, not just symmetry.
1555 # * The diagonal will (as a result) be real.
1556 #
1557 S = []
1558 for i in xrange(n):
1559 for j in xrange(i+1):
1560 Eij = matrix(F, n, lambda k,l: k==i and l==j)
1561 if i == j:
1562 Sij = cls.real_embed(Eij)
1563 S.append(Sij)
1564 else:
1565 # The second one has a minus because it's conjugated.
1566 Sij_real = cls.real_embed(Eij + Eij.transpose())
1567 S.append(Sij_real)
1568 Sij_imag = cls.real_embed(I*Eij - I*Eij.transpose())
1569 S.append(Sij_imag)
1570
1571 # Since we embedded these, we can drop back to the "field" that we
1572 # started with instead of the complex extension "F".
1573 return ( s.change_ring(field) for s in S )
1574
1575
1576 def __init__(self, n, field=QQ, **kwargs):
1577 basis = self._denormalized_basis(n,field)
1578 super(ComplexHermitianEJA,self).__init__(field, basis, n, **kwargs)
1579
1580
1581 class QuaternionMatrixEuclideanJordanAlgebra(MatrixEuclideanJordanAlgebra):
1582 @staticmethod
1583 def real_embed(M):
1584 """
1585 Embed the n-by-n quaternion matrix ``M`` into the space of real
1586 matrices of size 4n-by-4n by first sending each quaternion entry `z
1587 = a + bi + cj + dk` to the block-complex matrix ``[[a + bi,
1588 c+di],[-c + di, a-bi]]`, and then embedding those into a real
1589 matrix.
1590
1591 SETUP::
1592
1593 sage: from mjo.eja.eja_algebra import \
1594 ....: QuaternionMatrixEuclideanJordanAlgebra
1595
1596 EXAMPLES::
1597
1598 sage: Q = QuaternionAlgebra(QQ,-1,-1)
1599 sage: i,j,k = Q.gens()
1600 sage: x = 1 + 2*i + 3*j + 4*k
1601 sage: M = matrix(Q, 1, [[x]])
1602 sage: QuaternionMatrixEuclideanJordanAlgebra.real_embed(M)
1603 [ 1 2 3 4]
1604 [-2 1 -4 3]
1605 [-3 4 1 -2]
1606 [-4 -3 2 1]
1607
1608 Embedding is a homomorphism (isomorphism, in fact)::
1609
1610 sage: set_random_seed()
1611 sage: n_max = QuaternionMatrixEuclideanJordanAlgebra._max_test_case_size()
1612 sage: n = ZZ.random_element(n_max)
1613 sage: Q = QuaternionAlgebra(QQ,-1,-1)
1614 sage: X = random_matrix(Q, n)
1615 sage: Y = random_matrix(Q, n)
1616 sage: Xe = QuaternionMatrixEuclideanJordanAlgebra.real_embed(X)
1617 sage: Ye = QuaternionMatrixEuclideanJordanAlgebra.real_embed(Y)
1618 sage: XYe = QuaternionMatrixEuclideanJordanAlgebra.real_embed(X*Y)
1619 sage: Xe*Ye == XYe
1620 True
1621
1622 """
1623 quaternions = M.base_ring()
1624 n = M.nrows()
1625 if M.ncols() != n:
1626 raise ValueError("the matrix 'M' must be square")
1627
1628 F = QuadraticField(-1, 'i')
1629 i = F.gen()
1630
1631 blocks = []
1632 for z in M.list():
1633 t = z.coefficient_tuple()
1634 a = t[0]
1635 b = t[1]
1636 c = t[2]
1637 d = t[3]
1638 cplxM = matrix(F, 2, [[ a + b*i, c + d*i],
1639 [-c + d*i, a - b*i]])
1640 realM = ComplexMatrixEuclideanJordanAlgebra.real_embed(cplxM)
1641 blocks.append(realM)
1642
1643 # We should have real entries by now, so use the realest field
1644 # we've got for the return value.
1645 return matrix.block(quaternions.base_ring(), n, blocks)
1646
1647
1648
1649 @staticmethod
1650 def real_unembed(M):
1651 """
1652 The inverse of _embed_quaternion_matrix().
1653
1654 SETUP::
1655
1656 sage: from mjo.eja.eja_algebra import \
1657 ....: QuaternionMatrixEuclideanJordanAlgebra
1658
1659 EXAMPLES::
1660
1661 sage: M = matrix(QQ, [[ 1, 2, 3, 4],
1662 ....: [-2, 1, -4, 3],
1663 ....: [-3, 4, 1, -2],
1664 ....: [-4, -3, 2, 1]])
1665 sage: QuaternionMatrixEuclideanJordanAlgebra.real_unembed(M)
1666 [1 + 2*i + 3*j + 4*k]
1667
1668 TESTS:
1669
1670 Unembedding is the inverse of embedding::
1671
1672 sage: set_random_seed()
1673 sage: Q = QuaternionAlgebra(QQ, -1, -1)
1674 sage: M = random_matrix(Q, 3)
1675 sage: Me = QuaternionMatrixEuclideanJordanAlgebra.real_embed(M)
1676 sage: QuaternionMatrixEuclideanJordanAlgebra.real_unembed(Me) == M
1677 True
1678
1679 """
1680 n = ZZ(M.nrows())
1681 if M.ncols() != n:
1682 raise ValueError("the matrix 'M' must be square")
1683 if not n.mod(4).is_zero():
1684 raise ValueError("the matrix 'M' must be a quaternion embedding")
1685
1686 # Use the base ring of the matrix to ensure that its entries can be
1687 # multiplied by elements of the quaternion algebra.
1688 field = M.base_ring()
1689 Q = QuaternionAlgebra(field,-1,-1)
1690 i,j,k = Q.gens()
1691
1692 # Go top-left to bottom-right (reading order), converting every
1693 # 4-by-4 block we see to a 2-by-2 complex block, to a 1-by-1
1694 # quaternion block.
1695 elements = []
1696 for l in xrange(n/4):
1697 for m in xrange(n/4):
1698 submat = ComplexMatrixEuclideanJordanAlgebra.real_unembed(
1699 M[4*l:4*l+4,4*m:4*m+4] )
1700 if submat[0,0] != submat[1,1].conjugate():
1701 raise ValueError('bad on-diagonal submatrix')
1702 if submat[0,1] != -submat[1,0].conjugate():
1703 raise ValueError('bad off-diagonal submatrix')
1704 z = submat[0,0].vector()[0] # real part
1705 z += submat[0,0].vector()[1]*i # imag part
1706 z += submat[0,1].vector()[0]*j # real part
1707 z += submat[0,1].vector()[1]*k # imag part
1708 elements.append(z)
1709
1710 return matrix(Q, n/4, elements)
1711
1712
1713 @classmethod
1714 def natural_inner_product(cls,X,Y):
1715 """
1716 Compute a natural inner product in this algebra directly from
1717 its real embedding.
1718
1719 SETUP::
1720
1721 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
1722
1723 TESTS:
1724
1725 This gives the same answer as the slow, default method implemented
1726 in :class:`MatrixEuclideanJordanAlgebra`::
1727
1728 sage: set_random_seed()
1729 sage: J = QuaternionHermitianEJA.random_instance()
1730 sage: x,y = J.random_elements(2)
1731 sage: Xe = x.natural_representation()
1732 sage: Ye = y.natural_representation()
1733 sage: X = QuaternionHermitianEJA.real_unembed(Xe)
1734 sage: Y = QuaternionHermitianEJA.real_unembed(Ye)
1735 sage: expected = (X*Y).trace().coefficient_tuple()[0]
1736 sage: actual = QuaternionHermitianEJA.natural_inner_product(Xe,Ye)
1737 sage: actual == expected
1738 True
1739
1740 """
1741 return RealMatrixEuclideanJordanAlgebra.natural_inner_product(X,Y)/4
1742
1743
1744 class QuaternionHermitianEJA(QuaternionMatrixEuclideanJordanAlgebra,
1745 KnownRankEJA):
1746 """
1747 The rank-n simple EJA consisting of self-adjoint n-by-n quaternion
1748 matrices, the usual symmetric Jordan product, and the
1749 real-part-of-trace inner product. It has dimension `2n^2 - n` over
1750 the reals.
1751
1752 SETUP::
1753
1754 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
1755
1756 EXAMPLES:
1757
1758 In theory, our "field" can be any subfield of the reals::
1759
1760 sage: QuaternionHermitianEJA(2, AA)
1761 Euclidean Jordan algebra of dimension 6 over Algebraic Real Field
1762 sage: QuaternionHermitianEJA(2, RR)
1763 Euclidean Jordan algebra of dimension 6 over Real Field with
1764 53 bits of precision
1765
1766 TESTS:
1767
1768 The dimension of this algebra is `2*n^2 - n`::
1769
1770 sage: set_random_seed()
1771 sage: n_max = QuaternionHermitianEJA._max_test_case_size()
1772 sage: n = ZZ.random_element(1, n_max)
1773 sage: J = QuaternionHermitianEJA(n)
1774 sage: J.dimension() == 2*(n^2) - n
1775 True
1776
1777 The Jordan multiplication is what we think it is::
1778
1779 sage: set_random_seed()
1780 sage: J = QuaternionHermitianEJA.random_instance()
1781 sage: x,y = J.random_elements(2)
1782 sage: actual = (x*y).natural_representation()
1783 sage: X = x.natural_representation()
1784 sage: Y = y.natural_representation()
1785 sage: expected = (X*Y + Y*X)/2
1786 sage: actual == expected
1787 True
1788 sage: J(expected) == x*y
1789 True
1790
1791 We can change the generator prefix::
1792
1793 sage: QuaternionHermitianEJA(2, prefix='a').gens()
1794 (a0, a1, a2, a3, a4, a5)
1795
1796 Our natural basis is normalized with respect to the natural inner
1797 product unless we specify otherwise::
1798
1799 sage: set_random_seed()
1800 sage: J = QuaternionHermitianEJA.random_instance()
1801 sage: all( b.norm() == 1 for b in J.gens() )
1802 True
1803
1804 Since our natural basis is normalized with respect to the natural
1805 inner product, and since we know that this algebra is an EJA, any
1806 left-multiplication operator's matrix will be symmetric because
1807 natural->EJA basis representation is an isometry and within the EJA
1808 the operator is self-adjoint by the Jordan axiom::
1809
1810 sage: set_random_seed()
1811 sage: x = QuaternionHermitianEJA.random_instance().random_element()
1812 sage: x.operator().matrix().is_symmetric()
1813 True
1814
1815 """
1816 @classmethod
1817 def _denormalized_basis(cls, n, field):
1818 """
1819 Returns a basis for the space of quaternion Hermitian n-by-n matrices.
1820
1821 Why do we embed these? Basically, because all of numerical
1822 linear algebra assumes that you're working with vectors consisting
1823 of `n` entries from a field and scalars from the same field. There's
1824 no way to tell SageMath that (for example) the vectors contain
1825 complex numbers, while the scalar field is real.
1826
1827 SETUP::
1828
1829 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
1830
1831 TESTS::
1832
1833 sage: set_random_seed()
1834 sage: n = ZZ.random_element(1,5)
1835 sage: B = QuaternionHermitianEJA._denormalized_basis(n,QQ)
1836 sage: all( M.is_symmetric() for M in B )
1837 True
1838
1839 """
1840 Q = QuaternionAlgebra(QQ,-1,-1)
1841 I,J,K = Q.gens()
1842
1843 # This is like the symmetric case, but we need to be careful:
1844 #
1845 # * We want conjugate-symmetry, not just symmetry.
1846 # * The diagonal will (as a result) be real.
1847 #
1848 S = []
1849 for i in xrange(n):
1850 for j in xrange(i+1):
1851 Eij = matrix(Q, n, lambda k,l: k==i and l==j)
1852 if i == j:
1853 Sij = cls.real_embed(Eij)
1854 S.append(Sij)
1855 else:
1856 # The second, third, and fourth ones have a minus
1857 # because they're conjugated.
1858 Sij_real = cls.real_embed(Eij + Eij.transpose())
1859 S.append(Sij_real)
1860 Sij_I = cls.real_embed(I*Eij - I*Eij.transpose())
1861 S.append(Sij_I)
1862 Sij_J = cls.real_embed(J*Eij - J*Eij.transpose())
1863 S.append(Sij_J)
1864 Sij_K = cls.real_embed(K*Eij - K*Eij.transpose())
1865 S.append(Sij_K)
1866
1867 # Since we embedded these, we can drop back to the "field" that we
1868 # started with instead of the quaternion algebra "Q".
1869 return ( s.change_ring(field) for s in S )
1870
1871
1872 def __init__(self, n, field=QQ, **kwargs):
1873 basis = self._denormalized_basis(n,field)
1874 super(QuaternionHermitianEJA,self).__init__(field, basis, n, **kwargs)
1875
1876
1877 class JordanSpinEJA(FiniteDimensionalEuclideanJordanAlgebra, KnownRankEJA):
1878 """
1879 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
1880 with the usual inner product and jordan product ``x*y =
1881 (<x_bar,y_bar>, x0*y_bar + y0*x_bar)``. It has dimension `n` over
1882 the reals.
1883
1884 SETUP::
1885
1886 sage: from mjo.eja.eja_algebra import JordanSpinEJA
1887
1888 EXAMPLES:
1889
1890 This multiplication table can be verified by hand::
1891
1892 sage: J = JordanSpinEJA(4)
1893 sage: e0,e1,e2,e3 = J.gens()
1894 sage: e0*e0
1895 e0
1896 sage: e0*e1
1897 e1
1898 sage: e0*e2
1899 e2
1900 sage: e0*e3
1901 e3
1902 sage: e1*e2
1903 0
1904 sage: e1*e3
1905 0
1906 sage: e2*e3
1907 0
1908
1909 We can change the generator prefix::
1910
1911 sage: JordanSpinEJA(2, prefix='B').gens()
1912 (B0, B1)
1913
1914 """
1915 def __init__(self, n, field=QQ, **kwargs):
1916 V = VectorSpace(field, n)
1917 mult_table = [[V.zero() for j in xrange(n)] for i in xrange(n)]
1918 for i in xrange(n):
1919 for j in xrange(n):
1920 x = V.gen(i)
1921 y = V.gen(j)
1922 x0 = x[0]
1923 xbar = x[1:]
1924 y0 = y[0]
1925 ybar = y[1:]
1926 # z = x*y
1927 z0 = x.inner_product(y)
1928 zbar = y0*xbar + x0*ybar
1929 z = V([z0] + zbar.list())
1930 mult_table[i][j] = z
1931
1932 # The rank of the spin algebra is two, unless we're in a
1933 # one-dimensional ambient space (because the rank is bounded by
1934 # the ambient dimension).
1935 fdeja = super(JordanSpinEJA, self)
1936 return fdeja.__init__(field, mult_table, rank=min(n,2), **kwargs)
1937
1938 def inner_product(self, x, y):
1939 """
1940 Faster to reimplement than to use natural representations.
1941
1942 SETUP::
1943
1944 sage: from mjo.eja.eja_algebra import JordanSpinEJA
1945
1946 TESTS:
1947
1948 Ensure that this is the usual inner product for the algebras
1949 over `R^n`::
1950
1951 sage: set_random_seed()
1952 sage: J = JordanSpinEJA.random_instance()
1953 sage: x,y = J.random_elements(2)
1954 sage: X = x.natural_representation()
1955 sage: Y = y.natural_representation()
1956 sage: x.inner_product(y) == J.natural_inner_product(X,Y)
1957 True
1958
1959 """
1960 return x.to_vector().inner_product(y.to_vector())
1961
1962
1963 class TrivialEJA(FiniteDimensionalEuclideanJordanAlgebra, KnownRankEJA):
1964 """
1965 The trivial Euclidean Jordan algebra consisting of only a zero element.
1966
1967 SETUP::
1968
1969 sage: from mjo.eja.eja_algebra import TrivialEJA
1970
1971 EXAMPLES::
1972
1973 sage: J = TrivialEJA()
1974 sage: J.dimension()
1975 0
1976 sage: J.zero()
1977 0
1978 sage: J.one()
1979 0
1980 sage: 7*J.one()*12*J.one()
1981 0
1982 sage: J.one().inner_product(J.one())
1983 0
1984 sage: J.one().norm()
1985 0
1986 sage: J.one().subalgebra_generated_by()
1987 Euclidean Jordan algebra of dimension 0 over Rational Field
1988 sage: J.rank()
1989 0
1990
1991 """
1992 def __init__(self, field=QQ, **kwargs):
1993 mult_table = []
1994 fdeja = super(TrivialEJA, self)
1995 # The rank is zero using my definition, namely the dimension of the
1996 # largest subalgebra generated by any element.
1997 return fdeja.__init__(field, mult_table, rank=0, **kwargs)