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