]> gitweb.michael.orlitzky.com - sage.d.git/blob - mjo/eja/eja_element.py
eja: rename operator_inner_product -> operator_trace inner_product.
[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 EJAOperator
7 from mjo.eja.eja_utils import _scale
8
9
10 class EJAElement(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 (AlbertEJA,
336 ....: JordanSpinEJA,
337 ....: TrivialEJA,
338 ....: RealSymmetricEJA,
339 ....: ComplexHermitianEJA,
340 ....: random_eja)
341
342 EXAMPLES::
343
344 sage: J = JordanSpinEJA(2)
345 sage: x = sum( J.gens() )
346 sage: x.det()
347 0
348
349 ::
350
351 sage: J = JordanSpinEJA(3)
352 sage: x = sum( J.gens() )
353 sage: x.det()
354 -1
355
356 The determinant of the sole element in the rank-zero trivial
357 algebra is ``1``, by three paths of reasoning. First, its
358 characteristic polynomial is a constant ``1``, so the constant
359 term in that polynomial is ``1``. Second, the characteristic
360 polynomial evaluated at zero is again ``1``. And finally, the
361 (empty) product of its eigenvalues is likewise just unity::
362
363 sage: J = TrivialEJA()
364 sage: J.zero().det()
365 1
366
367 TESTS:
368
369 An element is invertible if and only if its determinant is
370 non-zero::
371
372 sage: x = random_eja().random_element()
373 sage: x.is_invertible() == (x.det() != 0)
374 True
375
376 Ensure that the determinant is multiplicative on an associative
377 subalgebra as in Faraut and Korányi's Proposition II.2.2::
378
379 sage: x0 = random_eja().random_element()
380 sage: J = x0.subalgebra_generated_by(orthonormalize=False)
381 sage: x,y = J.random_elements(2)
382 sage: (x*y).det() == x.det()*y.det()
383 True
384
385 The determinant in real matrix algebras is the usual determinant::
386
387 sage: X = matrix.random(QQ,3)
388 sage: X = X + X.T
389 sage: J1 = RealSymmetricEJA(3)
390 sage: J2 = RealSymmetricEJA(3,field=QQ,orthonormalize=False)
391 sage: expected = X.det()
392 sage: actual1 = J1(X).det()
393 sage: actual2 = J2(X).det()
394 sage: actual1 == expected
395 True
396 sage: actual2 == expected
397 True
398
399 There's a formula for the determinant of the Albert algebra
400 (Yokota, Section 2.1)::
401
402 sage: def albert_det(x):
403 ....: X = x.to_matrix()
404 ....: res = X[0,0]*X[1,1]*X[2,2]
405 ....: res += 2*(X[1,2]*X[2,0]*X[0,1]).real()
406 ....: res -= X[0,0]*X[1,2]*X[2,1]
407 ....: res -= X[1,1]*X[2,0]*X[0,2]
408 ....: res -= X[2,2]*X[0,1]*X[1,0]
409 ....: return res.leading_coefficient()
410 sage: J = AlbertEJA(field=QQ, orthonormalize=False)
411 sage: xs = J.random_elements(10)
412 sage: all( albert_det(x) == x.det() for x in xs )
413 True
414
415 """
416 P = self.parent()
417 r = P.rank()
418
419 if r == 0:
420 # Special case, since we don't get the a0=1
421 # coefficient when the rank of the algebra
422 # is zero.
423 return P.base_ring().one()
424
425 p = P._charpoly_coefficients()[0]
426 # The _charpoly_coeff function already adds the factor of -1
427 # to ensure that _charpoly_coefficients()[0] is really what
428 # appears in front of t^{0} in the charpoly. However, we want
429 # (-1)^r times THAT for the determinant.
430 return ((-1)**r)*p(*self.to_vector())
431
432
433 @cached_method
434 def inverse(self):
435 """
436 Return the Jordan-multiplicative inverse of this element.
437
438 ALGORITHM:
439
440 In general we appeal to the quadratic representation as in
441 Koecher's Theorem 12 in Chapter III, Section 5. But if the
442 parent algebra's "characteristic polynomial of" coefficients
443 happen to be cached, then we use Proposition II.2.4 in Faraut
444 and Korányi which gives a formula for the inverse based on the
445 characteristic polynomial and the Cayley-Hamilton theorem for
446 Euclidean Jordan algebras::
447
448 SETUP::
449
450 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
451 ....: JordanSpinEJA,
452 ....: random_eja)
453
454 EXAMPLES:
455
456 The inverse in the spin factor algebra is given in Alizadeh's
457 Example 11.11::
458
459 sage: J = JordanSpinEJA.random_instance()
460 sage: x = J.random_element()
461 sage: while not x.is_invertible():
462 ....: x = J.random_element()
463 sage: x_vec = x.to_vector()
464 sage: x0 = x_vec[:1]
465 sage: x_bar = x_vec[1:]
466 sage: coeff = x0.inner_product(x0) - x_bar.inner_product(x_bar)
467 sage: x_inverse = x_vec.parent()(x0.list() + (-x_bar).list())
468 sage: if not coeff.is_zero(): x_inverse = x_inverse/coeff
469 sage: x.inverse() == J.from_vector(x_inverse)
470 True
471
472 Trying to invert a non-invertible element throws an error:
473
474 sage: JordanSpinEJA(3).zero().inverse()
475 Traceback (most recent call last):
476 ...
477 ZeroDivisionError: element is not invertible
478
479 TESTS:
480
481 The identity element is its own inverse::
482
483 sage: J = random_eja()
484 sage: J.one().inverse() == J.one()
485 True
486
487 If an element has an inverse, it acts like one::
488
489 sage: J = random_eja()
490 sage: x = J.random_element()
491 sage: (not x.is_invertible()) or (x.inverse()*x == J.one())
492 True
493
494 The inverse of the inverse is what we started with::
495
496 sage: J = random_eja()
497 sage: x = J.random_element()
498 sage: (not x.is_invertible()) or (x.inverse().inverse() == x)
499 True
500
501 Proposition II.2.3 in Faraut and Korányi says that the inverse
502 of an element is the inverse of its left-multiplication operator
503 applied to the algebra's identity, when that inverse exists::
504
505 sage: J = random_eja() # long time
506 sage: x = J.random_element() # long time
507 sage: (not x.operator().is_invertible()) or ( # long time
508 ....: x.operator().inverse()(J.one()) # long time
509 ....: == # long time
510 ....: x.inverse() ) # long time
511 True
512
513 Check that the fast (cached) and slow algorithms give the same
514 answer::
515
516 sage: J = random_eja(field=QQ, orthonormalize=False) # long time
517 sage: x = J.random_element() # long time
518 sage: while not x.is_invertible(): # long time
519 ....: x = J.random_element() # long time
520 sage: slow = x.inverse() # long time
521 sage: _ = J._charpoly_coefficients() # long time
522 sage: fast = x.inverse() # long time
523 sage: slow == fast # long time
524 True
525 """
526 not_invertible_msg = "element is not invertible"
527
528 algebra = self.parent()
529 if algebra._charpoly_coefficients.is_in_cache():
530 # We can invert using our charpoly if it will be fast to
531 # compute. If the coefficients are cached, our rank had
532 # better be too!
533 if self.det().is_zero():
534 raise ZeroDivisionError(not_invertible_msg)
535 r = algebra.rank()
536 a = self.characteristic_polynomial().coefficients(sparse=False)
537 return (-1)**(r+1)*algebra.sum(a[i+1]*self**i
538 for i in range(r))/self.det()
539
540 try:
541 inv = (~self.quadratic_representation())(self)
542 self.is_invertible.set_cache(True)
543 return inv
544 except ZeroDivisionError:
545 self.is_invertible.set_cache(False)
546 raise ZeroDivisionError(not_invertible_msg)
547
548
549 @cached_method
550 def is_invertible(self):
551 """
552 Return whether or not this element is invertible.
553
554 ALGORITHM:
555
556 If computing my determinant will be fast, we do so and compare
557 with zero (Proposition II.2.4 in Faraut and
558 Korányi). Otherwise, Proposition II.3.2 in Faraut and Korányi
559 reduces the problem to the invertibility of my quadratic
560 representation.
561
562 SETUP::
563
564 sage: from mjo.eja.eja_algebra import random_eja
565
566 TESTS:
567
568 The identity element is always invertible::
569
570 sage: J = random_eja()
571 sage: J.one().is_invertible()
572 True
573
574 The zero element is never invertible in a non-trivial algebra::
575
576 sage: J = random_eja()
577 sage: (not J.is_trivial()) and J.zero().is_invertible()
578 False
579
580 Test that the fast (cached) and slow algorithms give the same
581 answer::
582
583 sage: J = random_eja(field=QQ, orthonormalize=False) # long time
584 sage: x = J.random_element() # long time
585 sage: slow = x.is_invertible() # long time
586 sage: _ = J._charpoly_coefficients() # long time
587 sage: fast = x.is_invertible() # long time
588 sage: slow == fast # long time
589 True
590 """
591 if self.is_zero():
592 if self.parent().is_trivial():
593 return True
594 else:
595 return False
596
597 if self.parent()._charpoly_coefficients.is_in_cache():
598 # The determinant will be quicker than inverting the
599 # quadratic representation, most likely.
600 return (not self.det().is_zero())
601
602 # The easiest way to determine if I'm invertible is to try.
603 try:
604 inv = (~self.quadratic_representation())(self)
605 self.inverse.set_cache(inv)
606 return True
607 except ZeroDivisionError:
608 return False
609
610
611 def is_primitive_idempotent(self):
612 """
613 Return whether or not this element is a primitive (or minimal)
614 idempotent.
615
616 A primitive idempotent is a non-zero idempotent that is not
617 the sum of two other non-zero idempotents. Remark 2.7.15 in
618 Baes shows that this is what he refers to as a "minimal
619 idempotent."
620
621 An element of a Euclidean Jordan algebra is a minimal idempotent
622 if it :meth:`is_idempotent` and if its Peirce subalgebra
623 corresponding to the eigenvalue ``1`` has dimension ``1`` (Baes,
624 Proposition 2.7.17).
625
626 SETUP::
627
628 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
629 ....: RealSymmetricEJA,
630 ....: TrivialEJA,
631 ....: random_eja)
632
633 WARNING::
634
635 This method is sloooooow.
636
637 EXAMPLES:
638
639 The spectral decomposition of a non-regular element should always
640 contain at least one non-minimal idempotent::
641
642 sage: J = RealSymmetricEJA(3)
643 sage: x = sum(J.gens())
644 sage: x.is_regular()
645 False
646 sage: [ c.is_primitive_idempotent()
647 ....: for (l,c) in x.spectral_decomposition() ]
648 [False, True]
649
650 On the other hand, the spectral decomposition of a regular
651 element should always be in terms of minimal idempotents::
652
653 sage: J = JordanSpinEJA(4)
654 sage: x = sum( i*J.monomial(i) for i in range(len(J.gens())) )
655 sage: x.is_regular()
656 True
657 sage: [ c.is_primitive_idempotent()
658 ....: for (l,c) in x.spectral_decomposition() ]
659 [True, True]
660
661 TESTS:
662
663 The identity element is minimal only in an EJA of rank one::
664
665 sage: J = random_eja()
666 sage: J.rank() == 1 or not J.one().is_primitive_idempotent()
667 True
668
669 A non-idempotent cannot be a minimal idempotent::
670
671 sage: J = JordanSpinEJA(4)
672 sage: x = J.random_element()
673 sage: (not x.is_idempotent()) and x.is_primitive_idempotent()
674 False
675
676 Proposition 2.7.19 in Baes says that an element is a minimal
677 idempotent if and only if it's idempotent with trace equal to
678 unity::
679
680 sage: J = JordanSpinEJA(4)
681 sage: x = J.random_element()
682 sage: expected = (x.is_idempotent() and x.trace() == 1)
683 sage: actual = x.is_primitive_idempotent()
684 sage: actual == expected
685 True
686
687 Primitive idempotents must be non-zero::
688
689 sage: J = random_eja()
690 sage: J.zero().is_idempotent()
691 True
692 sage: J.zero().is_primitive_idempotent()
693 False
694
695 As a consequence of the fact that primitive idempotents must
696 be non-zero, there are no primitive idempotents in a trivial
697 Euclidean Jordan algebra::
698
699 sage: J = TrivialEJA()
700 sage: J.one().is_idempotent()
701 True
702 sage: J.one().is_primitive_idempotent()
703 False
704
705 """
706 if not self.is_idempotent():
707 return False
708
709 if self.is_zero():
710 return False
711
712 (_,_,J1) = self.parent().peirce_decomposition(self)
713 return (J1.dimension() == 1)
714
715
716 def is_nilpotent(self):
717 """
718 Return whether or not some power of this element is zero.
719
720 ALGORITHM:
721
722 We use Theorem 5 in Chapter III of Koecher, which says that
723 an element ``x`` is nilpotent if and only if ``x.operator()``
724 is nilpotent. And it is a basic fact of linear algebra that
725 an operator on an `n`-dimensional space is nilpotent if and
726 only if, when raised to the `n`th power, it equals the zero
727 operator (for example, see Axler Corollary 8.8).
728
729 SETUP::
730
731 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
732 ....: random_eja)
733
734 EXAMPLES::
735
736 sage: J = JordanSpinEJA(3)
737 sage: x = sum(J.gens())
738 sage: x.is_nilpotent()
739 False
740
741 TESTS:
742
743 The identity element is never nilpotent, except in a trivial EJA::
744
745 sage: J = random_eja()
746 sage: J.one().is_nilpotent() and not J.is_trivial()
747 False
748
749 The additive identity is always nilpotent::
750
751 sage: random_eja().zero().is_nilpotent()
752 True
753
754 """
755 P = self.parent()
756 zero_operator = P.zero().operator()
757 return self.operator()**P.dimension() == zero_operator
758
759
760 def is_regular(self):
761 """
762 Return whether or not this is a regular element.
763
764 SETUP::
765
766 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
767 ....: random_eja)
768
769 EXAMPLES:
770
771 The identity element always has degree one, but any element
772 linearly-independent from it is regular::
773
774 sage: J = JordanSpinEJA(5)
775 sage: J.one().is_regular()
776 False
777 sage: b0, b1, b2, b3, b4 = J.gens()
778 sage: b0 == J.one()
779 True
780 sage: for x in J.gens():
781 ....: (J.one() + x).is_regular()
782 False
783 True
784 True
785 True
786 True
787
788 TESTS:
789
790 The zero element should never be regular, unless the parent
791 algebra has dimension less than or equal to one::
792
793 sage: J = random_eja()
794 sage: J.dimension() <= 1 or not J.zero().is_regular()
795 True
796
797 The unit element isn't regular unless the algebra happens to
798 consist of only its scalar multiples::
799
800 sage: J = random_eja()
801 sage: J.dimension() <= 1 or not J.one().is_regular()
802 True
803
804 """
805 return self.degree() == self.parent().rank()
806
807
808 def degree(self):
809 """
810 Return the degree of this element, which is defined to be
811 the degree of its minimal polynomial.
812
813 ALGORITHM:
814
815 First we handle the special cases where the algebra is
816 trivial, this element is zero, or the dimension of the algebra
817 is one and this element is not zero. With those out of the
818 way, we may assume that ``self`` is nonzero, the algebra is
819 nontrivial, and that the dimension of the algebra is at least
820 two.
821
822 Beginning with the algebra's unit element (power zero), we add
823 successive (basis representations of) powers of this element
824 to a matrix, row-reducing at each step. After row-reducing, we
825 check the rank of the matrix. If adding a row and row-reducing
826 does not increase the rank of the matrix at any point, the row
827 we've just added lives in the span of the previous ones; thus
828 the corresponding power of ``self`` lives in the span of its
829 lesser powers. When that happens, the degree of the minimal
830 polynomial is the rank of the matrix; if it never happens, the
831 degree must be the dimension of the entire space.
832
833 SETUP::
834
835 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
836 ....: random_eja)
837
838 EXAMPLES::
839
840 sage: J = JordanSpinEJA(4)
841 sage: J.one().degree()
842 1
843 sage: b0,b1,b2,b3 = J.gens()
844 sage: (b0 - b1).degree()
845 2
846
847 In the spin factor algebra (of rank two), all elements that
848 aren't multiples of the identity are regular::
849
850 sage: J = JordanSpinEJA.random_instance()
851 sage: n = J.dimension()
852 sage: x = J.random_element()
853 sage: x.degree() == min(n,2) or (x == x.coefficient(0)*J.one())
854 True
855
856 TESTS:
857
858 The zero and unit elements are both of degree one in nontrivial
859 algebras::
860
861 sage: J = random_eja()
862 sage: d = J.zero().degree()
863 sage: (J.is_trivial() and d == 0) or d == 1
864 True
865 sage: d = J.one().degree()
866 sage: (J.is_trivial() and d == 0) or d == 1
867 True
868
869 Our implementation agrees with the definition::
870
871 sage: x = random_eja().random_element()
872 sage: x.degree() == x.minimal_polynomial().degree()
873 True
874 """
875 n = self.parent().dimension()
876
877 if n == 0:
878 # The minimal polynomial is an empty product, i.e. the
879 # constant polynomial "1" having degree zero.
880 return 0
881 elif self.is_zero():
882 # The minimal polynomial of zero in a nontrivial algebra
883 # is "t", and is of degree one.
884 return 1
885 elif n == 1:
886 # If this is a nonzero element of a nontrivial algebra, it
887 # has degree at least one. It follows that, in an algebra
888 # of dimension one, the degree must be actually one.
889 return 1
890
891 # BEWARE: The subalgebra_generated_by() method uses the result
892 # of this method to construct a basis for the subalgebra. That
893 # means, in particular, that we cannot implement this method
894 # as ``self.subalgebra_generated_by().dimension()``.
895
896 # Algorithm: keep appending (vector representations of) powers
897 # self as rows to a matrix and echelonizing it. When its rank
898 # stops increasing, we've reached a redundancy.
899
900 # Given the special cases above, we can assume that "self" is
901 # nonzero, the algebra is nontrivial, and that its dimension
902 # is at least two.
903 M = matrix([(self.parent().one()).to_vector()])
904 old_rank = 1
905
906 # Specifying the row-reduction algorithm can e.g. help over
907 # AA because it avoids the RecursionError that gets thrown
908 # when we have to look too hard for a root.
909 #
910 # Beware: QQ supports an entirely different set of "algorithm"
911 # keywords than do AA and RR.
912 algo = None
913 from sage.rings.all import QQ
914 if self.parent().base_ring() is not QQ:
915 algo = "scaled_partial_pivoting"
916
917 for d in range(1,n):
918 M = matrix(M.rows() + [(self**d).to_vector()])
919 M.echelonize(algo)
920 new_rank = M.rank()
921 if new_rank == old_rank:
922 return new_rank
923 else:
924 old_rank = new_rank
925
926 return n
927
928
929
930 def left_matrix(self):
931 """
932 Our parent class defines ``left_matrix`` and ``matrix``
933 methods whose names are misleading. We don't want them.
934 """
935 raise NotImplementedError("use operator().matrix() instead")
936
937 matrix = left_matrix
938
939
940 def minimal_polynomial(self):
941 """
942 Return the minimal polynomial of this element,
943 as a function of the variable `t`.
944
945 ALGORITHM:
946
947 We restrict ourselves to the associative subalgebra
948 generated by this element, and then return the minimal
949 polynomial of this element's operator matrix (in that
950 subalgebra). This works by Baes Proposition 2.3.16.
951
952 SETUP::
953
954 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
955 ....: RealSymmetricEJA,
956 ....: TrivialEJA,
957 ....: random_eja)
958
959 EXAMPLES:
960
961 Keeping in mind that the polynomial ``1`` evaluates the identity
962 element (also the zero element) of the trivial algebra, it is clear
963 that the polynomial ``1`` is the minimal polynomial of the only
964 element in a trivial algebra::
965
966 sage: J = TrivialEJA()
967 sage: J.one().minimal_polynomial()
968 1
969 sage: J.zero().minimal_polynomial()
970 1
971
972 TESTS:
973
974 The minimal polynomial of the identity and zero elements are
975 always the same, except in trivial algebras where the minimal
976 polynomial of the unit/zero element is ``1``::
977
978 sage: J = random_eja()
979 sage: mu = J.one().minimal_polynomial()
980 sage: t = mu.parent().gen()
981 sage: mu + int(J.is_trivial())*(t-2)
982 t - 1
983 sage: mu = J.zero().minimal_polynomial()
984 sage: t = mu.parent().gen()
985 sage: mu + int(J.is_trivial())*(t-1)
986 t
987
988 The degree of an element is (by one definition) the degree
989 of its minimal polynomial::
990
991 sage: x = random_eja().random_element()
992 sage: x.degree() == x.minimal_polynomial().degree()
993 True
994
995 The minimal polynomial and the characteristic polynomial coincide
996 and are known (see Alizadeh, Example 11.11) for all elements of
997 the spin factor algebra that aren't scalar multiples of the
998 identity. We require the dimension of the algebra to be at least
999 two here so that said elements actually exist::
1000
1001 sage: d_max = JordanSpinEJA._max_random_instance_dimension()
1002 sage: n = ZZ.random_element(2, max(2,d_max))
1003 sage: J = JordanSpinEJA(n)
1004 sage: y = J.random_element()
1005 sage: while y == y.coefficient(0)*J.one():
1006 ....: y = J.random_element()
1007 sage: y0 = y.to_vector()[0]
1008 sage: y_bar = y.to_vector()[1:]
1009 sage: actual = y.minimal_polynomial()
1010 sage: t = PolynomialRing(J.base_ring(),'t').gen(0)
1011 sage: expected = t^2 - 2*y0*t + (y0^2 - norm(y_bar)^2)
1012 sage: bool(actual == expected)
1013 True
1014
1015 The minimal polynomial should always kill its element::
1016
1017 sage: x = random_eja().random_element() # long time
1018 sage: p = x.minimal_polynomial() # long time
1019 sage: x.apply_univariate_polynomial(p) # long time
1020 0
1021
1022 The minimal polynomial is invariant under a change of basis,
1023 and in particular, a re-scaling of the basis::
1024
1025 sage: d_max = RealSymmetricEJA._max_random_instance_dimension()
1026 sage: d = ZZ.random_element(1, d_max)
1027 sage: n = RealSymmetricEJA._max_random_instance_size(d)
1028 sage: J1 = RealSymmetricEJA(n)
1029 sage: J2 = RealSymmetricEJA(n,orthonormalize=False)
1030 sage: X = random_matrix(AA,n)
1031 sage: X = X*X.transpose()
1032 sage: x1 = J1(X)
1033 sage: x2 = J2(X)
1034 sage: x1.minimal_polynomial() == x2.minimal_polynomial()
1035 True
1036
1037 """
1038 if self.is_zero():
1039 # Pretty sure we know what the minimal polynomial of
1040 # the zero operator is going to be. This ensures
1041 # consistency of e.g. the polynomial variable returned
1042 # in the "normal" case without us having to think about it.
1043 return self.operator().minimal_polynomial()
1044
1045 # If we don't orthonormalize the subalgebra's basis, then the
1046 # first two monomials in the subalgebra will be self^0 and
1047 # self^1... assuming that self^1 is not a scalar multiple of
1048 # self^0 (the unit element). We special case these to avoid
1049 # having to solve a system to coerce self into the subalgebra.
1050 A = self.subalgebra_generated_by(orthonormalize=False)
1051
1052 if A.dimension() == 1:
1053 # Does a solve to find the scalar multiple alpha such that
1054 # alpha*unit = self. We have to do this because the basis
1055 # for the subalgebra will be [ self^0 ], and not [ self^1 ]!
1056 unit = self.parent().one()
1057 alpha = self.to_vector() / unit.to_vector()
1058 return (unit.operator()*alpha).minimal_polynomial()
1059 else:
1060 # If the dimension of the subalgebra is >= 2, then we just
1061 # use the second basis element.
1062 return A.monomial(1).operator().minimal_polynomial()
1063
1064
1065
1066 def to_matrix(self):
1067 """
1068 Return an (often more natural) representation of this element as a
1069 matrix.
1070
1071 Every finite-dimensional Euclidean Jordan Algebra is a direct
1072 sum of five simple algebras, four of which comprise Hermitian
1073 matrices. This method returns a "natural" matrix
1074 representation of this element as either a Hermitian matrix or
1075 column vector.
1076
1077 SETUP::
1078
1079 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
1080 ....: HadamardEJA,
1081 ....: QuaternionHermitianEJA,
1082 ....: RealSymmetricEJA)
1083
1084 EXAMPLES::
1085
1086 sage: J = ComplexHermitianEJA(3)
1087 sage: J.one()
1088 b0 + b3 + b8
1089 sage: J.one().to_matrix()
1090 +---+---+---+
1091 | 1 | 0 | 0 |
1092 +---+---+---+
1093 | 0 | 1 | 0 |
1094 +---+---+---+
1095 | 0 | 0 | 1 |
1096 +---+---+---+
1097
1098 ::
1099
1100 sage: J = QuaternionHermitianEJA(2)
1101 sage: J.one()
1102 b0 + b5
1103 sage: J.one().to_matrix()
1104 +---+---+
1105 | 1 | 0 |
1106 +---+---+
1107 | 0 | 1 |
1108 +---+---+
1109
1110 This also works in Cartesian product algebras::
1111
1112 sage: J1 = HadamardEJA(1)
1113 sage: J2 = RealSymmetricEJA(2)
1114 sage: J = cartesian_product([J1,J2])
1115 sage: x = sum(J.gens())
1116 sage: x.to_matrix()[0]
1117 [1]
1118 sage: x.to_matrix()[1]
1119 [ 1 0.7071067811865475?]
1120 [0.7071067811865475? 1]
1121
1122 """
1123 B = self.parent().matrix_basis()
1124 W = self.parent().matrix_space()
1125
1126 # This is just a manual "from_vector()", but of course
1127 # matrix spaces aren't vector spaces in sage, so they
1128 # don't have a from_vector() method.
1129 return W.linear_combination( zip(B, self.to_vector()) )
1130
1131
1132
1133 def norm(self):
1134 """
1135 The norm of this element with respect to :meth:`inner_product`.
1136
1137 SETUP::
1138
1139 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
1140 ....: HadamardEJA)
1141
1142 EXAMPLES::
1143
1144 sage: J = HadamardEJA(2)
1145 sage: x = sum(J.gens())
1146 sage: x.norm()
1147 1.414213562373095?
1148
1149 ::
1150
1151 sage: J = JordanSpinEJA(4)
1152 sage: x = sum(J.gens())
1153 sage: x.norm()
1154 2
1155
1156 """
1157 return self.inner_product(self).sqrt()
1158
1159
1160 def operator(self):
1161 """
1162 Return the left-multiplication-by-this-element
1163 operator on the ambient algebra.
1164
1165 SETUP::
1166
1167 sage: from mjo.eja.eja_algebra import random_eja
1168
1169 TESTS::
1170
1171 sage: J = random_eja()
1172 sage: x,y = J.random_elements(2)
1173 sage: x.operator()(y) == x*y
1174 True
1175 sage: y.operator()(x) == x*y
1176 True
1177
1178 """
1179 P = self.parent()
1180 left_mult_by_self = lambda y: self*y
1181 L = P.module_morphism(function=left_mult_by_self, codomain=P)
1182 return EJAOperator(P, P, L.matrix() )
1183
1184
1185 def quadratic_representation(self, other=None):
1186 """
1187 Return the quadratic representation of this element.
1188
1189 SETUP::
1190
1191 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
1192 ....: random_eja)
1193
1194 EXAMPLES:
1195
1196 The explicit form in the spin factor algebra is given by
1197 Alizadeh's Example 11.12::
1198
1199 sage: x = JordanSpinEJA.random_instance().random_element()
1200 sage: x_vec = x.to_vector()
1201 sage: Q = matrix.identity(x.base_ring(), 0)
1202 sage: n = x_vec.degree()
1203 sage: if n > 0:
1204 ....: x0 = x_vec[0]
1205 ....: x_bar = x_vec[1:]
1206 ....: A = matrix(x.base_ring(), 1, [x_vec.inner_product(x_vec)])
1207 ....: B = 2*x0*x_bar.row()
1208 ....: C = 2*x0*x_bar.column()
1209 ....: D = matrix.identity(x.base_ring(), n-1)
1210 ....: D = (x0^2 - x_bar.inner_product(x_bar))*D
1211 ....: D = D + 2*x_bar.tensor_product(x_bar)
1212 ....: Q = matrix.block(2,2,[A,B,C,D])
1213 sage: Q == x.quadratic_representation().matrix()
1214 True
1215
1216 Test all of the properties from Theorem 11.2 in Alizadeh::
1217
1218 sage: J = random_eja()
1219 sage: x,y = J.random_elements(2)
1220 sage: Lx = x.operator()
1221 sage: Lxx = (x*x).operator()
1222 sage: Qx = x.quadratic_representation()
1223 sage: Qy = y.quadratic_representation()
1224 sage: Qxy = x.quadratic_representation(y)
1225 sage: Qex = J.one().quadratic_representation(x)
1226 sage: n = ZZ.random_element(10)
1227 sage: Qxn = (x^n).quadratic_representation()
1228
1229 Property 1:
1230
1231 sage: 2*Qxy == (x+y).quadratic_representation() - Qx - Qy
1232 True
1233
1234 Property 2 (multiply on the right for :trac:`28272`):
1235
1236 sage: alpha = J.base_ring().random_element()
1237 sage: (alpha*x).quadratic_representation() == Qx*(alpha^2)
1238 True
1239
1240 Property 3:
1241
1242 sage: not x.is_invertible() or ( Qx(x.inverse()) == x )
1243 True
1244
1245 sage: not x.is_invertible() or (
1246 ....: ~Qx
1247 ....: ==
1248 ....: x.inverse().quadratic_representation() )
1249 True
1250
1251 sage: Qxy(J.one()) == x*y
1252 True
1253
1254 Property 4:
1255
1256 sage: not x.is_invertible() or (
1257 ....: x.quadratic_representation(x.inverse())*Qx
1258 ....: == Qx*x.quadratic_representation(x.inverse()) )
1259 True
1260
1261 sage: not x.is_invertible() or (
1262 ....: x.quadratic_representation(x.inverse())*Qx
1263 ....: ==
1264 ....: 2*Lx*Qex - Qx )
1265 True
1266
1267 sage: 2*Lx*Qex - Qx == Lxx
1268 True
1269
1270 Property 5:
1271
1272 sage: Qy(x).quadratic_representation() == Qy*Qx*Qy
1273 True
1274
1275 Property 6:
1276
1277 sage: Qxn == (Qx)^n
1278 True
1279
1280 Property 7:
1281
1282 sage: not x.is_invertible() or (
1283 ....: Qx*x.inverse().operator() == Lx )
1284 True
1285
1286 Property 8:
1287
1288 sage: not x.operator_commutes_with(y) or (
1289 ....: Qx(y)^n == Qxn(y^n) )
1290 True
1291
1292 """
1293 if other is None:
1294 other=self
1295 elif not other in self.parent():
1296 raise TypeError("'other' must live in the same algebra")
1297
1298 L = self.operator()
1299 M = other.operator()
1300 return ( L*M + M*L - (self*other).operator() )
1301
1302
1303
1304 def spectral_decomposition(self):
1305 """
1306 Return the unique spectral decomposition of this element.
1307
1308 ALGORITHM:
1309
1310 Following Faraut and Korányi's Theorem III.1.1, we restrict this
1311 element's left-multiplication-by operator to the subalgebra it
1312 generates. We then compute the spectral decomposition of that
1313 operator, and the spectral projectors we get back must be the
1314 left-multiplication-by operators for the idempotents we
1315 seek. Thus applying them to the identity element gives us those
1316 idempotents.
1317
1318 Since the eigenvalues are required to be distinct, we take
1319 the spectral decomposition of the zero element to be zero
1320 times the identity element of the algebra (which is idempotent,
1321 obviously).
1322
1323 SETUP::
1324
1325 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1326
1327 EXAMPLES:
1328
1329 The spectral decomposition of the identity is ``1`` times itself,
1330 and the spectral decomposition of zero is ``0`` times the identity::
1331
1332 sage: J = RealSymmetricEJA(3)
1333 sage: J.one()
1334 b0 + b2 + b5
1335 sage: J.one().spectral_decomposition()
1336 [(1, b0 + b2 + b5)]
1337 sage: J.zero().spectral_decomposition()
1338 [(0, b0 + b2 + b5)]
1339
1340 TESTS::
1341
1342 sage: J = RealSymmetricEJA(4)
1343 sage: x = sum(J.gens())
1344 sage: sd = x.spectral_decomposition()
1345 sage: l0 = sd[0][0]
1346 sage: l1 = sd[1][0]
1347 sage: c0 = sd[0][1]
1348 sage: c1 = sd[1][1]
1349 sage: c0.inner_product(c1) == 0
1350 True
1351 sage: c0.is_idempotent()
1352 True
1353 sage: c1.is_idempotent()
1354 True
1355 sage: c0 + c1 == J.one()
1356 True
1357 sage: l0*c0 + l1*c1 == x
1358 True
1359
1360 The spectral decomposition should work in subalgebras, too::
1361
1362 sage: J = RealSymmetricEJA(4)
1363 sage: (b0, b1, b2, b3, b4, b5, b6, b7, b8, b9) = J.gens()
1364 sage: A = 2*b5 - 2*b8
1365 sage: (lambda1, c1) = A.spectral_decomposition()[1]
1366 sage: (J0, J5, J1) = J.peirce_decomposition(c1)
1367 sage: (f0, f1, f2) = J1.gens()
1368 sage: f0.spectral_decomposition()
1369 [(0, 1.000000000000000?*c2), (1, 1.000000000000000?*c0)]
1370
1371 """
1372 A = self.subalgebra_generated_by(orthonormalize=True)
1373 result = []
1374 for (evalue, proj) in A(self).operator().spectral_decomposition():
1375 result.append( (evalue, proj(A.one()).superalgebra_element()) )
1376 return result
1377
1378 def subalgebra_generated_by(self, **kwargs):
1379 """
1380 Return the associative subalgebra of the parent EJA generated
1381 by this element.
1382
1383 Since our parent algebra is unital, we want "subalgebra" to mean
1384 "unital subalgebra" as well; thus the subalgebra that an element
1385 generates will itself be a Euclidean Jordan algebra after
1386 restricting the algebra operations appropriately. This is the
1387 subalgebra that Faraut and Korányi work with in section II.2, for
1388 example.
1389
1390 SETUP::
1391
1392 sage: from mjo.eja.eja_algebra import (random_eja,
1393 ....: HadamardEJA,
1394 ....: RealSymmetricEJA)
1395
1396 EXAMPLES:
1397
1398 We can create subalgebras of Cartesian product EJAs that are not
1399 themselves Cartesian product EJAs (they're just "regular" EJAs)::
1400
1401 sage: J1 = HadamardEJA(3)
1402 sage: J2 = RealSymmetricEJA(2)
1403 sage: J = cartesian_product([J1,J2])
1404 sage: J.one().subalgebra_generated_by()
1405 Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
1406
1407 TESTS:
1408
1409 This subalgebra, being composed of only powers, is associative::
1410
1411 sage: x0 = random_eja().random_element()
1412 sage: A = x0.subalgebra_generated_by(orthonormalize=False)
1413 sage: x,y,z = A.random_elements(3)
1414 sage: (x*y)*z == x*(y*z)
1415 True
1416
1417 Squaring in the subalgebra should work the same as in
1418 the superalgebra::
1419
1420 sage: x = random_eja().random_element()
1421 sage: A = x.subalgebra_generated_by(orthonormalize=False)
1422 sage: A(x^2) == A(x)*A(x)
1423 True
1424
1425 By definition, the subalgebra generated by the zero element is
1426 the one-dimensional algebra generated by the identity
1427 element... unless the original algebra was trivial, in which
1428 case the subalgebra is trivial too::
1429
1430 sage: A = random_eja().zero().subalgebra_generated_by()
1431 sage: (A.is_trivial() and A.dimension() == 0) or A.dimension() == 1
1432 True
1433
1434 """
1435 powers = tuple( self**k for k in range(self.degree()) )
1436 A = self.parent().subalgebra(powers,
1437 associative=True,
1438 check_field=False,
1439 check_axioms=False,
1440 **kwargs)
1441 A.one.set_cache(A(self.parent().one()))
1442 return A
1443
1444
1445 def subalgebra_idempotent(self):
1446 """
1447 Find an idempotent in the associative subalgebra I generate
1448 using Proposition 2.3.5 in Baes.
1449
1450 SETUP::
1451
1452 sage: from mjo.eja.eja_algebra import random_eja
1453
1454 TESTS:
1455
1456 Ensure that we can find an idempotent in a non-trivial algebra
1457 where there are non-nilpotent elements, or that we get the dumb
1458 solution in the trivial algebra::
1459
1460 sage: J = random_eja(field=QQ, orthonormalize=False)
1461 sage: x = J.random_element()
1462 sage: while x.is_nilpotent() and not J.is_trivial():
1463 ....: x = J.random_element()
1464 sage: c = x.subalgebra_idempotent()
1465 sage: c^2 == c
1466 True
1467
1468 """
1469 if self.parent().is_trivial():
1470 return self
1471
1472 if self.is_nilpotent():
1473 raise ValueError("this only works with non-nilpotent elements!")
1474
1475 # The subalgebra is transient (we return an element of the
1476 # superalgebra, i.e. this algebra) so why bother
1477 # orthonormalizing?
1478 J = self.subalgebra_generated_by(orthonormalize=False)
1479 u = J(self)
1480
1481 # The image of the matrix of left-u^m-multiplication
1482 # will be minimal for some natural number s...
1483 s = 0
1484 minimal_dim = J.dimension()
1485 for i in range(1, minimal_dim):
1486 this_dim = (u**i).operator().matrix().image().dimension()
1487 if this_dim < minimal_dim:
1488 minimal_dim = this_dim
1489 s = i
1490
1491 # Now minimal_matrix should correspond to the smallest
1492 # non-zero subspace in Baes's (or really, Koecher's)
1493 # proposition.
1494 #
1495 # However, we need to restrict the matrix to work on the
1496 # subspace... or do we? Can't we just solve, knowing that
1497 # A(c) = u^(s+1) should have a solution in the big space,
1498 # too?
1499 u_next = u**(s+1)
1500 A = u_next.operator().matrix()
1501 c = J.from_vector(A.solve_right(u_next.to_vector()))
1502
1503 # Now c is the idempotent we want, but it still lives in
1504 # the subalgebra.
1505 return c.superalgebra_element()
1506
1507
1508 def trace(self):
1509 """
1510 Return my trace, the sum of my eigenvalues.
1511
1512 In a trivial algebra, however you want to look at it, the trace is
1513 an empty sum for which we declare the result to be zero.
1514
1515 SETUP::
1516
1517 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
1518 ....: HadamardEJA,
1519 ....: TrivialEJA,
1520 ....: random_eja)
1521
1522 EXAMPLES::
1523
1524 sage: J = TrivialEJA()
1525 sage: J.zero().trace()
1526 0
1527
1528 ::
1529 sage: J = JordanSpinEJA(3)
1530 sage: x = sum(J.gens())
1531 sage: x.trace()
1532 2
1533
1534 ::
1535
1536 sage: J = HadamardEJA(5)
1537 sage: J.one().trace()
1538 5
1539
1540 TESTS:
1541
1542 The trace of an element is a real number::
1543
1544 sage: J = random_eja()
1545 sage: J.random_element().trace() in RLF
1546 True
1547
1548 The trace is linear::
1549
1550 sage: J = random_eja()
1551 sage: x,y = J.random_elements(2)
1552 sage: alpha = J.base_ring().random_element()
1553 sage: (alpha*x + y).trace() == alpha*x.trace() + y.trace()
1554 True
1555
1556 The trace of a square is nonnegative::
1557
1558 sage: x = random_eja().random_element()
1559 sage: (x*x).trace() >= 0
1560 True
1561
1562 """
1563 P = self.parent()
1564 r = P.rank()
1565
1566 if r == 0:
1567 # Special case for the trivial algebra where
1568 # the trace is an empty sum.
1569 return P.base_ring().zero()
1570
1571 p = P._charpoly_coefficients()[r-1]
1572 # The _charpoly_coeff function already adds the factor of
1573 # -1 to ensure that _charpoly_coeff(r-1) is really what
1574 # appears in front of t^{r-1} in the charpoly. However,
1575 # we want the negative of THAT for the trace.
1576 return -p(*self.to_vector())
1577
1578
1579 def trace_inner_product(self, other):
1580 """
1581 Return the trace inner product of myself and ``other``.
1582
1583 SETUP::
1584
1585 sage: from mjo.eja.eja_algebra import random_eja
1586
1587 TESTS:
1588
1589 The trace inner product is commutative, bilinear, and associative::
1590
1591 sage: J = random_eja()
1592 sage: x,y,z = J.random_elements(3)
1593 sage: # commutative
1594 sage: x.trace_inner_product(y) == y.trace_inner_product(x)
1595 True
1596 sage: # bilinear
1597 sage: a = J.base_ring().random_element()
1598 sage: actual = (a*(x+z)).trace_inner_product(y)
1599 sage: expected = ( a*x.trace_inner_product(y) +
1600 ....: a*z.trace_inner_product(y) )
1601 sage: actual == expected
1602 True
1603 sage: actual = x.trace_inner_product(a*(y+z))
1604 sage: expected = ( a*x.trace_inner_product(y) +
1605 ....: a*x.trace_inner_product(z) )
1606 sage: actual == expected
1607 True
1608 sage: # associative
1609 sage: (x*y).trace_inner_product(z) == y.trace_inner_product(x*z)
1610 True
1611
1612 """
1613 if not other in self.parent():
1614 raise TypeError("'other' must live in the same algebra")
1615
1616 return (self*other).trace()
1617
1618
1619 def trace_norm(self):
1620 """
1621 The norm of this element with respect to :meth:`trace_inner_product`.
1622
1623 SETUP::
1624
1625 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
1626 ....: HadamardEJA)
1627
1628 EXAMPLES::
1629
1630 sage: J = HadamardEJA(2)
1631 sage: x = sum(J.gens())
1632 sage: x.trace_norm()
1633 1.414213562373095?
1634
1635 ::
1636
1637 sage: J = JordanSpinEJA(4)
1638 sage: x = sum(J.gens())
1639 sage: x.trace_norm()
1640 2.828427124746190?
1641
1642 """
1643 return self.trace_inner_product(self).sqrt()
1644
1645
1646 def operator_trace_inner_product(self, other):
1647 r"""
1648 Return the operator inner product of myself and ``other``.
1649
1650 The "operator inner product," whose name is not standard, is
1651 defined be the usual linear-algebraic trace of the
1652 ``(x*y).operator()``.
1653
1654 Proposition III.1.5 in Faraut and Korányi shows that on any
1655 Euclidean Jordan algebra, this is another associative inner
1656 product under which the cone of squares is symmetric.
1657
1658 This works even if the basis hasn't been orthonormalized
1659 because the eigenvalues of the corresponding matrix don't
1660 change when the basis does (they're preserved by any
1661 similarity transformation).
1662
1663 SETUP::
1664
1665 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
1666 ....: RealSymmetricEJA,
1667 ....: ComplexHermitianEJA,
1668 ....: random_eja)
1669
1670 EXAMPLES:
1671
1672 Proposition III.4.2 of Faraut and Korányi shows that on a
1673 simple algebra of rank `r` and dimension `n`, this inner
1674 product is `n/r` times the canonical
1675 :meth:`trace_inner_product`::
1676
1677 sage: J = JordanSpinEJA(4, field=QQ)
1678 sage: x,y = J.random_elements(2)
1679 sage: n = J.dimension()
1680 sage: r = J.rank()
1681 sage: actual = x.operator_trace_inner_product(y)
1682 sage: expected = (n/r)*x.trace_inner_product(y)
1683 sage: actual == expected
1684 True
1685
1686 ::
1687
1688 sage: J = RealSymmetricEJA(3)
1689 sage: x,y = J.random_elements(2)
1690 sage: n = J.dimension()
1691 sage: r = J.rank()
1692 sage: actual = x.operator_trace_inner_product(y)
1693 sage: expected = (n/r)*x.trace_inner_product(y)
1694 sage: actual == expected
1695 True
1696
1697 ::
1698
1699 sage: J = ComplexHermitianEJA(3, field=QQ, orthonormalize=False)
1700 sage: x,y = J.random_elements(2)
1701 sage: n = J.dimension()
1702 sage: r = J.rank()
1703 sage: actual = x.operator_trace_inner_product(y)
1704 sage: expected = (n/r)*x.trace_inner_product(y)
1705 sage: actual == expected
1706 True
1707
1708 TESTS:
1709
1710 The operator inner product is commutative, bilinear, and
1711 associative::
1712
1713 sage: J = random_eja()
1714 sage: x,y,z = J.random_elements(3)
1715 sage: # commutative
1716 sage: actual = x.operator_trace_inner_product(y)
1717 sage: expected = y.operator_trace_inner_product(x)
1718 sage: actual == expected
1719 True
1720 sage: # bilinear
1721 sage: a = J.base_ring().random_element()
1722 sage: actual = (a*(x+z)).operator_trace_inner_product(y)
1723 sage: expected = ( a*x.operator_trace_inner_product(y) +
1724 ....: a*z.operator_trace_inner_product(y) )
1725 sage: actual == expected
1726 True
1727 sage: actual = x.operator_trace_inner_product(a*(y+z))
1728 sage: expected = ( a*x.operator_trace_inner_product(y) +
1729 ....: a*x.operator_trace_inner_product(z) )
1730 sage: actual == expected
1731 True
1732 sage: # associative
1733 sage: actual = (x*y).operator_trace_inner_product(z)
1734 sage: expected = y.operator_trace_inner_product(x*z)
1735 sage: actual == expected
1736 True
1737
1738 Despite the fact that the implementation uses a matrix representation,
1739 the answer is independent of the basis used::
1740
1741 sage: J = RealSymmetricEJA(3, field=QQ, orthonormalize=False)
1742 sage: V = RealSymmetricEJA(3)
1743 sage: x,y = J.random_elements(2)
1744 sage: w = V(x.to_matrix())
1745 sage: z = V(y.to_matrix())
1746 sage: expected = x.operator_trace_inner_product(y)
1747 sage: actual = w.operator_trace_inner_product(z)
1748 sage: actual == expected
1749 True
1750
1751 """
1752 if not other in self.parent():
1753 raise TypeError("'other' must live in the same algebra")
1754
1755 return (self*other).operator().matrix().trace()
1756
1757
1758 def operator_trace_norm(self):
1759 """
1760 The norm of this element with respect to
1761 :meth:`operator_trace_inner_product`.
1762
1763 SETUP::
1764
1765 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
1766 ....: HadamardEJA)
1767
1768 EXAMPLES:
1769
1770 On a simple algebra, this will differ from :meth:`trace_norm`
1771 by the scalar factor ``(n/r).sqrt()``, where `n` is the
1772 dimension of the algebra and `r` its rank. This follows from
1773 the corresponding result (Proposition III.4.2 of Faraut and
1774 Korányi) for the trace inner product::
1775
1776 sage: J = HadamardEJA(2)
1777 sage: x = sum(J.gens())
1778 sage: x.operator_trace_norm()
1779 1.414213562373095?
1780
1781 ::
1782
1783 sage: J = JordanSpinEJA(4)
1784 sage: x = sum(J.gens())
1785 sage: x.operator_trace_norm()
1786 4
1787
1788 """
1789 return self.operator_trace_inner_product(self).sqrt()
1790
1791
1792 class CartesianProductParentEJAElement(EJAElement):
1793 r"""
1794 An intermediate class for elements that have a Cartesian
1795 product as their parent algebra.
1796
1797 This is needed because the ``to_matrix`` method (which gives you a
1798 representation from the superalgebra) needs to do special stuff
1799 for Cartesian products. Specifically, an EJA subalgebra of a
1800 Cartesian product EJA will not itself be a Cartesian product (it
1801 has its own basis) -- but we want ``to_matrix()`` to be able to
1802 give us a Cartesian product representation.
1803 """
1804 def to_matrix(self):
1805 # An override is necessary to call our custom _scale().
1806 B = self.parent().matrix_basis()
1807 W = self.parent().matrix_space()
1808
1809 # Aaaaand linear combinations don't work in Cartesian
1810 # product spaces, even though they provide a method with
1811 # that name. This is hidden in a subclass because the
1812 # _scale() function is slow.
1813 pairs = zip(B, self.to_vector())
1814 return W.sum( _scale(b, alpha) for (b,alpha) in pairs )
1815
1816 class CartesianProductEJAElement(CartesianProductParentEJAElement):
1817 def det(self):
1818 r"""
1819 Compute the determinant of this product-element using the
1820 determianants of its factors.
1821
1822 This result Follows from the spectral decomposition of (say)
1823 the pair `(x,y)` in terms of the Jordan frame `\left\{ (c_1,
1824 0),(c_2, 0),...,(0,d_1),(0,d_2),... \right\}.
1825 """
1826 from sage.misc.misc_c import prod
1827 return prod( f.det() for f in self.cartesian_factors() )