]> gitweb.michael.orlitzky.com - sage.d.git/blob - mjo/eja/eja_element.py
85ec494e53beaf05c788dbd5b04a23a18c0952d6
[sage.d.git] / mjo / eja / eja_element.py
1 from sage.matrix.constructor import matrix
2 from sage.misc.cachefunc import cached_method
3 from sage.modules.free_module import VectorSpace
4 from sage.modules.with_basis.indexed_element import IndexedFreeModuleElement
5
6 from mjo.eja.eja_operator import FiniteDimensionalEJAOperator
7 from mjo.eja.eja_utils import _scale
8
9
10 class FiniteDimensionalEJAElement(IndexedFreeModuleElement):
11 """
12 An element of a Euclidean Jordan algebra.
13 """
14
15 def __dir__(self):
16 """
17 Oh man, I should not be doing this. This hides the "disabled"
18 methods ``left_matrix`` and ``matrix`` from introspection;
19 in particular it removes them from tab-completion.
20 """
21 return filter(lambda s: s not in ['left_matrix', 'matrix'],
22 dir(self.__class__) )
23
24
25
26
27 def __pow__(self, n):
28 """
29 Return ``self`` raised to the power ``n``.
30
31 Jordan algebras are always power-associative; see for
32 example Faraut and Korányi, Proposition II.1.2 (ii).
33
34 We have to override this because our superclass uses row
35 vectors instead of column vectors! We, on the other hand,
36 assume column vectors everywhere.
37
38 SETUP::
39
40 sage: from mjo.eja.eja_algebra import random_eja
41
42 TESTS:
43
44 The definition of `x^2` is the unambiguous `x*x`::
45
46 sage: x = random_eja().random_element()
47 sage: x*x == (x^2)
48 True
49
50 A few examples of power-associativity::
51
52 sage: x = random_eja().random_element()
53 sage: x*(x*x)*(x*x) == x^5
54 True
55 sage: (x*x)*(x*x*x) == x^5
56 True
57
58 We also know that powers operator-commute (Koecher, Chapter
59 III, Corollary 1)::
60
61 sage: x = random_eja().random_element()
62 sage: m = ZZ.random_element(0,10)
63 sage: n = ZZ.random_element(0,10)
64 sage: Lxm = (x^m).operator()
65 sage: Lxn = (x^n).operator()
66 sage: Lxm*Lxn == Lxn*Lxm
67 True
68
69 """
70 if n == 0:
71 return self.parent().one()
72 elif n == 1:
73 return self
74 else:
75 return (self**(n-1))*self
76
77
78 def apply_univariate_polynomial(self, p):
79 """
80 Apply the univariate polynomial ``p`` to this element.
81
82 A priori, SageMath won't allow us to apply a univariate
83 polynomial to an element of an EJA, because we don't know
84 that EJAs are rings (they are usually not associative). Of
85 course, we know that EJAs are power-associative, so the
86 operation is ultimately kosher. This function sidesteps
87 the CAS to get the answer we want and expect.
88
89 SETUP::
90
91 sage: from mjo.eja.eja_algebra import (HadamardEJA,
92 ....: random_eja)
93
94 EXAMPLES::
95
96 sage: R = PolynomialRing(QQ, 't')
97 sage: t = R.gen(0)
98 sage: p = t^4 - t^3 + 5*t - 2
99 sage: J = HadamardEJA(5)
100 sage: J.one().apply_univariate_polynomial(p) == 3*J.one()
101 True
102
103 TESTS:
104
105 We should always get back an element of the algebra::
106
107 sage: p = PolynomialRing(AA, 't').random_element()
108 sage: J = random_eja()
109 sage: x = J.random_element()
110 sage: x.apply_univariate_polynomial(p) in J
111 True
112
113 """
114 if len(p.variables()) > 1:
115 raise ValueError("not a univariate polynomial")
116 P = self.parent()
117 R = P.base_ring()
118 # Convert the coeficcients to the parent's base ring,
119 # because a priori they might live in an (unnecessarily)
120 # larger ring for which P.sum() would fail below.
121 cs = [ R(c) for c in p.coefficients(sparse=False) ]
122 return P.sum( cs[k]*(self**k) for k in range(len(cs)) )
123
124
125 def characteristic_polynomial(self):
126 """
127 Return the characteristic polynomial of this element.
128
129 SETUP::
130
131 sage: from mjo.eja.eja_algebra import (random_eja,
132 ....: HadamardEJA)
133
134 EXAMPLES:
135
136 The rank of `R^3` is three, and the minimal polynomial of
137 the identity element is `(t-1)` from which it follows that
138 the characteristic polynomial should be `(t-1)^3`::
139
140 sage: J = HadamardEJA(3)
141 sage: J.one().characteristic_polynomial()
142 t^3 - 3*t^2 + 3*t - 1
143
144 Likewise, the characteristic of the zero element in the
145 rank-three algebra `R^{n}` should be `t^{3}`::
146
147 sage: J = HadamardEJA(3)
148 sage: J.zero().characteristic_polynomial()
149 t^3
150
151 TESTS:
152
153 The characteristic polynomial of an element should evaluate
154 to zero on that element::
155
156 sage: x = random_eja().random_element()
157 sage: p = x.characteristic_polynomial()
158 sage: x.apply_univariate_polynomial(p).is_zero()
159 True
160
161 The characteristic polynomials of the zero and unit elements
162 should be what we think they are in a subalgebra, too::
163
164 sage: J = HadamardEJA(3)
165 sage: p1 = J.one().characteristic_polynomial()
166 sage: q1 = J.zero().characteristic_polynomial()
167 sage: b0,b1,b2 = J.gens()
168 sage: A = (b0 + 2*b1 + 3*b2).subalgebra_generated_by() # dim 3
169 sage: p2 = A.one().characteristic_polynomial()
170 sage: q2 = A.zero().characteristic_polynomial()
171 sage: p1 == p2
172 True
173 sage: q1 == q2
174 True
175
176 """
177 p = self.parent().characteristic_polynomial_of()
178 return p(*self.to_vector())
179
180
181 def inner_product(self, other):
182 """
183 Return the parent algebra's inner product of myself and ``other``.
184
185 SETUP::
186
187 sage: from mjo.eja.eja_algebra import (
188 ....: ComplexHermitianEJA,
189 ....: JordanSpinEJA,
190 ....: QuaternionHermitianEJA,
191 ....: RealSymmetricEJA,
192 ....: random_eja)
193
194 EXAMPLES:
195
196 The inner product in the Jordan spin algebra is the usual
197 inner product on `R^n` (this example only works because the
198 basis for the Jordan algebra is the standard basis in `R^n`)::
199
200 sage: J = JordanSpinEJA(3)
201 sage: x = vector(QQ,[1,2,3])
202 sage: y = vector(QQ,[4,5,6])
203 sage: x.inner_product(y)
204 32
205 sage: J.from_vector(x).inner_product(J.from_vector(y))
206 32
207
208 The inner product on `S^n` is `<X,Y> = trace(X*Y)`, where
209 multiplication is the usual matrix multiplication in `S^n`,
210 so the inner product of the identity matrix with itself
211 should be the `n`::
212
213 sage: J = RealSymmetricEJA(3)
214 sage: J.one().inner_product(J.one())
215 3
216
217 Likewise, the inner product on `C^n` is `<X,Y> =
218 Re(trace(X*Y))`, where we must necessarily take the real
219 part because the product of Hermitian matrices may not be
220 Hermitian::
221
222 sage: J = ComplexHermitianEJA(3)
223 sage: J.one().inner_product(J.one())
224 3
225
226 Ditto for the quaternions::
227
228 sage: J = QuaternionHermitianEJA(2)
229 sage: J.one().inner_product(J.one())
230 2
231
232 TESTS:
233
234 Ensure that we can always compute an inner product, and that
235 it gives us back a real number::
236
237 sage: J = random_eja()
238 sage: x,y = J.random_elements(2)
239 sage: x.inner_product(y) in RLF
240 True
241
242 """
243 P = self.parent()
244 if not other in P:
245 raise TypeError("'other' must live in the same algebra")
246
247 return P.inner_product(self, other)
248
249
250 def operator_commutes_with(self, other):
251 """
252 Return whether or not this element operator-commutes
253 with ``other``.
254
255 SETUP::
256
257 sage: from mjo.eja.eja_algebra import random_eja
258
259 EXAMPLES:
260
261 The definition of a Jordan algebra says that any element
262 operator-commutes with its square::
263
264 sage: x = random_eja().random_element()
265 sage: x.operator_commutes_with(x^2)
266 True
267
268 TESTS:
269
270 Test Lemma 1 from Chapter III of Koecher::
271
272 sage: u,v = random_eja().random_elements(2)
273 sage: lhs = u.operator_commutes_with(u*v)
274 sage: rhs = v.operator_commutes_with(u^2)
275 sage: lhs == rhs
276 True
277
278 Test the first polarization identity from my notes, Koecher
279 Chapter III, or from Baes (2.3)::
280
281 sage: x,y = random_eja().random_elements(2)
282 sage: Lx = x.operator()
283 sage: Ly = y.operator()
284 sage: Lxx = (x*x).operator()
285 sage: Lxy = (x*y).operator()
286 sage: bool(2*Lx*Lxy + Ly*Lxx == 2*Lxy*Lx + Lxx*Ly)
287 True
288
289 Test the second polarization identity from my notes or from
290 Baes (2.4)::
291
292 sage: x,y,z = random_eja().random_elements(3) # long time
293 sage: Lx = x.operator() # long time
294 sage: Ly = y.operator() # long time
295 sage: Lz = z.operator() # long time
296 sage: Lzy = (z*y).operator() # long time
297 sage: Lxy = (x*y).operator() # long time
298 sage: Lxz = (x*z).operator() # long time
299 sage: lhs = Lx*Lzy + Lz*Lxy + Ly*Lxz # long time
300 sage: rhs = Lzy*Lx + Lxy*Lz + Lxz*Ly # long time
301 sage: bool(lhs == rhs) # long time
302 True
303
304 Test the third polarization identity from my notes or from
305 Baes (2.5)::
306
307 sage: u,y,z = random_eja().random_elements(3) # long time
308 sage: Lu = u.operator() # long time
309 sage: Ly = y.operator() # long time
310 sage: Lz = z.operator() # long time
311 sage: Lzy = (z*y).operator() # long time
312 sage: Luy = (u*y).operator() # long time
313 sage: Luz = (u*z).operator() # long time
314 sage: Luyz = (u*(y*z)).operator() # long time
315 sage: lhs = Lu*Lzy + Lz*Luy + Ly*Luz # long time
316 sage: rhs = Luyz + Ly*Lu*Lz + Lz*Lu*Ly # long time
317 sage: bool(lhs == rhs) # long time
318 True
319
320 """
321 if not other in self.parent():
322 raise TypeError("'other' must live in the same algebra")
323
324 A = self.operator()
325 B = other.operator()
326 return (A*B == B*A)
327
328
329 def det(self):
330 """
331 Return my determinant, the product of my eigenvalues.
332
333 SETUP::
334
335 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
336 ....: TrivialEJA,
337 ....: RealSymmetricEJA,
338 ....: ComplexHermitianEJA,
339 ....: random_eja)
340
341 EXAMPLES::
342
343 sage: J = JordanSpinEJA(2)
344 sage: x = sum( J.gens() )
345 sage: x.det()
346 0
347
348 ::
349
350 sage: J = JordanSpinEJA(3)
351 sage: x = sum( J.gens() )
352 sage: x.det()
353 -1
354
355 The determinant of the sole element in the rank-zero trivial
356 algebra is ``1``, by three paths of reasoning. First, its
357 characteristic polynomial is a constant ``1``, so the constant
358 term in that polynomial is ``1``. Second, the characteristic
359 polynomial evaluated at zero is again ``1``. And finally, the
360 (empty) product of its eigenvalues is likewise just unity::
361
362 sage: J = TrivialEJA()
363 sage: J.zero().det()
364 1
365
366 TESTS:
367
368 An element is invertible if and only if its determinant is
369 non-zero::
370
371 sage: x = random_eja().random_element()
372 sage: x.is_invertible() == (x.det() != 0)
373 True
374
375 Ensure that the determinant is multiplicative on an associative
376 subalgebra as in Faraut and Korányi's Proposition II.2.2::
377
378 sage: J = random_eja().random_element().subalgebra_generated_by()
379 sage: x,y = J.random_elements(2)
380 sage: (x*y).det() == x.det()*y.det()
381 True
382
383 The determinant in real matrix algebras is the usual determinant::
384
385 sage: X = matrix.random(QQ,3)
386 sage: X = X + X.T
387 sage: J1 = RealSymmetricEJA(3)
388 sage: J2 = RealSymmetricEJA(3,field=QQ,orthonormalize=False)
389 sage: expected = X.det()
390 sage: actual1 = J1(X).det()
391 sage: actual2 = J2(X).det()
392 sage: actual1 == expected
393 True
394 sage: actual2 == expected
395 True
396
397 """
398 P = self.parent()
399 r = P.rank()
400
401 if r == 0:
402 # Special case, since we don't get the a0=1
403 # coefficient when the rank of the algebra
404 # is zero.
405 return P.base_ring().one()
406
407 p = P._charpoly_coefficients()[0]
408 # The _charpoly_coeff function already adds the factor of -1
409 # to ensure that _charpoly_coefficients()[0] is really what
410 # appears in front of t^{0} in the charpoly. However, we want
411 # (-1)^r times THAT for the determinant.
412 return ((-1)**r)*p(*self.to_vector())
413
414
415 @cached_method
416 def inverse(self):
417 """
418 Return the Jordan-multiplicative inverse of this element.
419
420 ALGORITHM:
421
422 In general we appeal to the quadratic representation as in
423 Koecher's Theorem 12 in Chapter III, Section 5. But if the
424 parent algebra's "characteristic polynomial of" coefficients
425 happen to be cached, then we use Proposition II.2.4 in Faraut
426 and Korányi which gives a formula for the inverse based on the
427 characteristic polynomial and the Cayley-Hamilton theorem for
428 Euclidean Jordan algebras::
429
430 SETUP::
431
432 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
433 ....: JordanSpinEJA,
434 ....: random_eja)
435
436 EXAMPLES:
437
438 The inverse in the spin factor algebra is given in Alizadeh's
439 Example 11.11::
440
441 sage: J = JordanSpinEJA.random_instance()
442 sage: x = J.random_element()
443 sage: while not x.is_invertible():
444 ....: x = J.random_element()
445 sage: x_vec = x.to_vector()
446 sage: x0 = x_vec[:1]
447 sage: x_bar = x_vec[1:]
448 sage: coeff = x0.inner_product(x0) - x_bar.inner_product(x_bar)
449 sage: x_inverse = x_vec.parent()(x0.list() + (-x_bar).list())
450 sage: if not coeff.is_zero(): x_inverse = x_inverse/coeff
451 sage: x.inverse() == J.from_vector(x_inverse)
452 True
453
454 Trying to invert a non-invertible element throws an error:
455
456 sage: JordanSpinEJA(3).zero().inverse()
457 Traceback (most recent call last):
458 ...
459 ZeroDivisionError: element is not invertible
460
461 TESTS:
462
463 The identity element is its own inverse::
464
465 sage: J = random_eja()
466 sage: J.one().inverse() == J.one()
467 True
468
469 If an element has an inverse, it acts like one::
470
471 sage: J = random_eja()
472 sage: x = J.random_element()
473 sage: (not x.is_invertible()) or (x.inverse()*x == J.one())
474 True
475
476 The inverse of the inverse is what we started with::
477
478 sage: J = random_eja()
479 sage: x = J.random_element()
480 sage: (not x.is_invertible()) or (x.inverse().inverse() == x)
481 True
482
483 Proposition II.2.3 in Faraut and Korányi says that the inverse
484 of an element is the inverse of its left-multiplication operator
485 applied to the algebra's identity, when that inverse exists::
486
487 sage: J = random_eja() # long time
488 sage: x = J.random_element() # long time
489 sage: (not x.operator().is_invertible()) or ( # long time
490 ....: x.operator().inverse()(J.one()) # long time
491 ....: == # long time
492 ....: x.inverse() ) # long time
493 True
494
495 Check that the fast (cached) and slow algorithms give the same
496 answer::
497
498 sage: J = random_eja(field=QQ, orthonormalize=False) # long time
499 sage: x = J.random_element() # long time
500 sage: while not x.is_invertible(): # long time
501 ....: x = J.random_element() # long time
502 sage: slow = x.inverse() # long time
503 sage: _ = J._charpoly_coefficients() # long time
504 sage: fast = x.inverse() # long time
505 sage: slow == fast # long time
506 True
507 """
508 not_invertible_msg = "element is not invertible"
509
510 algebra = self.parent()
511 if algebra._charpoly_coefficients.is_in_cache():
512 # We can invert using our charpoly if it will be fast to
513 # compute. If the coefficients are cached, our rank had
514 # better be too!
515 if self.det().is_zero():
516 raise ZeroDivisionError(not_invertible_msg)
517 r = algebra.rank()
518 a = self.characteristic_polynomial().coefficients(sparse=False)
519 return (-1)**(r+1)*algebra.sum(a[i+1]*self**i
520 for i in range(r))/self.det()
521
522 try:
523 inv = (~self.quadratic_representation())(self)
524 self.is_invertible.set_cache(True)
525 return inv
526 except ZeroDivisionError:
527 self.is_invertible.set_cache(False)
528 raise ZeroDivisionError(not_invertible_msg)
529
530
531 @cached_method
532 def is_invertible(self):
533 """
534 Return whether or not this element is invertible.
535
536 ALGORITHM:
537
538 If computing my determinant will be fast, we do so and compare
539 with zero (Proposition II.2.4 in Faraut and
540 Koranyi). Otherwise, Proposition II.3.2 in Faraut and Koranyi
541 reduces the problem to the invertibility of my quadratic
542 representation.
543
544 SETUP::
545
546 sage: from mjo.eja.eja_algebra import random_eja
547
548 TESTS:
549
550 The identity element is always invertible::
551
552 sage: J = random_eja()
553 sage: J.one().is_invertible()
554 True
555
556 The zero element is never invertible in a non-trivial algebra::
557
558 sage: J = random_eja()
559 sage: (not J.is_trivial()) and J.zero().is_invertible()
560 False
561
562 Test that the fast (cached) and slow algorithms give the same
563 answer::
564
565 sage: J = random_eja(field=QQ, orthonormalize=False) # long time
566 sage: x = J.random_element() # long time
567 sage: slow = x.is_invertible() # long time
568 sage: _ = J._charpoly_coefficients() # long time
569 sage: fast = x.is_invertible() # long time
570 sage: slow == fast # long time
571 True
572 """
573 if self.is_zero():
574 if self.parent().is_trivial():
575 return True
576 else:
577 return False
578
579 if self.parent()._charpoly_coefficients.is_in_cache():
580 # The determinant will be quicker than inverting the
581 # quadratic representation, most likely.
582 return (not self.det().is_zero())
583
584 # The easiest way to determine if I'm invertible is to try.
585 try:
586 inv = (~self.quadratic_representation())(self)
587 self.inverse.set_cache(inv)
588 return True
589 except ZeroDivisionError:
590 return False
591
592
593 def is_primitive_idempotent(self):
594 """
595 Return whether or not this element is a primitive (or minimal)
596 idempotent.
597
598 A primitive idempotent is a non-zero idempotent that is not
599 the sum of two other non-zero idempotents. Remark 2.7.15 in
600 Baes shows that this is what he refers to as a "minimal
601 idempotent."
602
603 An element of a Euclidean Jordan algebra is a minimal idempotent
604 if it :meth:`is_idempotent` and if its Peirce subalgebra
605 corresponding to the eigenvalue ``1`` has dimension ``1`` (Baes,
606 Proposition 2.7.17).
607
608 SETUP::
609
610 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
611 ....: RealSymmetricEJA,
612 ....: TrivialEJA,
613 ....: random_eja)
614
615 WARNING::
616
617 This method is sloooooow.
618
619 EXAMPLES:
620
621 The spectral decomposition of a non-regular element should always
622 contain at least one non-minimal idempotent::
623
624 sage: J = RealSymmetricEJA(3)
625 sage: x = sum(J.gens())
626 sage: x.is_regular()
627 False
628 sage: [ c.is_primitive_idempotent()
629 ....: for (l,c) in x.spectral_decomposition() ]
630 [False, True]
631
632 On the other hand, the spectral decomposition of a regular
633 element should always be in terms of minimal idempotents::
634
635 sage: J = JordanSpinEJA(4)
636 sage: x = sum( i*J.monomial(i) for i in range(len(J.gens())) )
637 sage: x.is_regular()
638 True
639 sage: [ c.is_primitive_idempotent()
640 ....: for (l,c) in x.spectral_decomposition() ]
641 [True, True]
642
643 TESTS:
644
645 The identity element is minimal only in an EJA of rank one::
646
647 sage: J = random_eja()
648 sage: J.rank() == 1 or not J.one().is_primitive_idempotent()
649 True
650
651 A non-idempotent cannot be a minimal idempotent::
652
653 sage: J = JordanSpinEJA(4)
654 sage: x = J.random_element()
655 sage: (not x.is_idempotent()) and x.is_primitive_idempotent()
656 False
657
658 Proposition 2.7.19 in Baes says that an element is a minimal
659 idempotent if and only if it's idempotent with trace equal to
660 unity::
661
662 sage: J = JordanSpinEJA(4)
663 sage: x = J.random_element()
664 sage: expected = (x.is_idempotent() and x.trace() == 1)
665 sage: actual = x.is_primitive_idempotent()
666 sage: actual == expected
667 True
668
669 Primitive idempotents must be non-zero::
670
671 sage: J = random_eja()
672 sage: J.zero().is_idempotent()
673 True
674 sage: J.zero().is_primitive_idempotent()
675 False
676
677 As a consequence of the fact that primitive idempotents must
678 be non-zero, there are no primitive idempotents in a trivial
679 Euclidean Jordan algebra::
680
681 sage: J = TrivialEJA()
682 sage: J.one().is_idempotent()
683 True
684 sage: J.one().is_primitive_idempotent()
685 False
686
687 """
688 if not self.is_idempotent():
689 return False
690
691 if self.is_zero():
692 return False
693
694 (_,_,J1) = self.parent().peirce_decomposition(self)
695 return (J1.dimension() == 1)
696
697
698 def is_nilpotent(self):
699 """
700 Return whether or not some power of this element is zero.
701
702 ALGORITHM:
703
704 We use Theorem 5 in Chapter III of Koecher, which says that
705 an element ``x`` is nilpotent if and only if ``x.operator()``
706 is nilpotent. And it is a basic fact of linear algebra that
707 an operator on an `n`-dimensional space is nilpotent if and
708 only if, when raised to the `n`th power, it equals the zero
709 operator (for example, see Axler Corollary 8.8).
710
711 SETUP::
712
713 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
714 ....: random_eja)
715
716 EXAMPLES::
717
718 sage: J = JordanSpinEJA(3)
719 sage: x = sum(J.gens())
720 sage: x.is_nilpotent()
721 False
722
723 TESTS:
724
725 The identity element is never nilpotent, except in a trivial EJA::
726
727 sage: J = random_eja()
728 sage: J.one().is_nilpotent() and not J.is_trivial()
729 False
730
731 The additive identity is always nilpotent::
732
733 sage: random_eja().zero().is_nilpotent()
734 True
735
736 """
737 P = self.parent()
738 zero_operator = P.zero().operator()
739 return self.operator()**P.dimension() == zero_operator
740
741
742 def is_regular(self):
743 """
744 Return whether or not this is a regular element.
745
746 SETUP::
747
748 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
749 ....: random_eja)
750
751 EXAMPLES:
752
753 The identity element always has degree one, but any element
754 linearly-independent from it is regular::
755
756 sage: J = JordanSpinEJA(5)
757 sage: J.one().is_regular()
758 False
759 sage: b0, b1, b2, b3, b4 = J.gens()
760 sage: b0 == J.one()
761 True
762 sage: for x in J.gens():
763 ....: (J.one() + x).is_regular()
764 False
765 True
766 True
767 True
768 True
769
770 TESTS:
771
772 The zero element should never be regular, unless the parent
773 algebra has dimension less than or equal to one::
774
775 sage: J = random_eja()
776 sage: J.dimension() <= 1 or not J.zero().is_regular()
777 True
778
779 The unit element isn't regular unless the algebra happens to
780 consist of only its scalar multiples::
781
782 sage: J = random_eja()
783 sage: J.dimension() <= 1 or not J.one().is_regular()
784 True
785
786 """
787 return self.degree() == self.parent().rank()
788
789
790 def degree(self):
791 """
792 Return the degree of this element, which is defined to be
793 the degree of its minimal polynomial.
794
795 ALGORITHM:
796
797 .........
798
799 SETUP::
800
801 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
802 ....: random_eja)
803
804 EXAMPLES::
805
806 sage: J = JordanSpinEJA(4)
807 sage: J.one().degree()
808 1
809 sage: b0,b1,b2,b3 = J.gens()
810 sage: (b0 - b1).degree()
811 2
812
813 In the spin factor algebra (of rank two), all elements that
814 aren't multiples of the identity are regular::
815
816 sage: J = JordanSpinEJA.random_instance()
817 sage: n = J.dimension()
818 sage: x = J.random_element()
819 sage: x.degree() == min(n,2) or (x == x.coefficient(0)*J.one())
820 True
821
822 TESTS:
823
824 The zero and unit elements are both of degree one in nontrivial
825 algebras::
826
827 sage: J = random_eja()
828 sage: d = J.zero().degree()
829 sage: (J.is_trivial() and d == 0) or d == 1
830 True
831 sage: d = J.one().degree()
832 sage: (J.is_trivial() and d == 0) or d == 1
833 True
834
835 Our implementation agrees with the definition::
836
837 sage: x = random_eja().random_element()
838 sage: x.degree() == x.minimal_polynomial().degree()
839 True
840
841 """
842 n = self.parent().dimension()
843
844 if n == 0:
845 # The minimal polynomial is an empty product, i.e. the
846 # constant polynomial "1" having degree zero.
847 return 0
848 elif self.is_zero():
849 # The minimal polynomial of zero in a nontrivial algebra
850 # is "t", and is of degree one.
851 return 1
852 elif n == 1:
853 # If this is a nonzero element of a nontrivial algebra, it
854 # has degree at least one. It follows that, in an algebra
855 # of dimension one, the degree must be actually one.
856 return 1
857
858 # BEWARE: The subalgebra_generated_by() method uses the result
859 # of this method to construct a basis for the subalgebra. That
860 # means, in particular, that we cannot implement this method
861 # as ``self.subalgebra_generated_by().dimension()``.
862
863 # Algorithm: keep appending (vector representations of) powers
864 # self as rows to a matrix and echelonizing it. When its rank
865 # stops increasing, we've reached a redundancy.
866
867 # Given the special cases above, we can assume that "self" is
868 # nonzero, the algebra is nontrivial, and that its dimension
869 # is at least two.
870 M = matrix([(self.parent().one()).to_vector()])
871 old_rank = 1
872
873 # Specifying the row-reduction algorithm can e.g. help over
874 # AA because it avoids the RecursionError that gets thrown
875 # when we have to look too hard for a root.
876 #
877 # Beware: QQ supports an entirely different set of "algorithm"
878 # keywords than do AA and RR.
879 algo = None
880 from sage.rings.all import QQ
881 if self.parent().base_ring() is not QQ:
882 algo = "scaled_partial_pivoting"
883
884 for d in range(1,n):
885 M = matrix(M.rows() + [(self**d).to_vector()])
886 M.echelonize(algo)
887 new_rank = M.rank()
888 if new_rank == old_rank:
889 return new_rank
890 else:
891 old_rank = new_rank
892
893 return n
894
895
896
897 def left_matrix(self):
898 """
899 Our parent class defines ``left_matrix`` and ``matrix``
900 methods whose names are misleading. We don't want them.
901 """
902 raise NotImplementedError("use operator().matrix() instead")
903
904 matrix = left_matrix
905
906
907 def minimal_polynomial(self):
908 """
909 Return the minimal polynomial of this element,
910 as a function of the variable `t`.
911
912 ALGORITHM:
913
914 We restrict ourselves to the associative subalgebra
915 generated by this element, and then return the minimal
916 polynomial of this element's operator matrix (in that
917 subalgebra). This works by Baes Proposition 2.3.16.
918
919 SETUP::
920
921 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
922 ....: RealSymmetricEJA,
923 ....: TrivialEJA,
924 ....: random_eja)
925
926 EXAMPLES:
927
928 Keeping in mind that the polynomial ``1`` evaluates the identity
929 element (also the zero element) of the trivial algebra, it is clear
930 that the polynomial ``1`` is the minimal polynomial of the only
931 element in a trivial algebra::
932
933 sage: J = TrivialEJA()
934 sage: J.one().minimal_polynomial()
935 1
936 sage: J.zero().minimal_polynomial()
937 1
938
939 TESTS:
940
941 The minimal polynomial of the identity and zero elements are
942 always the same, except in trivial algebras where the minimal
943 polynomial of the unit/zero element is ``1``::
944
945 sage: J = random_eja()
946 sage: mu = J.one().minimal_polynomial()
947 sage: t = mu.parent().gen()
948 sage: mu + int(J.is_trivial())*(t-2)
949 t - 1
950 sage: mu = J.zero().minimal_polynomial()
951 sage: t = mu.parent().gen()
952 sage: mu + int(J.is_trivial())*(t-1)
953 t
954
955 The degree of an element is (by one definition) the degree
956 of its minimal polynomial::
957
958 sage: x = random_eja().random_element()
959 sage: x.degree() == x.minimal_polynomial().degree()
960 True
961
962 The minimal polynomial and the characteristic polynomial coincide
963 and are known (see Alizadeh, Example 11.11) for all elements of
964 the spin factor algebra that aren't scalar multiples of the
965 identity. We require the dimension of the algebra to be at least
966 two here so that said elements actually exist::
967
968 sage: d_max = JordanSpinEJA._max_random_instance_dimension()
969 sage: n = ZZ.random_element(2, max(2,d_max))
970 sage: J = JordanSpinEJA(n)
971 sage: y = J.random_element()
972 sage: while y == y.coefficient(0)*J.one():
973 ....: y = J.random_element()
974 sage: y0 = y.to_vector()[0]
975 sage: y_bar = y.to_vector()[1:]
976 sage: actual = y.minimal_polynomial()
977 sage: t = PolynomialRing(J.base_ring(),'t').gen(0)
978 sage: expected = t^2 - 2*y0*t + (y0^2 - norm(y_bar)^2)
979 sage: bool(actual == expected)
980 True
981
982 The minimal polynomial should always kill its element::
983
984 sage: x = random_eja().random_element() # long time
985 sage: p = x.minimal_polynomial() # long time
986 sage: x.apply_univariate_polynomial(p) # long time
987 0
988
989 The minimal polynomial is invariant under a change of basis,
990 and in particular, a re-scaling of the basis::
991
992 sage: d_max = RealSymmetricEJA._max_random_instance_dimension()
993 sage: d = ZZ.random_element(1, d_max)
994 sage: n = RealSymmetricEJA._max_random_instance_size(d)
995 sage: J1 = RealSymmetricEJA(n)
996 sage: J2 = RealSymmetricEJA(n,orthonormalize=False)
997 sage: X = random_matrix(AA,n)
998 sage: X = X*X.transpose()
999 sage: x1 = J1(X)
1000 sage: x2 = J2(X)
1001 sage: x1.minimal_polynomial() == x2.minimal_polynomial()
1002 True
1003
1004 """
1005 if self.is_zero():
1006 # Pretty sure we know what the minimal polynomial of
1007 # the zero operator is going to be. This ensures
1008 # consistency of e.g. the polynomial variable returned
1009 # in the "normal" case without us having to think about it.
1010 return self.operator().minimal_polynomial()
1011
1012 # If we don't orthonormalize the subalgebra's basis, then the
1013 # first two monomials in the subalgebra will be self^0 and
1014 # self^1... assuming that self^1 is not a scalar multiple of
1015 # self^0 (the unit element). We special case these to avoid
1016 # having to solve a system to coerce self into the subalgebra.
1017 A = self.subalgebra_generated_by(orthonormalize=False)
1018
1019 if A.dimension() == 1:
1020 # Does a solve to find the scalar multiple alpha such that
1021 # alpha*unit = self. We have to do this because the basis
1022 # for the subalgebra will be [ self^0 ], and not [ self^1 ]!
1023 unit = self.parent().one()
1024 alpha = self.to_vector() / unit.to_vector()
1025 return (unit.operator()*alpha).minimal_polynomial()
1026 else:
1027 # If the dimension of the subalgebra is >= 2, then we just
1028 # use the second basis element.
1029 return A.monomial(1).operator().minimal_polynomial()
1030
1031
1032
1033 def to_matrix(self):
1034 """
1035 Return an (often more natural) representation of this element as a
1036 matrix.
1037
1038 Every finite-dimensional Euclidean Jordan Algebra is a direct
1039 sum of five simple algebras, four of which comprise Hermitian
1040 matrices. This method returns a "natural" matrix
1041 representation of this element as either a Hermitian matrix or
1042 column vector.
1043
1044 SETUP::
1045
1046 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
1047 ....: HadamardEJA,
1048 ....: QuaternionHermitianEJA,
1049 ....: RealSymmetricEJA)
1050
1051 EXAMPLES::
1052
1053 sage: J = ComplexHermitianEJA(3)
1054 sage: J.one()
1055 b0 + b3 + b8
1056 sage: J.one().to_matrix()
1057 +---+---+---+
1058 | 1 | 0 | 0 |
1059 +---+---+---+
1060 | 0 | 1 | 0 |
1061 +---+---+---+
1062 | 0 | 0 | 1 |
1063 +---+---+---+
1064
1065 ::
1066
1067 sage: J = QuaternionHermitianEJA(2)
1068 sage: J.one()
1069 b0 + b5
1070 sage: J.one().to_matrix()
1071 +---+---+
1072 | 1 | 0 |
1073 +---+---+
1074 | 0 | 1 |
1075 +---+---+
1076
1077 This also works in Cartesian product algebras::
1078
1079 sage: J1 = HadamardEJA(1)
1080 sage: J2 = RealSymmetricEJA(2)
1081 sage: J = cartesian_product([J1,J2])
1082 sage: x = sum(J.gens())
1083 sage: x.to_matrix()[0]
1084 [1]
1085 sage: x.to_matrix()[1]
1086 [ 1 0.7071067811865475?]
1087 [0.7071067811865475? 1]
1088
1089 """
1090 B = self.parent().matrix_basis()
1091 W = self.parent().matrix_space()
1092
1093 # This is just a manual "from_vector()", but of course
1094 # matrix spaces aren't vector spaces in sage, so they
1095 # don't have a from_vector() method.
1096 return W.linear_combination( zip(B, self.to_vector()) )
1097
1098
1099
1100 def norm(self):
1101 """
1102 The norm of this element with respect to :meth:`inner_product`.
1103
1104 SETUP::
1105
1106 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
1107 ....: HadamardEJA)
1108
1109 EXAMPLES::
1110
1111 sage: J = HadamardEJA(2)
1112 sage: x = sum(J.gens())
1113 sage: x.norm()
1114 1.414213562373095?
1115
1116 ::
1117
1118 sage: J = JordanSpinEJA(4)
1119 sage: x = sum(J.gens())
1120 sage: x.norm()
1121 2
1122
1123 """
1124 return self.inner_product(self).sqrt()
1125
1126
1127 def operator(self):
1128 """
1129 Return the left-multiplication-by-this-element
1130 operator on the ambient algebra.
1131
1132 SETUP::
1133
1134 sage: from mjo.eja.eja_algebra import random_eja
1135
1136 TESTS::
1137
1138 sage: J = random_eja()
1139 sage: x,y = J.random_elements(2)
1140 sage: x.operator()(y) == x*y
1141 True
1142 sage: y.operator()(x) == x*y
1143 True
1144
1145 """
1146 P = self.parent()
1147 left_mult_by_self = lambda y: self*y
1148 L = P.module_morphism(function=left_mult_by_self, codomain=P)
1149 return FiniteDimensionalEJAOperator(P, P, L.matrix() )
1150
1151
1152 def quadratic_representation(self, other=None):
1153 """
1154 Return the quadratic representation of this element.
1155
1156 SETUP::
1157
1158 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
1159 ....: random_eja)
1160
1161 EXAMPLES:
1162
1163 The explicit form in the spin factor algebra is given by
1164 Alizadeh's Example 11.12::
1165
1166 sage: x = JordanSpinEJA.random_instance().random_element()
1167 sage: x_vec = x.to_vector()
1168 sage: Q = matrix.identity(x.base_ring(), 0)
1169 sage: n = x_vec.degree()
1170 sage: if n > 0:
1171 ....: x0 = x_vec[0]
1172 ....: x_bar = x_vec[1:]
1173 ....: A = matrix(x.base_ring(), 1, [x_vec.inner_product(x_vec)])
1174 ....: B = 2*x0*x_bar.row()
1175 ....: C = 2*x0*x_bar.column()
1176 ....: D = matrix.identity(x.base_ring(), n-1)
1177 ....: D = (x0^2 - x_bar.inner_product(x_bar))*D
1178 ....: D = D + 2*x_bar.tensor_product(x_bar)
1179 ....: Q = matrix.block(2,2,[A,B,C,D])
1180 sage: Q == x.quadratic_representation().matrix()
1181 True
1182
1183 Test all of the properties from Theorem 11.2 in Alizadeh::
1184
1185 sage: J = random_eja()
1186 sage: x,y = J.random_elements(2)
1187 sage: Lx = x.operator()
1188 sage: Lxx = (x*x).operator()
1189 sage: Qx = x.quadratic_representation()
1190 sage: Qy = y.quadratic_representation()
1191 sage: Qxy = x.quadratic_representation(y)
1192 sage: Qex = J.one().quadratic_representation(x)
1193 sage: n = ZZ.random_element(10)
1194 sage: Qxn = (x^n).quadratic_representation()
1195
1196 Property 1:
1197
1198 sage: 2*Qxy == (x+y).quadratic_representation() - Qx - Qy
1199 True
1200
1201 Property 2 (multiply on the right for :trac:`28272`):
1202
1203 sage: alpha = J.base_ring().random_element()
1204 sage: (alpha*x).quadratic_representation() == Qx*(alpha^2)
1205 True
1206
1207 Property 3:
1208
1209 sage: not x.is_invertible() or ( Qx(x.inverse()) == x )
1210 True
1211
1212 sage: not x.is_invertible() or (
1213 ....: ~Qx
1214 ....: ==
1215 ....: x.inverse().quadratic_representation() )
1216 True
1217
1218 sage: Qxy(J.one()) == x*y
1219 True
1220
1221 Property 4:
1222
1223 sage: not x.is_invertible() or (
1224 ....: x.quadratic_representation(x.inverse())*Qx
1225 ....: == Qx*x.quadratic_representation(x.inverse()) )
1226 True
1227
1228 sage: not x.is_invertible() or (
1229 ....: x.quadratic_representation(x.inverse())*Qx
1230 ....: ==
1231 ....: 2*Lx*Qex - Qx )
1232 True
1233
1234 sage: 2*Lx*Qex - Qx == Lxx
1235 True
1236
1237 Property 5:
1238
1239 sage: Qy(x).quadratic_representation() == Qy*Qx*Qy
1240 True
1241
1242 Property 6:
1243
1244 sage: Qxn == (Qx)^n
1245 True
1246
1247 Property 7:
1248
1249 sage: not x.is_invertible() or (
1250 ....: Qx*x.inverse().operator() == Lx )
1251 True
1252
1253 Property 8:
1254
1255 sage: not x.operator_commutes_with(y) or (
1256 ....: Qx(y)^n == Qxn(y^n) )
1257 True
1258
1259 """
1260 if other is None:
1261 other=self
1262 elif not other in self.parent():
1263 raise TypeError("'other' must live in the same algebra")
1264
1265 L = self.operator()
1266 M = other.operator()
1267 return ( L*M + M*L - (self*other).operator() )
1268
1269
1270
1271 def spectral_decomposition(self):
1272 """
1273 Return the unique spectral decomposition of this element.
1274
1275 ALGORITHM:
1276
1277 Following Faraut and Korányi's Theorem III.1.1, we restrict this
1278 element's left-multiplication-by operator to the subalgebra it
1279 generates. We then compute the spectral decomposition of that
1280 operator, and the spectral projectors we get back must be the
1281 left-multiplication-by operators for the idempotents we
1282 seek. Thus applying them to the identity element gives us those
1283 idempotents.
1284
1285 Since the eigenvalues are required to be distinct, we take
1286 the spectral decomposition of the zero element to be zero
1287 times the identity element of the algebra (which is idempotent,
1288 obviously).
1289
1290 SETUP::
1291
1292 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1293
1294 EXAMPLES:
1295
1296 The spectral decomposition of the identity is ``1`` times itself,
1297 and the spectral decomposition of zero is ``0`` times the identity::
1298
1299 sage: J = RealSymmetricEJA(3)
1300 sage: J.one()
1301 b0 + b2 + b5
1302 sage: J.one().spectral_decomposition()
1303 [(1, b0 + b2 + b5)]
1304 sage: J.zero().spectral_decomposition()
1305 [(0, b0 + b2 + b5)]
1306
1307 TESTS::
1308
1309 sage: J = RealSymmetricEJA(4)
1310 sage: x = sum(J.gens())
1311 sage: sd = x.spectral_decomposition()
1312 sage: l0 = sd[0][0]
1313 sage: l1 = sd[1][0]
1314 sage: c0 = sd[0][1]
1315 sage: c1 = sd[1][1]
1316 sage: c0.inner_product(c1) == 0
1317 True
1318 sage: c0.is_idempotent()
1319 True
1320 sage: c1.is_idempotent()
1321 True
1322 sage: c0 + c1 == J.one()
1323 True
1324 sage: l0*c0 + l1*c1 == x
1325 True
1326
1327 The spectral decomposition should work in subalgebras, too::
1328
1329 sage: J = RealSymmetricEJA(4)
1330 sage: (b0, b1, b2, b3, b4, b5, b6, b7, b8, b9) = J.gens()
1331 sage: A = 2*b5 - 2*b8
1332 sage: (lambda1, c1) = A.spectral_decomposition()[1]
1333 sage: (J0, J5, J1) = J.peirce_decomposition(c1)
1334 sage: (f0, f1, f2) = J1.gens()
1335 sage: f0.spectral_decomposition()
1336 [(0, 1.000000000000000?*c2), (1, 1.000000000000000?*c0)]
1337
1338 """
1339 A = self.subalgebra_generated_by(orthonormalize=True)
1340 result = []
1341 for (evalue, proj) in A(self).operator().spectral_decomposition():
1342 result.append( (evalue, proj(A.one()).superalgebra_element()) )
1343 return result
1344
1345 def subalgebra_generated_by(self, **kwargs):
1346 """
1347 Return the associative subalgebra of the parent EJA generated
1348 by this element.
1349
1350 Since our parent algebra is unital, we want "subalgebra" to mean
1351 "unital subalgebra" as well; thus the subalgebra that an element
1352 generates will itself be a Euclidean Jordan algebra after
1353 restricting the algebra operations appropriately. This is the
1354 subalgebra that Faraut and Korányi work with in section II.2, for
1355 example.
1356
1357 SETUP::
1358
1359 sage: from mjo.eja.eja_algebra import (random_eja,
1360 ....: HadamardEJA,
1361 ....: RealSymmetricEJA)
1362
1363 EXAMPLES:
1364
1365 We can create subalgebras of Cartesian product EJAs that are not
1366 themselves Cartesian product EJAs (they're just "regular" EJAs)::
1367
1368 sage: J1 = HadamardEJA(3)
1369 sage: J2 = RealSymmetricEJA(2)
1370 sage: J = cartesian_product([J1,J2])
1371 sage: J.one().subalgebra_generated_by()
1372 Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
1373
1374 TESTS:
1375
1376 This subalgebra, being composed of only powers, is associative::
1377
1378 sage: x0 = random_eja().random_element()
1379 sage: A = x0.subalgebra_generated_by()
1380 sage: x,y,z = A.random_elements(3)
1381 sage: (x*y)*z == x*(y*z)
1382 True
1383
1384 Squaring in the subalgebra should work the same as in
1385 the superalgebra::
1386
1387 sage: x = random_eja().random_element()
1388 sage: A = x.subalgebra_generated_by()
1389 sage: A(x^2) == A(x)*A(x)
1390 True
1391
1392 By definition, the subalgebra generated by the zero element is
1393 the one-dimensional algebra generated by the identity
1394 element... unless the original algebra was trivial, in which
1395 case the subalgebra is trivial too::
1396
1397 sage: A = random_eja().zero().subalgebra_generated_by()
1398 sage: (A.is_trivial() and A.dimension() == 0) or A.dimension() == 1
1399 True
1400
1401 """
1402 powers = tuple( self**k for k in range(self.degree()) )
1403 A = self.parent().subalgebra(powers,
1404 associative=True,
1405 check_field=False,
1406 check_axioms=False,
1407 **kwargs)
1408 A.one.set_cache(A(self.parent().one()))
1409 return A
1410
1411
1412 def subalgebra_idempotent(self):
1413 """
1414 Find an idempotent in the associative subalgebra I generate
1415 using Proposition 2.3.5 in Baes.
1416
1417 SETUP::
1418
1419 sage: from mjo.eja.eja_algebra import random_eja
1420
1421 TESTS:
1422
1423 Ensure that we can find an idempotent in a non-trivial algebra
1424 where there are non-nilpotent elements, or that we get the dumb
1425 solution in the trivial algebra::
1426
1427 sage: J = random_eja()
1428 sage: x = J.random_element()
1429 sage: while x.is_nilpotent() and not J.is_trivial():
1430 ....: x = J.random_element()
1431 sage: c = x.subalgebra_idempotent()
1432 sage: c^2 == c
1433 True
1434
1435 """
1436 if self.parent().is_trivial():
1437 return self
1438
1439 if self.is_nilpotent():
1440 raise ValueError("this only works with non-nilpotent elements!")
1441
1442 J = self.subalgebra_generated_by()
1443 u = J(self)
1444
1445 # The image of the matrix of left-u^m-multiplication
1446 # will be minimal for some natural number s...
1447 s = 0
1448 minimal_dim = J.dimension()
1449 for i in range(1, minimal_dim):
1450 this_dim = (u**i).operator().matrix().image().dimension()
1451 if this_dim < minimal_dim:
1452 minimal_dim = this_dim
1453 s = i
1454
1455 # Now minimal_matrix should correspond to the smallest
1456 # non-zero subspace in Baes's (or really, Koecher's)
1457 # proposition.
1458 #
1459 # However, we need to restrict the matrix to work on the
1460 # subspace... or do we? Can't we just solve, knowing that
1461 # A(c) = u^(s+1) should have a solution in the big space,
1462 # too?
1463 #
1464 # Beware, solve_right() means that we're using COLUMN vectors.
1465 # Our FiniteDimensionalAlgebraElement superclass uses rows.
1466 u_next = u**(s+1)
1467 A = u_next.operator().matrix()
1468 c = J.from_vector(A.solve_right(u_next.to_vector()))
1469
1470 # Now c is the idempotent we want, but it still lives in the subalgebra.
1471 return c.superalgebra_element()
1472
1473
1474 def trace(self):
1475 """
1476 Return my trace, the sum of my eigenvalues.
1477
1478 In a trivial algebra, however you want to look at it, the trace is
1479 an empty sum for which we declare the result to be zero.
1480
1481 SETUP::
1482
1483 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
1484 ....: HadamardEJA,
1485 ....: TrivialEJA,
1486 ....: random_eja)
1487
1488 EXAMPLES::
1489
1490 sage: J = TrivialEJA()
1491 sage: J.zero().trace()
1492 0
1493
1494 ::
1495 sage: J = JordanSpinEJA(3)
1496 sage: x = sum(J.gens())
1497 sage: x.trace()
1498 2
1499
1500 ::
1501
1502 sage: J = HadamardEJA(5)
1503 sage: J.one().trace()
1504 5
1505
1506 TESTS:
1507
1508 The trace of an element is a real number::
1509
1510 sage: J = random_eja()
1511 sage: J.random_element().trace() in RLF
1512 True
1513
1514 The trace is linear::
1515
1516 sage: J = random_eja()
1517 sage: x,y = J.random_elements(2)
1518 sage: alpha = J.base_ring().random_element()
1519 sage: (alpha*x + y).trace() == alpha*x.trace() + y.trace()
1520 True
1521
1522 The trace of a square is nonnegative::
1523
1524 sage: x = random_eja().random_element()
1525 sage: (x*x).trace() >= 0
1526 True
1527
1528 """
1529 P = self.parent()
1530 r = P.rank()
1531
1532 if r == 0:
1533 # Special case for the trivial algebra where
1534 # the trace is an empty sum.
1535 return P.base_ring().zero()
1536
1537 p = P._charpoly_coefficients()[r-1]
1538 # The _charpoly_coeff function already adds the factor of
1539 # -1 to ensure that _charpoly_coeff(r-1) is really what
1540 # appears in front of t^{r-1} in the charpoly. However,
1541 # we want the negative of THAT for the trace.
1542 return -p(*self.to_vector())
1543
1544
1545 def trace_inner_product(self, other):
1546 """
1547 Return the trace inner product of myself and ``other``.
1548
1549 SETUP::
1550
1551 sage: from mjo.eja.eja_algebra import random_eja
1552
1553 TESTS:
1554
1555 The trace inner product is commutative, bilinear, and associative::
1556
1557 sage: J = random_eja()
1558 sage: x,y,z = J.random_elements(3)
1559 sage: # commutative
1560 sage: x.trace_inner_product(y) == y.trace_inner_product(x)
1561 True
1562 sage: # bilinear
1563 sage: a = J.base_ring().random_element();
1564 sage: actual = (a*(x+z)).trace_inner_product(y)
1565 sage: expected = ( a*x.trace_inner_product(y) +
1566 ....: a*z.trace_inner_product(y) )
1567 sage: actual == expected
1568 True
1569 sage: actual = x.trace_inner_product(a*(y+z))
1570 sage: expected = ( a*x.trace_inner_product(y) +
1571 ....: a*x.trace_inner_product(z) )
1572 sage: actual == expected
1573 True
1574 sage: # associative
1575 sage: (x*y).trace_inner_product(z) == y.trace_inner_product(x*z)
1576 True
1577
1578 """
1579 if not other in self.parent():
1580 raise TypeError("'other' must live in the same algebra")
1581
1582 return (self*other).trace()
1583
1584
1585 def trace_norm(self):
1586 """
1587 The norm of this element with respect to :meth:`trace_inner_product`.
1588
1589 SETUP::
1590
1591 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
1592 ....: HadamardEJA)
1593
1594 EXAMPLES::
1595
1596 sage: J = HadamardEJA(2)
1597 sage: x = sum(J.gens())
1598 sage: x.trace_norm()
1599 1.414213562373095?
1600
1601 ::
1602
1603 sage: J = JordanSpinEJA(4)
1604 sage: x = sum(J.gens())
1605 sage: x.trace_norm()
1606 2.828427124746190?
1607
1608 """
1609 return self.trace_inner_product(self).sqrt()
1610
1611
1612 class CartesianProductEJAElement(FiniteDimensionalEJAElement):
1613 def det(self):
1614 r"""
1615 Compute the determinant of this product-element using the
1616 determianants of its factors.
1617
1618 This result Follows from the spectral decomposition of (say)
1619 the pair `(x,y)` in terms of the Jordan frame `\left\{ (c_1,
1620 0),(c_2, 0),...,(0,d_1),(0,d_2),... \right\}.
1621 """
1622 from sage.misc.misc_c import prod
1623 return prod( f.det() for f in self.cartesian_factors() )
1624
1625 def to_matrix(self):
1626 # An override is necessary to call our custom _scale().
1627 B = self.parent().matrix_basis()
1628 W = self.parent().matrix_space()
1629
1630 # Aaaaand linear combinations don't work in Cartesian
1631 # product spaces, even though they provide a method with
1632 # that name. This is hidden behind an "if" because the
1633 # _scale() function is slow.
1634 pairs = zip(B, self.to_vector())
1635 return W.sum( _scale(b, alpha) for (b,alpha) in pairs )