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