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