]> gitweb.michael.orlitzky.com - sage.d.git/blob - mjo/eja/eja_algebra.py
204a537df12d2ab4d3d27764c20c830ec324a68a
[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 sage.algebras.quatalg.quaternion_algebra import QuaternionAlgebra
9 from sage.categories.magmatic_algebras import MagmaticAlgebras
10 from sage.combinat.free_module import CombinatorialFreeModule
11 from sage.matrix.constructor import matrix
12 from sage.matrix.matrix_space import MatrixSpace
13 from sage.misc.cachefunc import cached_method
14 from sage.misc.prandom import choice
15 from sage.misc.table import table
16 from sage.modules.free_module import FreeModule, VectorSpace
17 from sage.rings.integer_ring import ZZ
18 from sage.rings.number_field.number_field import QuadraticField
19 from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
20 from sage.rings.rational_field import QQ
21 from sage.structure.element import is_Matrix
22
23 from mjo.eja.eja_element import FiniteDimensionalEuclideanJordanAlgebraElement
24 from mjo.eja.eja_utils import _mat2vec
25
26 class FiniteDimensionalEuclideanJordanAlgebra(CombinatorialFreeModule):
27 # This is an ugly hack needed to prevent the category framework
28 # from implementing a coercion from our base ring (e.g. the
29 # rationals) into the algebra. First of all -- such a coercion is
30 # nonsense to begin with. But more importantly, it tries to do so
31 # in the category of rings, and since our algebras aren't
32 # associative they generally won't be rings.
33 _no_generic_basering_coercion = True
34
35 def __init__(self,
36 field,
37 mult_table,
38 rank,
39 prefix='e',
40 category=None,
41 natural_basis=None):
42 """
43 SETUP::
44
45 sage: from mjo.eja.eja_algebra import 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 = J.random_element()
54 sage: y = J.random_element()
55 sage: x*y == y*x
56 True
57
58 """
59 self._rank = rank
60 self._natural_basis = natural_basis
61
62 if category is None:
63 category = MagmaticAlgebras(field).FiniteDimensional()
64 category = category.WithBasis().Unital()
65
66 fda = super(FiniteDimensionalEuclideanJordanAlgebra, self)
67 fda.__init__(field,
68 range(len(mult_table)),
69 prefix=prefix,
70 category=category)
71 self.print_options(bracket='')
72
73 # The multiplication table we're given is necessarily in terms
74 # of vectors, because we don't have an algebra yet for
75 # anything to be an element of. However, it's faster in the
76 # long run to have the multiplication table be in terms of
77 # algebra elements. We do this after calling the superclass
78 # constructor so that from_vector() knows what to do.
79 self._multiplication_table = [ map(lambda x: self.from_vector(x), ls)
80 for ls in mult_table ]
81
82
83 def _element_constructor_(self, elt):
84 """
85 Construct an element of this algebra from its natural
86 representation.
87
88 This gets called only after the parent element _call_ method
89 fails to find a coercion for the argument.
90
91 SETUP::
92
93 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
94 ....: RealCartesianProductEJA,
95 ....: RealSymmetricEJA)
96
97 EXAMPLES:
98
99 The identity in `S^n` is converted to the identity in the EJA::
100
101 sage: J = RealSymmetricEJA(3)
102 sage: I = matrix.identity(QQ,3)
103 sage: J(I) == J.one()
104 True
105
106 This skew-symmetric matrix can't be represented in the EJA::
107
108 sage: J = RealSymmetricEJA(3)
109 sage: A = matrix(QQ,3, lambda i,j: i-j)
110 sage: J(A)
111 Traceback (most recent call last):
112 ...
113 ArithmeticError: vector is not in free module
114
115 TESTS:
116
117 Ensure that we can convert any element of the two non-matrix
118 simple algebras (whose natural representations are their usual
119 vector representations) back and forth faithfully::
120
121 sage: set_random_seed()
122 sage: J = RealCartesianProductEJA(5)
123 sage: x = J.random_element()
124 sage: J(x.to_vector().column()) == x
125 True
126 sage: J = JordanSpinEJA(5)
127 sage: x = J.random_element()
128 sage: J(x.to_vector().column()) == x
129 True
130
131 """
132 if elt == 0:
133 # The superclass implementation of random_element()
134 # needs to be able to coerce "0" into the algebra.
135 return self.zero()
136
137 natural_basis = self.natural_basis()
138 if elt not in natural_basis[0].matrix_space():
139 raise ValueError("not a naturally-represented algebra element")
140
141 # Thanks for nothing! Matrix spaces aren't vector
142 # spaces in Sage, so we have to figure out its
143 # natural-basis coordinates ourselves.
144 V = VectorSpace(elt.base_ring(), elt.nrows()*elt.ncols())
145 W = V.span_of_basis( _mat2vec(s) for s in natural_basis )
146 coords = W.coordinate_vector(_mat2vec(elt))
147 return self.from_vector(coords)
148
149
150 def _repr_(self):
151 """
152 Return a string representation of ``self``.
153
154 SETUP::
155
156 sage: from mjo.eja.eja_algebra import JordanSpinEJA
157
158 TESTS:
159
160 Ensure that it says what we think it says::
161
162 sage: JordanSpinEJA(2, field=QQ)
163 Euclidean Jordan algebra of dimension 2 over Rational Field
164 sage: JordanSpinEJA(3, field=RDF)
165 Euclidean Jordan algebra of dimension 3 over Real Double Field
166
167 """
168 fmt = "Euclidean Jordan algebra of dimension {} over {}"
169 return fmt.format(self.dimension(), self.base_ring())
170
171 def product_on_basis(self, i, j):
172 return self._multiplication_table[i][j]
173
174 def _a_regular_element(self):
175 """
176 Guess a regular element. Needed to compute the basis for our
177 characteristic polynomial coefficients.
178
179 SETUP::
180
181 sage: from mjo.eja.eja_algebra import random_eja
182
183 TESTS:
184
185 Ensure that this hacky method succeeds for every algebra that we
186 know how to construct::
187
188 sage: set_random_seed()
189 sage: J = random_eja()
190 sage: J._a_regular_element().is_regular()
191 True
192
193 """
194 gs = self.gens()
195 z = self.sum( (i+1)*gs[i] for i in range(len(gs)) )
196 if not z.is_regular():
197 raise ValueError("don't know a regular element")
198 return z
199
200
201 @cached_method
202 def _charpoly_basis_space(self):
203 """
204 Return the vector space spanned by the basis used in our
205 characteristic polynomial coefficients. This is used not only to
206 compute those coefficients, but also any time we need to
207 evaluate the coefficients (like when we compute the trace or
208 determinant).
209 """
210 z = self._a_regular_element()
211 # Don't use the parent vector space directly here in case this
212 # happens to be a subalgebra. In that case, we would be e.g.
213 # two-dimensional but span_of_basis() would expect three
214 # coordinates.
215 V = VectorSpace(self.base_ring(), self.vector_space().dimension())
216 basis = [ (z**k).to_vector() for k in range(self.rank()) ]
217 V1 = V.span_of_basis( basis )
218 b = (V1.basis() + V1.complement().basis())
219 return V.span_of_basis(b)
220
221
222 @cached_method
223 def _charpoly_coeff(self, i):
224 """
225 Return the coefficient polynomial "a_{i}" of this algebra's
226 general characteristic polynomial.
227
228 Having this be a separate cached method lets us compute and
229 store the trace/determinant (a_{r-1} and a_{0} respectively)
230 separate from the entire characteristic polynomial.
231 """
232 (A_of_x, x, xr, detA) = self._charpoly_matrix_system()
233 R = A_of_x.base_ring()
234 if i >= self.rank():
235 # Guaranteed by theory
236 return R.zero()
237
238 # Danger: the in-place modification is done for performance
239 # reasons (reconstructing a matrix with huge polynomial
240 # entries is slow), but I don't know how cached_method works,
241 # so it's highly possible that we're modifying some global
242 # list variable by reference, here. In other words, you
243 # probably shouldn't call this method twice on the same
244 # algebra, at the same time, in two threads
245 Ai_orig = A_of_x.column(i)
246 A_of_x.set_column(i,xr)
247 numerator = A_of_x.det()
248 A_of_x.set_column(i,Ai_orig)
249
250 # We're relying on the theory here to ensure that each a_i is
251 # indeed back in R, and the added negative signs are to make
252 # the whole charpoly expression sum to zero.
253 return R(-numerator/detA)
254
255
256 @cached_method
257 def _charpoly_matrix_system(self):
258 """
259 Compute the matrix whose entries A_ij are polynomials in
260 X1,...,XN, the vector ``x`` of variables X1,...,XN, the vector
261 corresponding to `x^r` and the determinent of the matrix A =
262 [A_ij]. In other words, all of the fixed (cachable) data needed
263 to compute the coefficients of the characteristic polynomial.
264 """
265 r = self.rank()
266 n = self.dimension()
267
268 # Turn my vector space into a module so that "vectors" can
269 # have multivatiate polynomial entries.
270 names = tuple('X' + str(i) for i in range(1,n+1))
271 R = PolynomialRing(self.base_ring(), names)
272
273 # Using change_ring() on the parent's vector space doesn't work
274 # here because, in a subalgebra, that vector space has a basis
275 # and change_ring() tries to bring the basis along with it. And
276 # that doesn't work unless the new ring is a PID, which it usually
277 # won't be.
278 V = FreeModule(R,n)
279
280 # Now let x = (X1,X2,...,Xn) be the vector whose entries are
281 # indeterminates...
282 x = V(names)
283
284 # And figure out the "left multiplication by x" matrix in
285 # that setting.
286 lmbx_cols = []
287 monomial_matrices = [ self.monomial(i).operator().matrix()
288 for i in range(n) ] # don't recompute these!
289 for k in range(n):
290 ek = self.monomial(k).to_vector()
291 lmbx_cols.append(
292 sum( x[i]*(monomial_matrices[i]*ek)
293 for i in range(n) ) )
294 Lx = matrix.column(R, lmbx_cols)
295
296 # Now we can compute powers of x "symbolically"
297 x_powers = [self.one().to_vector(), x]
298 for d in range(2, r+1):
299 x_powers.append( Lx*(x_powers[-1]) )
300
301 idmat = matrix.identity(R, n)
302
303 W = self._charpoly_basis_space()
304 W = W.change_ring(R.fraction_field())
305
306 # Starting with the standard coordinates x = (X1,X2,...,Xn)
307 # and then converting the entries to W-coordinates allows us
308 # to pass in the standard coordinates to the charpoly and get
309 # back the right answer. Specifically, with x = (X1,X2,...,Xn),
310 # we have
311 #
312 # W.coordinates(x^2) eval'd at (standard z-coords)
313 # =
314 # W-coords of (z^2)
315 # =
316 # W-coords of (standard coords of x^2 eval'd at std-coords of z)
317 #
318 # We want the middle equivalent thing in our matrix, but use
319 # the first equivalent thing instead so that we can pass in
320 # standard coordinates.
321 x_powers = [ W.coordinate_vector(xp) for xp in x_powers ]
322 l2 = [idmat.column(k-1) for k in range(r+1, n+1)]
323 A_of_x = matrix.column(R, n, (x_powers[:r] + l2))
324 return (A_of_x, x, x_powers[r], A_of_x.det())
325
326
327 @cached_method
328 def characteristic_polynomial(self):
329 """
330 Return a characteristic polynomial that works for all elements
331 of this algebra.
332
333 The resulting polynomial has `n+1` variables, where `n` is the
334 dimension of this algebra. The first `n` variables correspond to
335 the coordinates of an algebra element: when evaluated at the
336 coordinates of an algebra element with respect to a certain
337 basis, the result is a univariate polynomial (in the one
338 remaining variable ``t``), namely the characteristic polynomial
339 of that element.
340
341 SETUP::
342
343 sage: from mjo.eja.eja_algebra import JordanSpinEJA
344
345 EXAMPLES:
346
347 The characteristic polynomial in the spin algebra is given in
348 Alizadeh, Example 11.11::
349
350 sage: J = JordanSpinEJA(3)
351 sage: p = J.characteristic_polynomial(); p
352 X1^2 - X2^2 - X3^2 + (-2*t)*X1 + t^2
353 sage: xvec = J.one().to_vector()
354 sage: p(*xvec)
355 t^2 - 2*t + 1
356
357 """
358 r = self.rank()
359 n = self.dimension()
360
361 # The list of coefficient polynomials a_1, a_2, ..., a_n.
362 a = [ self._charpoly_coeff(i) for i in range(n) ]
363
364 # We go to a bit of trouble here to reorder the
365 # indeterminates, so that it's easier to evaluate the
366 # characteristic polynomial at x's coordinates and get back
367 # something in terms of t, which is what we want.
368 R = a[0].parent()
369 S = PolynomialRing(self.base_ring(),'t')
370 t = S.gen(0)
371 S = PolynomialRing(S, R.variable_names())
372 t = S(t)
373
374 # Note: all entries past the rth should be zero. The
375 # coefficient of the highest power (x^r) is 1, but it doesn't
376 # appear in the solution vector which contains coefficients
377 # for the other powers (to make them sum to x^r).
378 if (r < n):
379 a[r] = 1 # corresponds to x^r
380 else:
381 # When the rank is equal to the dimension, trying to
382 # assign a[r] goes out-of-bounds.
383 a.append(1) # corresponds to x^r
384
385 return sum( a[k]*(t**k) for k in range(len(a)) )
386
387
388 def inner_product(self, x, y):
389 """
390 The inner product associated with this Euclidean Jordan algebra.
391
392 Defaults to the trace inner product, but can be overridden by
393 subclasses if they are sure that the necessary properties are
394 satisfied.
395
396 SETUP::
397
398 sage: from mjo.eja.eja_algebra import random_eja
399
400 EXAMPLES:
401
402 The inner product must satisfy its axiom for this algebra to truly
403 be a Euclidean Jordan Algebra::
404
405 sage: set_random_seed()
406 sage: J = random_eja()
407 sage: x = J.random_element()
408 sage: y = J.random_element()
409 sage: z = J.random_element()
410 sage: (x*y).inner_product(z) == y.inner_product(x*z)
411 True
412
413 """
414 if (not x in self) or (not y in self):
415 raise TypeError("arguments must live in this algebra")
416 return x.trace_inner_product(y)
417
418
419 def is_trivial(self):
420 """
421 Return whether or not this algebra is trivial.
422
423 A trivial algebra contains only the zero element.
424
425 SETUP::
426
427 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
428
429 EXAMPLES::
430
431 sage: J = ComplexHermitianEJA(3)
432 sage: J.is_trivial()
433 False
434 sage: A = J.zero().subalgebra_generated_by()
435 sage: A.is_trivial()
436 True
437
438 """
439 return self.dimension() == 0
440
441
442 def multiplication_table(self):
443 """
444 Return a visual representation of this algebra's multiplication
445 table (on basis elements).
446
447 SETUP::
448
449 sage: from mjo.eja.eja_algebra import JordanSpinEJA
450
451 EXAMPLES::
452
453 sage: J = JordanSpinEJA(4)
454 sage: J.multiplication_table()
455 +----++----+----+----+----+
456 | * || e0 | e1 | e2 | e3 |
457 +====++====+====+====+====+
458 | e0 || e0 | e1 | e2 | e3 |
459 +----++----+----+----+----+
460 | e1 || e1 | e0 | 0 | 0 |
461 +----++----+----+----+----+
462 | e2 || e2 | 0 | e0 | 0 |
463 +----++----+----+----+----+
464 | e3 || e3 | 0 | 0 | e0 |
465 +----++----+----+----+----+
466
467 """
468 M = list(self._multiplication_table) # copy
469 for i in range(len(M)):
470 # M had better be "square"
471 M[i] = [self.monomial(i)] + M[i]
472 M = [["*"] + list(self.gens())] + M
473 return table(M, header_row=True, header_column=True, frame=True)
474
475
476 def natural_basis(self):
477 """
478 Return a more-natural representation of this algebra's basis.
479
480 Every finite-dimensional Euclidean Jordan Algebra is a direct
481 sum of five simple algebras, four of which comprise Hermitian
482 matrices. This method returns the original "natural" basis
483 for our underlying vector space. (Typically, the natural basis
484 is used to construct the multiplication table in the first place.)
485
486 Note that this will always return a matrix. The standard basis
487 in `R^n` will be returned as `n`-by-`1` column matrices.
488
489 SETUP::
490
491 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
492 ....: RealSymmetricEJA)
493
494 EXAMPLES::
495
496 sage: J = RealSymmetricEJA(2)
497 sage: J.basis()
498 Finite family {0: e0, 1: e1, 2: e2}
499 sage: J.natural_basis()
500 (
501 [1 0] [0 1] [0 0]
502 [0 0], [1 0], [0 1]
503 )
504
505 ::
506
507 sage: J = JordanSpinEJA(2)
508 sage: J.basis()
509 Finite family {0: e0, 1: e1}
510 sage: J.natural_basis()
511 (
512 [1] [0]
513 [0], [1]
514 )
515
516 """
517 if self._natural_basis is None:
518 M = self.natural_basis_space()
519 return tuple( M(b.to_vector()) for b in self.basis() )
520 else:
521 return self._natural_basis
522
523
524 def natural_basis_space(self):
525 """
526 Return the matrix space in which this algebra's natural basis
527 elements live.
528 """
529 if self._natural_basis is None or len(self._natural_basis) == 0:
530 return MatrixSpace(self.base_ring(), self.dimension(), 1)
531 else:
532 return self._natural_basis[0].matrix_space()
533
534
535 @cached_method
536 def one(self):
537 """
538 Return the unit element of this algebra.
539
540 SETUP::
541
542 sage: from mjo.eja.eja_algebra import (RealCartesianProductEJA,
543 ....: random_eja)
544
545 EXAMPLES::
546
547 sage: J = RealCartesianProductEJA(5)
548 sage: J.one()
549 e0 + e1 + e2 + e3 + e4
550
551 TESTS:
552
553 The identity element acts like the identity::
554
555 sage: set_random_seed()
556 sage: J = random_eja()
557 sage: x = J.random_element()
558 sage: J.one()*x == x and x*J.one() == x
559 True
560
561 The matrix of the unit element's operator is the identity::
562
563 sage: set_random_seed()
564 sage: J = random_eja()
565 sage: actual = J.one().operator().matrix()
566 sage: expected = matrix.identity(J.base_ring(), J.dimension())
567 sage: actual == expected
568 True
569
570 """
571 # We can brute-force compute the matrices of the operators
572 # that correspond to the basis elements of this algebra.
573 # If some linear combination of those basis elements is the
574 # algebra identity, then the same linear combination of
575 # their matrices has to be the identity matrix.
576 #
577 # Of course, matrices aren't vectors in sage, so we have to
578 # appeal to the "long vectors" isometry.
579 oper_vecs = [ _mat2vec(g.operator().matrix()) for g in self.gens() ]
580
581 # Now we use basis linear algebra to find the coefficients,
582 # of the matrices-as-vectors-linear-combination, which should
583 # work for the original algebra basis too.
584 A = matrix.column(self.base_ring(), oper_vecs)
585
586 # We used the isometry on the left-hand side already, but we
587 # still need to do it for the right-hand side. Recall that we
588 # wanted something that summed to the identity matrix.
589 b = _mat2vec( matrix.identity(self.base_ring(), self.dimension()) )
590
591 # Now if there's an identity element in the algebra, this should work.
592 coeffs = A.solve_right(b)
593 return self.linear_combination(zip(self.gens(), coeffs))
594
595
596 def random_element(self):
597 # Temporary workaround for https://trac.sagemath.org/ticket/28327
598 if self.is_trivial():
599 return self.zero()
600 else:
601 s = super(FiniteDimensionalEuclideanJordanAlgebra, self)
602 return s.random_element()
603
604
605 def rank(self):
606 """
607 Return the rank of this EJA.
608
609 ALGORITHM:
610
611 The author knows of no algorithm to compute the rank of an EJA
612 where only the multiplication table is known. In lieu of one, we
613 require the rank to be specified when the algebra is created,
614 and simply pass along that number here.
615
616 SETUP::
617
618 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
619 ....: RealSymmetricEJA,
620 ....: ComplexHermitianEJA,
621 ....: QuaternionHermitianEJA,
622 ....: random_eja)
623
624 EXAMPLES:
625
626 The rank of the Jordan spin algebra is always two::
627
628 sage: JordanSpinEJA(2).rank()
629 2
630 sage: JordanSpinEJA(3).rank()
631 2
632 sage: JordanSpinEJA(4).rank()
633 2
634
635 The rank of the `n`-by-`n` Hermitian real, complex, or
636 quaternion matrices is `n`::
637
638 sage: RealSymmetricEJA(2).rank()
639 2
640 sage: ComplexHermitianEJA(2).rank()
641 2
642 sage: QuaternionHermitianEJA(2).rank()
643 2
644 sage: RealSymmetricEJA(5).rank()
645 5
646 sage: ComplexHermitianEJA(5).rank()
647 5
648 sage: QuaternionHermitianEJA(5).rank()
649 5
650
651 TESTS:
652
653 Ensure that every EJA that we know how to construct has a
654 positive integer rank::
655
656 sage: set_random_seed()
657 sage: r = random_eja().rank()
658 sage: r in ZZ and r > 0
659 True
660
661 """
662 return self._rank
663
664
665 def vector_space(self):
666 """
667 Return the vector space that underlies this algebra.
668
669 SETUP::
670
671 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
672
673 EXAMPLES::
674
675 sage: J = RealSymmetricEJA(2)
676 sage: J.vector_space()
677 Vector space of dimension 3 over Rational Field
678
679 """
680 return self.zero().to_vector().parent().ambient_vector_space()
681
682
683 Element = FiniteDimensionalEuclideanJordanAlgebraElement
684
685
686 class RealCartesianProductEJA(FiniteDimensionalEuclideanJordanAlgebra):
687 """
688 Return the Euclidean Jordan Algebra corresponding to the set
689 `R^n` under the Hadamard product.
690
691 Note: this is nothing more than the Cartesian product of ``n``
692 copies of the spin algebra. Once Cartesian product algebras
693 are implemented, this can go.
694
695 SETUP::
696
697 sage: from mjo.eja.eja_algebra import RealCartesianProductEJA
698
699 EXAMPLES:
700
701 This multiplication table can be verified by hand::
702
703 sage: J = RealCartesianProductEJA(3)
704 sage: e0,e1,e2 = J.gens()
705 sage: e0*e0
706 e0
707 sage: e0*e1
708 0
709 sage: e0*e2
710 0
711 sage: e1*e1
712 e1
713 sage: e1*e2
714 0
715 sage: e2*e2
716 e2
717
718 TESTS:
719
720 We can change the generator prefix::
721
722 sage: RealCartesianProductEJA(3, prefix='r').gens()
723 (r0, r1, r2)
724
725 Our inner product satisfies the Jordan axiom::
726
727 sage: set_random_seed()
728 sage: n = ZZ.random_element(1,5)
729 sage: J = RealCartesianProductEJA(n)
730 sage: x = J.random_element()
731 sage: y = J.random_element()
732 sage: z = J.random_element()
733 sage: (x*y).inner_product(z) == y.inner_product(x*z)
734 True
735
736 """
737 def __init__(self, n, field=QQ, **kwargs):
738 V = VectorSpace(field, n)
739 mult_table = [ [ V.gen(i)*(i == j) for j in range(n) ]
740 for i in range(n) ]
741
742 fdeja = super(RealCartesianProductEJA, self)
743 return fdeja.__init__(field, mult_table, rank=n, **kwargs)
744
745 def inner_product(self, x, y):
746 return _usual_ip(x,y)
747
748
749 def random_eja():
750 """
751 Return a "random" finite-dimensional Euclidean Jordan Algebra.
752
753 ALGORITHM:
754
755 For now, we choose a random natural number ``n`` (greater than zero)
756 and then give you back one of the following:
757
758 * The cartesian product of the rational numbers ``n`` times; this is
759 ``QQ^n`` with the Hadamard product.
760
761 * The Jordan spin algebra on ``QQ^n``.
762
763 * The ``n``-by-``n`` rational symmetric matrices with the symmetric
764 product.
765
766 * The ``n``-by-``n`` complex-rational Hermitian matrices embedded
767 in the space of ``2n``-by-``2n`` real symmetric matrices.
768
769 * The ``n``-by-``n`` quaternion-rational Hermitian matrices embedded
770 in the space of ``4n``-by-``4n`` real symmetric matrices.
771
772 Later this might be extended to return Cartesian products of the
773 EJAs above.
774
775 SETUP::
776
777 sage: from mjo.eja.eja_algebra import random_eja
778
779 TESTS::
780
781 sage: random_eja()
782 Euclidean Jordan algebra of dimension...
783
784 """
785
786 # The max_n component lets us choose different upper bounds on the
787 # value "n" that gets passed to the constructor. This is needed
788 # because e.g. R^{10} is reasonable to test, while the Hermitian
789 # 10-by-10 quaternion matrices are not.
790 (constructor, max_n) = choice([(RealCartesianProductEJA, 6),
791 (JordanSpinEJA, 6),
792 (RealSymmetricEJA, 5),
793 (ComplexHermitianEJA, 4),
794 (QuaternionHermitianEJA, 3)])
795 n = ZZ.random_element(1, max_n)
796 return constructor(n, field=QQ)
797
798
799
800 def _real_symmetric_basis(n, field):
801 """
802 Return a basis for the space of real symmetric n-by-n matrices.
803
804 SETUP::
805
806 sage: from mjo.eja.eja_algebra import _real_symmetric_basis
807
808 TESTS::
809
810 sage: set_random_seed()
811 sage: n = ZZ.random_element(1,5)
812 sage: B = _real_symmetric_basis(n, QQbar)
813 sage: all( M.is_symmetric() for M in B)
814 True
815
816 """
817 # The basis of symmetric matrices, as matrices, in their R^(n-by-n)
818 # coordinates.
819 S = []
820 for i in xrange(n):
821 for j in xrange(i+1):
822 Eij = matrix(field, n, lambda k,l: k==i and l==j)
823 if i == j:
824 Sij = Eij
825 else:
826 # Beware, orthogonal but not normalized!
827 Sij = Eij + Eij.transpose()
828 S.append(Sij)
829 return tuple(S)
830
831
832 def _complex_hermitian_basis(n, field):
833 """
834 Returns a basis for the space of complex Hermitian n-by-n matrices.
835
836 SETUP::
837
838 sage: from mjo.eja.eja_algebra import _complex_hermitian_basis
839
840 TESTS::
841
842 sage: set_random_seed()
843 sage: n = ZZ.random_element(1,5)
844 sage: all( M.is_symmetric() for M in _complex_hermitian_basis(n) )
845 True
846
847 """
848 F = QuadraticField(-1, 'I')
849 I = F.gen()
850
851 # This is like the symmetric case, but we need to be careful:
852 #
853 # * We want conjugate-symmetry, not just symmetry.
854 # * The diagonal will (as a result) be real.
855 #
856 S = []
857 for i in xrange(n):
858 for j in xrange(i+1):
859 Eij = matrix(field, n, lambda k,l: k==i and l==j)
860 if i == j:
861 Sij = _embed_complex_matrix(Eij)
862 S.append(Sij)
863 else:
864 # Beware, orthogonal but not normalized! The second one
865 # has a minus because it's conjugated.
866 Sij_real = _embed_complex_matrix(Eij + Eij.transpose())
867 S.append(Sij_real)
868 Sij_imag = _embed_complex_matrix(I*Eij - I*Eij.transpose())
869 S.append(Sij_imag)
870 return tuple(S)
871
872
873 def _quaternion_hermitian_basis(n, field):
874 """
875 Returns a basis for the space of quaternion Hermitian n-by-n matrices.
876
877 SETUP::
878
879 sage: from mjo.eja.eja_algebra import _quaternion_hermitian_basis
880
881 TESTS::
882
883 sage: set_random_seed()
884 sage: n = ZZ.random_element(1,5)
885 sage: all( M.is_symmetric() for M in _quaternion_hermitian_basis(n) )
886 True
887
888 """
889 Q = QuaternionAlgebra(QQ,-1,-1)
890 I,J,K = Q.gens()
891
892 # This is like the symmetric case, but we need to be careful:
893 #
894 # * We want conjugate-symmetry, not just symmetry.
895 # * The diagonal will (as a result) be real.
896 #
897 S = []
898 for i in xrange(n):
899 for j in xrange(i+1):
900 Eij = matrix(Q, n, lambda k,l: k==i and l==j)
901 if i == j:
902 Sij = _embed_quaternion_matrix(Eij)
903 S.append(Sij)
904 else:
905 # Beware, orthogonal but not normalized! The second,
906 # third, and fourth ones have a minus because they're
907 # conjugated.
908 Sij_real = _embed_quaternion_matrix(Eij + Eij.transpose())
909 S.append(Sij_real)
910 Sij_I = _embed_quaternion_matrix(I*Eij - I*Eij.transpose())
911 S.append(Sij_I)
912 Sij_J = _embed_quaternion_matrix(J*Eij - J*Eij.transpose())
913 S.append(Sij_J)
914 Sij_K = _embed_quaternion_matrix(K*Eij - K*Eij.transpose())
915 S.append(Sij_K)
916 return tuple(S)
917
918
919
920 def _multiplication_table_from_matrix_basis(basis):
921 """
922 At least three of the five simple Euclidean Jordan algebras have the
923 symmetric multiplication (A,B) |-> (AB + BA)/2, where the
924 multiplication on the right is matrix multiplication. Given a basis
925 for the underlying matrix space, this function returns a
926 multiplication table (obtained by looping through the basis
927 elements) for an algebra of those matrices.
928 """
929 # In S^2, for example, we nominally have four coordinates even
930 # though the space is of dimension three only. The vector space V
931 # is supposed to hold the entire long vector, and the subspace W
932 # of V will be spanned by the vectors that arise from symmetric
933 # matrices. Thus for S^2, dim(V) == 4 and dim(W) == 3.
934 field = basis[0].base_ring()
935 dimension = basis[0].nrows()
936
937 V = VectorSpace(field, dimension**2)
938 W = V.span_of_basis( _mat2vec(s) for s in basis )
939 n = len(basis)
940 mult_table = [[W.zero() for j in range(n)] for i in range(n)]
941 for i in range(n):
942 for j in range(n):
943 mat_entry = (basis[i]*basis[j] + basis[j]*basis[i])/2
944 mult_table[i][j] = W.coordinate_vector(_mat2vec(mat_entry))
945
946 return mult_table
947
948
949 def _embed_complex_matrix(M):
950 """
951 Embed the n-by-n complex matrix ``M`` into the space of real
952 matrices of size 2n-by-2n via the map the sends each entry `z = a +
953 bi` to the block matrix ``[[a,b],[-b,a]]``.
954
955 SETUP::
956
957 sage: from mjo.eja.eja_algebra import _embed_complex_matrix
958
959 EXAMPLES::
960
961 sage: F = QuadraticField(-1,'i')
962 sage: x1 = F(4 - 2*i)
963 sage: x2 = F(1 + 2*i)
964 sage: x3 = F(-i)
965 sage: x4 = F(6)
966 sage: M = matrix(F,2,[[x1,x2],[x3,x4]])
967 sage: _embed_complex_matrix(M)
968 [ 4 -2| 1 2]
969 [ 2 4|-2 1]
970 [-----+-----]
971 [ 0 -1| 6 0]
972 [ 1 0| 0 6]
973
974 TESTS:
975
976 Embedding is a homomorphism (isomorphism, in fact)::
977
978 sage: set_random_seed()
979 sage: n = ZZ.random_element(5)
980 sage: F = QuadraticField(-1, 'i')
981 sage: X = random_matrix(F, n)
982 sage: Y = random_matrix(F, n)
983 sage: actual = _embed_complex_matrix(X) * _embed_complex_matrix(Y)
984 sage: expected = _embed_complex_matrix(X*Y)
985 sage: actual == expected
986 True
987
988 """
989 n = M.nrows()
990 if M.ncols() != n:
991 raise ValueError("the matrix 'M' must be square")
992 field = M.base_ring()
993 blocks = []
994 for z in M.list():
995 a = z.real()
996 b = z.imag()
997 blocks.append(matrix(field, 2, [[a,b],[-b,a]]))
998
999 # We can drop the imaginaries here.
1000 return matrix.block(field.base_ring(), n, blocks)
1001
1002
1003 def _unembed_complex_matrix(M):
1004 """
1005 The inverse of _embed_complex_matrix().
1006
1007 SETUP::
1008
1009 sage: from mjo.eja.eja_algebra import (_embed_complex_matrix,
1010 ....: _unembed_complex_matrix)
1011
1012 EXAMPLES::
1013
1014 sage: A = matrix(QQ,[ [ 1, 2, 3, 4],
1015 ....: [-2, 1, -4, 3],
1016 ....: [ 9, 10, 11, 12],
1017 ....: [-10, 9, -12, 11] ])
1018 sage: _unembed_complex_matrix(A)
1019 [ 2*i + 1 4*i + 3]
1020 [ 10*i + 9 12*i + 11]
1021
1022 TESTS:
1023
1024 Unembedding is the inverse of embedding::
1025
1026 sage: set_random_seed()
1027 sage: F = QuadraticField(-1, 'i')
1028 sage: M = random_matrix(F, 3)
1029 sage: _unembed_complex_matrix(_embed_complex_matrix(M)) == M
1030 True
1031
1032 """
1033 n = ZZ(M.nrows())
1034 if M.ncols() != n:
1035 raise ValueError("the matrix 'M' must be square")
1036 if not n.mod(2).is_zero():
1037 raise ValueError("the matrix 'M' must be a complex embedding")
1038
1039 F = QuadraticField(-1, 'i')
1040 i = F.gen()
1041
1042 # Go top-left to bottom-right (reading order), converting every
1043 # 2-by-2 block we see to a single complex element.
1044 elements = []
1045 for k in xrange(n/2):
1046 for j in xrange(n/2):
1047 submat = M[2*k:2*k+2,2*j:2*j+2]
1048 if submat[0,0] != submat[1,1]:
1049 raise ValueError('bad on-diagonal submatrix')
1050 if submat[0,1] != -submat[1,0]:
1051 raise ValueError('bad off-diagonal submatrix')
1052 z = submat[0,0] + submat[0,1]*i
1053 elements.append(z)
1054
1055 return matrix(F, n/2, elements)
1056
1057
1058 def _embed_quaternion_matrix(M):
1059 """
1060 Embed the n-by-n quaternion matrix ``M`` into the space of real
1061 matrices of size 4n-by-4n by first sending each quaternion entry
1062 `z = a + bi + cj + dk` to the block-complex matrix
1063 ``[[a + bi, c+di],[-c + di, a-bi]]`, and then embedding those into
1064 a real matrix.
1065
1066 SETUP::
1067
1068 sage: from mjo.eja.eja_algebra import _embed_quaternion_matrix
1069
1070 EXAMPLES::
1071
1072 sage: Q = QuaternionAlgebra(QQ,-1,-1)
1073 sage: i,j,k = Q.gens()
1074 sage: x = 1 + 2*i + 3*j + 4*k
1075 sage: M = matrix(Q, 1, [[x]])
1076 sage: _embed_quaternion_matrix(M)
1077 [ 1 2 3 4]
1078 [-2 1 -4 3]
1079 [-3 4 1 -2]
1080 [-4 -3 2 1]
1081
1082 Embedding is a homomorphism (isomorphism, in fact)::
1083
1084 sage: set_random_seed()
1085 sage: n = ZZ.random_element(5)
1086 sage: Q = QuaternionAlgebra(QQ,-1,-1)
1087 sage: X = random_matrix(Q, n)
1088 sage: Y = random_matrix(Q, n)
1089 sage: actual = _embed_quaternion_matrix(X)*_embed_quaternion_matrix(Y)
1090 sage: expected = _embed_quaternion_matrix(X*Y)
1091 sage: actual == expected
1092 True
1093
1094 """
1095 quaternions = M.base_ring()
1096 n = M.nrows()
1097 if M.ncols() != n:
1098 raise ValueError("the matrix 'M' must be square")
1099
1100 F = QuadraticField(-1, 'i')
1101 i = F.gen()
1102
1103 blocks = []
1104 for z in M.list():
1105 t = z.coefficient_tuple()
1106 a = t[0]
1107 b = t[1]
1108 c = t[2]
1109 d = t[3]
1110 cplx_matrix = matrix(F, 2, [[ a + b*i, c + d*i],
1111 [-c + d*i, a - b*i]])
1112 blocks.append(_embed_complex_matrix(cplx_matrix))
1113
1114 # We should have real entries by now, so use the realest field
1115 # we've got for the return value.
1116 return matrix.block(quaternions.base_ring(), n, blocks)
1117
1118
1119 def _unembed_quaternion_matrix(M):
1120 """
1121 The inverse of _embed_quaternion_matrix().
1122
1123 SETUP::
1124
1125 sage: from mjo.eja.eja_algebra import (_embed_quaternion_matrix,
1126 ....: _unembed_quaternion_matrix)
1127
1128 EXAMPLES::
1129
1130 sage: M = matrix(QQ, [[ 1, 2, 3, 4],
1131 ....: [-2, 1, -4, 3],
1132 ....: [-3, 4, 1, -2],
1133 ....: [-4, -3, 2, 1]])
1134 sage: _unembed_quaternion_matrix(M)
1135 [1 + 2*i + 3*j + 4*k]
1136
1137 TESTS:
1138
1139 Unembedding is the inverse of embedding::
1140
1141 sage: set_random_seed()
1142 sage: Q = QuaternionAlgebra(QQ, -1, -1)
1143 sage: M = random_matrix(Q, 3)
1144 sage: _unembed_quaternion_matrix(_embed_quaternion_matrix(M)) == M
1145 True
1146
1147 """
1148 n = ZZ(M.nrows())
1149 if M.ncols() != n:
1150 raise ValueError("the matrix 'M' must be square")
1151 if not n.mod(4).is_zero():
1152 raise ValueError("the matrix 'M' must be a complex embedding")
1153
1154 Q = QuaternionAlgebra(QQ,-1,-1)
1155 i,j,k = Q.gens()
1156
1157 # Go top-left to bottom-right (reading order), converting every
1158 # 4-by-4 block we see to a 2-by-2 complex block, to a 1-by-1
1159 # quaternion block.
1160 elements = []
1161 for l in xrange(n/4):
1162 for m in xrange(n/4):
1163 submat = _unembed_complex_matrix(M[4*l:4*l+4,4*m:4*m+4])
1164 if submat[0,0] != submat[1,1].conjugate():
1165 raise ValueError('bad on-diagonal submatrix')
1166 if submat[0,1] != -submat[1,0].conjugate():
1167 raise ValueError('bad off-diagonal submatrix')
1168 z = submat[0,0].real() + submat[0,0].imag()*i
1169 z += submat[0,1].real()*j + submat[0,1].imag()*k
1170 elements.append(z)
1171
1172 return matrix(Q, n/4, elements)
1173
1174
1175 # The usual inner product on R^n.
1176 def _usual_ip(x,y):
1177 return x.to_vector().inner_product(y.to_vector())
1178
1179 # The inner product used for the real symmetric simple EJA.
1180 # We keep it as a separate function because e.g. the complex
1181 # algebra uses the same inner product, except divided by 2.
1182 def _matrix_ip(X,Y):
1183 X_mat = X.natural_representation()
1184 Y_mat = Y.natural_representation()
1185 return (X_mat*Y_mat).trace()
1186
1187
1188 class RealSymmetricEJA(FiniteDimensionalEuclideanJordanAlgebra):
1189 """
1190 The rank-n simple EJA consisting of real symmetric n-by-n
1191 matrices, the usual symmetric Jordan product, and the trace inner
1192 product. It has dimension `(n^2 + n)/2` over the reals.
1193
1194 SETUP::
1195
1196 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1197
1198 EXAMPLES::
1199
1200 sage: J = RealSymmetricEJA(2)
1201 sage: e0, e1, e2 = J.gens()
1202 sage: e0*e0
1203 e0
1204 sage: e1*e1
1205 e0 + e2
1206 sage: e2*e2
1207 e2
1208
1209 TESTS:
1210
1211 The dimension of this algebra is `(n^2 + n) / 2`::
1212
1213 sage: set_random_seed()
1214 sage: n = ZZ.random_element(1,5)
1215 sage: J = RealSymmetricEJA(n)
1216 sage: J.dimension() == (n^2 + n)/2
1217 True
1218
1219 The Jordan multiplication is what we think it is::
1220
1221 sage: set_random_seed()
1222 sage: n = ZZ.random_element(1,5)
1223 sage: J = RealSymmetricEJA(n)
1224 sage: x = J.random_element()
1225 sage: y = J.random_element()
1226 sage: actual = (x*y).natural_representation()
1227 sage: X = x.natural_representation()
1228 sage: Y = y.natural_representation()
1229 sage: expected = (X*Y + Y*X)/2
1230 sage: actual == expected
1231 True
1232 sage: J(expected) == x*y
1233 True
1234
1235 We can change the generator prefix::
1236
1237 sage: RealSymmetricEJA(3, prefix='q').gens()
1238 (q0, q1, q2, q3, q4, q5)
1239
1240 Our inner product satisfies the Jordan axiom::
1241
1242 sage: set_random_seed()
1243 sage: n = ZZ.random_element(1,5)
1244 sage: J = RealSymmetricEJA(n)
1245 sage: x = J.random_element()
1246 sage: y = J.random_element()
1247 sage: z = J.random_element()
1248 sage: (x*y).inner_product(z) == y.inner_product(x*z)
1249 True
1250
1251 """
1252 def __init__(self, n, field=QQ, **kwargs):
1253 S = _real_symmetric_basis(n, field)
1254 Qs = _multiplication_table_from_matrix_basis(S)
1255
1256 fdeja = super(RealSymmetricEJA, self)
1257 return fdeja.__init__(field,
1258 Qs,
1259 rank=n,
1260 natural_basis=S,
1261 **kwargs)
1262
1263 def inner_product(self, x, y):
1264 return _matrix_ip(x,y)
1265
1266
1267 class ComplexHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra):
1268 """
1269 The rank-n simple EJA consisting of complex Hermitian n-by-n
1270 matrices over the real numbers, the usual symmetric Jordan product,
1271 and the real-part-of-trace inner product. It has dimension `n^2` over
1272 the reals.
1273
1274 SETUP::
1275
1276 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1277
1278 TESTS:
1279
1280 The dimension of this algebra is `n^2`::
1281
1282 sage: set_random_seed()
1283 sage: n = ZZ.random_element(1,5)
1284 sage: J = ComplexHermitianEJA(n)
1285 sage: J.dimension() == n^2
1286 True
1287
1288 The Jordan multiplication is what we think it is::
1289
1290 sage: set_random_seed()
1291 sage: n = ZZ.random_element(1,5)
1292 sage: J = ComplexHermitianEJA(n)
1293 sage: x = J.random_element()
1294 sage: y = J.random_element()
1295 sage: actual = (x*y).natural_representation()
1296 sage: X = x.natural_representation()
1297 sage: Y = y.natural_representation()
1298 sage: expected = (X*Y + Y*X)/2
1299 sage: actual == expected
1300 True
1301 sage: J(expected) == x*y
1302 True
1303
1304 We can change the generator prefix::
1305
1306 sage: ComplexHermitianEJA(2, prefix='z').gens()
1307 (z0, z1, z2, z3)
1308
1309 Our inner product satisfies the Jordan axiom::
1310
1311 sage: set_random_seed()
1312 sage: n = ZZ.random_element(1,5)
1313 sage: J = ComplexHermitianEJA(n)
1314 sage: x = J.random_element()
1315 sage: y = J.random_element()
1316 sage: z = J.random_element()
1317 sage: (x*y).inner_product(z) == y.inner_product(x*z)
1318 True
1319
1320 """
1321 def __init__(self, n, field=QQ, **kwargs):
1322 S = _complex_hermitian_basis(n, field)
1323 Qs = _multiplication_table_from_matrix_basis(S)
1324
1325 fdeja = super(ComplexHermitianEJA, self)
1326 return fdeja.__init__(field,
1327 Qs,
1328 rank=n,
1329 natural_basis=S,
1330 **kwargs)
1331
1332
1333 def inner_product(self, x, y):
1334 # Since a+bi on the diagonal is represented as
1335 #
1336 # a + bi = [ a b ]
1337 # [ -b a ],
1338 #
1339 # we'll double-count the "a" entries if we take the trace of
1340 # the embedding.
1341 return _matrix_ip(x,y)/2
1342
1343
1344 class QuaternionHermitianEJA(FiniteDimensionalEuclideanJordanAlgebra):
1345 """
1346 The rank-n simple EJA consisting of self-adjoint n-by-n quaternion
1347 matrices, the usual symmetric Jordan product, and the
1348 real-part-of-trace inner product. It has dimension `2n^2 - n` over
1349 the reals.
1350
1351 SETUP::
1352
1353 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
1354
1355 TESTS:
1356
1357 The dimension of this algebra is `n^2`::
1358
1359 sage: set_random_seed()
1360 sage: n = ZZ.random_element(1,5)
1361 sage: J = QuaternionHermitianEJA(n)
1362 sage: J.dimension() == 2*(n^2) - n
1363 True
1364
1365 The Jordan multiplication is what we think it is::
1366
1367 sage: set_random_seed()
1368 sage: n = ZZ.random_element(1,5)
1369 sage: J = QuaternionHermitianEJA(n)
1370 sage: x = J.random_element()
1371 sage: y = J.random_element()
1372 sage: actual = (x*y).natural_representation()
1373 sage: X = x.natural_representation()
1374 sage: Y = y.natural_representation()
1375 sage: expected = (X*Y + Y*X)/2
1376 sage: actual == expected
1377 True
1378 sage: J(expected) == x*y
1379 True
1380
1381 We can change the generator prefix::
1382
1383 sage: QuaternionHermitianEJA(2, prefix='a').gens()
1384 (a0, a1, a2, a3, a4, a5)
1385
1386 Our inner product satisfies the Jordan axiom::
1387
1388 sage: set_random_seed()
1389 sage: n = ZZ.random_element(1,5)
1390 sage: J = QuaternionHermitianEJA(n)
1391 sage: x = J.random_element()
1392 sage: y = J.random_element()
1393 sage: z = J.random_element()
1394 sage: (x*y).inner_product(z) == y.inner_product(x*z)
1395 True
1396
1397 """
1398 def __init__(self, n, field=QQ, **kwargs):
1399 S = _quaternion_hermitian_basis(n, field)
1400 Qs = _multiplication_table_from_matrix_basis(S)
1401
1402 fdeja = super(QuaternionHermitianEJA, self)
1403 return fdeja.__init__(field,
1404 Qs,
1405 rank=n,
1406 natural_basis=S,
1407 **kwargs)
1408
1409 def inner_product(self, x, y):
1410 # Since a+bi+cj+dk on the diagonal is represented as
1411 #
1412 # a + bi +cj + dk = [ a b c d]
1413 # [ -b a -d c]
1414 # [ -c d a -b]
1415 # [ -d -c b a],
1416 #
1417 # we'll quadruple-count the "a" entries if we take the trace of
1418 # the embedding.
1419 return _matrix_ip(x,y)/4
1420
1421
1422 class JordanSpinEJA(FiniteDimensionalEuclideanJordanAlgebra):
1423 """
1424 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
1425 with the usual inner product and jordan product ``x*y =
1426 (<x_bar,y_bar>, x0*y_bar + y0*x_bar)``. It has dimension `n` over
1427 the reals.
1428
1429 SETUP::
1430
1431 sage: from mjo.eja.eja_algebra import JordanSpinEJA
1432
1433 EXAMPLES:
1434
1435 This multiplication table can be verified by hand::
1436
1437 sage: J = JordanSpinEJA(4)
1438 sage: e0,e1,e2,e3 = J.gens()
1439 sage: e0*e0
1440 e0
1441 sage: e0*e1
1442 e1
1443 sage: e0*e2
1444 e2
1445 sage: e0*e3
1446 e3
1447 sage: e1*e2
1448 0
1449 sage: e1*e3
1450 0
1451 sage: e2*e3
1452 0
1453
1454 We can change the generator prefix::
1455
1456 sage: JordanSpinEJA(2, prefix='B').gens()
1457 (B0, B1)
1458
1459 Our inner product satisfies the Jordan axiom::
1460
1461 sage: set_random_seed()
1462 sage: n = ZZ.random_element(1,5)
1463 sage: J = JordanSpinEJA(n)
1464 sage: x = J.random_element()
1465 sage: y = J.random_element()
1466 sage: z = J.random_element()
1467 sage: (x*y).inner_product(z) == y.inner_product(x*z)
1468 True
1469
1470 """
1471 def __init__(self, n, field=QQ, **kwargs):
1472 V = VectorSpace(field, n)
1473 mult_table = [[V.zero() for j in range(n)] for i in range(n)]
1474 for i in range(n):
1475 for j in range(n):
1476 x = V.gen(i)
1477 y = V.gen(j)
1478 x0 = x[0]
1479 xbar = x[1:]
1480 y0 = y[0]
1481 ybar = y[1:]
1482 # z = x*y
1483 z0 = x.inner_product(y)
1484 zbar = y0*xbar + x0*ybar
1485 z = V([z0] + zbar.list())
1486 mult_table[i][j] = z
1487
1488 # The rank of the spin algebra is two, unless we're in a
1489 # one-dimensional ambient space (because the rank is bounded by
1490 # the ambient dimension).
1491 fdeja = super(JordanSpinEJA, self)
1492 return fdeja.__init__(field, mult_table, rank=min(n,2), **kwargs)
1493
1494 def inner_product(self, x, y):
1495 return _usual_ip(x,y)