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