1 from sage
.algebras
.finite_dimensional_algebras
.finite_dimensional_algebra_element
import FiniteDimensionalAlgebraElement
2 from sage
.matrix
.constructor
import matrix
3 from sage
.modules
.free_module
import VectorSpace
5 # TODO: make this unnecessary somehow.
6 from sage
.misc
.lazy_import
import lazy_import
7 lazy_import('mjo.eja.eja_algebra', 'FiniteDimensionalEuclideanJordanAlgebra')
8 from mjo
.eja
.eja_operator
import FiniteDimensionalEuclideanJordanAlgebraOperator
9 from mjo
.eja
.eja_utils
import _mat2vec
11 class FiniteDimensionalEuclideanJordanAlgebraElement(FiniteDimensionalAlgebraElement
):
13 An element of a Euclidean Jordan algebra.
18 Oh man, I should not be doing this. This hides the "disabled"
19 methods ``left_matrix`` and ``matrix`` from introspection;
20 in particular it removes them from tab-completion.
22 return filter(lambda s
: s
not in ['left_matrix', 'matrix'],
26 def __init__(self
, A
, elt
=None):
31 sage: from mjo.eja.eja_algebra import (RealSymmetricEJA,
36 The identity in `S^n` is converted to the identity in the EJA::
38 sage: J = RealSymmetricEJA(3)
39 sage: I = matrix.identity(QQ,3)
43 This skew-symmetric matrix can't be represented in the EJA::
45 sage: J = RealSymmetricEJA(3)
46 sage: A = matrix(QQ,3, lambda i,j: i-j)
48 Traceback (most recent call last):
50 ArithmeticError: vector is not in free module
54 Ensure that we can convert any element of the parent's
55 underlying vector space back into an algebra element whose
56 vector representation is what we started with::
58 sage: set_random_seed()
59 sage: J = random_eja()
60 sage: v = J.vector_space().random_element()
61 sage: J(v).vector() == v
65 # Goal: if we're given a matrix, and if it lives in our
66 # parent algebra's "natural ambient space," convert it
67 # into an algebra element.
69 # The catch is, we make a recursive call after converting
70 # the given matrix into a vector that lives in the algebra.
71 # This we need to try the parent class initializer first,
72 # to avoid recursing forever if we're given something that
73 # already fits into the algebra, but also happens to live
74 # in the parent's "natural ambient space" (this happens with
77 FiniteDimensionalAlgebraElement
.__init
__(self
, A
, elt
)
79 natural_basis
= A
.natural_basis()
80 if elt
in natural_basis
[0].matrix_space():
81 # Thanks for nothing! Matrix spaces aren't vector
82 # spaces in Sage, so we have to figure out its
83 # natural-basis coordinates ourselves.
84 V
= VectorSpace(elt
.base_ring(), elt
.nrows()**2)
85 W
= V
.span( _mat2vec(s
) for s
in natural_basis
)
86 coords
= W
.coordinates(_mat2vec(elt
))
87 FiniteDimensionalAlgebraElement
.__init
__(self
, A
, coords
)
91 Return ``self`` raised to the power ``n``.
93 Jordan algebras are always power-associative; see for
94 example Faraut and Koranyi, Proposition II.1.2 (ii).
96 We have to override this because our superclass uses row
97 vectors instead of column vectors! We, on the other hand,
98 assume column vectors everywhere.
102 sage: from mjo.eja.eja_algebra import random_eja
106 The definition of `x^2` is the unambiguous `x*x`::
108 sage: set_random_seed()
109 sage: x = random_eja().random_element()
113 A few examples of power-associativity::
115 sage: set_random_seed()
116 sage: x = random_eja().random_element()
117 sage: x*(x*x)*(x*x) == x^5
119 sage: (x*x)*(x*x*x) == x^5
122 We also know that powers operator-commute (Koecher, Chapter
125 sage: set_random_seed()
126 sage: x = random_eja().random_element()
127 sage: m = ZZ.random_element(0,10)
128 sage: n = ZZ.random_element(0,10)
129 sage: Lxm = (x^m).operator()
130 sage: Lxn = (x^n).operator()
131 sage: Lxm*Lxn == Lxn*Lxm
136 return self
.parent().one()
140 return (self
.operator()**(n
-1))(self
)
143 def apply_univariate_polynomial(self
, p
):
145 Apply the univariate polynomial ``p`` to this element.
147 A priori, SageMath won't allow us to apply a univariate
148 polynomial to an element of an EJA, because we don't know
149 that EJAs are rings (they are usually not associative). Of
150 course, we know that EJAs are power-associative, so the
151 operation is ultimately kosher. This function sidesteps
152 the CAS to get the answer we want and expect.
156 sage: from mjo.eja.eja_algebra import (RealCartesianProductEJA,
161 sage: R = PolynomialRing(QQ, 't')
163 sage: p = t^4 - t^3 + 5*t - 2
164 sage: J = RealCartesianProductEJA(5)
165 sage: J.one().apply_univariate_polynomial(p) == 3*J.one()
170 We should always get back an element of the algebra::
172 sage: set_random_seed()
173 sage: p = PolynomialRing(QQ, 't').random_element()
174 sage: J = random_eja()
175 sage: x = J.random_element()
176 sage: x.apply_univariate_polynomial(p) in J
180 if len(p
.variables()) > 1:
181 raise ValueError("not a univariate polynomial")
184 # Convert the coeficcients to the parent's base ring,
185 # because a priori they might live in an (unnecessarily)
186 # larger ring for which P.sum() would fail below.
187 cs
= [ R(c
) for c
in p
.coefficients(sparse
=False) ]
188 return P
.sum( cs
[k
]*(self
**k
) for k
in range(len(cs
)) )
191 def characteristic_polynomial(self
):
193 Return the characteristic polynomial of this element.
197 sage: from mjo.eja.eja_algebra import RealCartesianProductEJA
201 The rank of `R^3` is three, and the minimal polynomial of
202 the identity element is `(t-1)` from which it follows that
203 the characteristic polynomial should be `(t-1)^3`::
205 sage: J = RealCartesianProductEJA(3)
206 sage: J.one().characteristic_polynomial()
207 t^3 - 3*t^2 + 3*t - 1
209 Likewise, the characteristic of the zero element in the
210 rank-three algebra `R^{n}` should be `t^{3}`::
212 sage: J = RealCartesianProductEJA(3)
213 sage: J.zero().characteristic_polynomial()
218 The characteristic polynomial of an element should evaluate
219 to zero on that element::
221 sage: set_random_seed()
222 sage: x = RealCartesianProductEJA(3).random_element()
223 sage: p = x.characteristic_polynomial()
224 sage: x.apply_univariate_polynomial(p)
228 p
= self
.parent().characteristic_polynomial()
229 return p(*self
.vector())
232 def inner_product(self
, other
):
234 Return the parent algebra's inner product of myself and ``other``.
238 sage: from mjo.eja.eja_algebra import (
239 ....: ComplexHermitianEJA,
241 ....: QuaternionHermitianEJA,
242 ....: RealSymmetricEJA,
247 The inner product in the Jordan spin algebra is the usual
248 inner product on `R^n` (this example only works because the
249 basis for the Jordan algebra is the standard basis in `R^n`)::
251 sage: J = JordanSpinEJA(3)
252 sage: x = vector(QQ,[1,2,3])
253 sage: y = vector(QQ,[4,5,6])
254 sage: x.inner_product(y)
256 sage: J(x).inner_product(J(y))
259 The inner product on `S^n` is `<X,Y> = trace(X*Y)`, where
260 multiplication is the usual matrix multiplication in `S^n`,
261 so the inner product of the identity matrix with itself
264 sage: J = RealSymmetricEJA(3)
265 sage: J.one().inner_product(J.one())
268 Likewise, the inner product on `C^n` is `<X,Y> =
269 Re(trace(X*Y))`, where we must necessarily take the real
270 part because the product of Hermitian matrices may not be
273 sage: J = ComplexHermitianEJA(3)
274 sage: J.one().inner_product(J.one())
277 Ditto for the quaternions::
279 sage: J = QuaternionHermitianEJA(3)
280 sage: J.one().inner_product(J.one())
285 Ensure that we can always compute an inner product, and that
286 it gives us back a real number::
288 sage: set_random_seed()
289 sage: J = random_eja()
290 sage: x = J.random_element()
291 sage: y = J.random_element()
292 sage: x.inner_product(y) in RR
298 raise TypeError("'other' must live in the same algebra")
300 return P
.inner_product(self
, other
)
303 def operator_commutes_with(self
, other
):
305 Return whether or not this element operator-commutes
310 sage: from mjo.eja.eja_algebra import random_eja
314 The definition of a Jordan algebra says that any element
315 operator-commutes with its square::
317 sage: set_random_seed()
318 sage: x = random_eja().random_element()
319 sage: x.operator_commutes_with(x^2)
324 Test Lemma 1 from Chapter III of Koecher::
326 sage: set_random_seed()
327 sage: J = random_eja()
328 sage: u = J.random_element()
329 sage: v = J.random_element()
330 sage: lhs = u.operator_commutes_with(u*v)
331 sage: rhs = v.operator_commutes_with(u^2)
335 Test the first polarization identity from my notes, Koecher
336 Chapter III, or from Baes (2.3)::
338 sage: set_random_seed()
339 sage: J = random_eja()
340 sage: x = J.random_element()
341 sage: y = J.random_element()
342 sage: Lx = x.operator()
343 sage: Ly = y.operator()
344 sage: Lxx = (x*x).operator()
345 sage: Lxy = (x*y).operator()
346 sage: bool(2*Lx*Lxy + Ly*Lxx == 2*Lxy*Lx + Lxx*Ly)
349 Test the second polarization identity from my notes or from
352 sage: set_random_seed()
353 sage: J = random_eja()
354 sage: x = J.random_element()
355 sage: y = J.random_element()
356 sage: z = J.random_element()
357 sage: Lx = x.operator()
358 sage: Ly = y.operator()
359 sage: Lz = z.operator()
360 sage: Lzy = (z*y).operator()
361 sage: Lxy = (x*y).operator()
362 sage: Lxz = (x*z).operator()
363 sage: bool(Lx*Lzy + Lz*Lxy + Ly*Lxz == Lzy*Lx + Lxy*Lz + Lxz*Ly)
366 Test the third polarization identity from my notes or from
369 sage: set_random_seed()
370 sage: J = random_eja()
371 sage: u = J.random_element()
372 sage: y = J.random_element()
373 sage: z = J.random_element()
374 sage: Lu = u.operator()
375 sage: Ly = y.operator()
376 sage: Lz = z.operator()
377 sage: Lzy = (z*y).operator()
378 sage: Luy = (u*y).operator()
379 sage: Luz = (u*z).operator()
380 sage: Luyz = (u*(y*z)).operator()
381 sage: lhs = Lu*Lzy + Lz*Luy + Ly*Luz
382 sage: rhs = Luyz + Ly*Lu*Lz + Lz*Lu*Ly
383 sage: bool(lhs == rhs)
387 if not other
in self
.parent():
388 raise TypeError("'other' must live in the same algebra")
397 Return my determinant, the product of my eigenvalues.
401 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
406 sage: J = JordanSpinEJA(2)
407 sage: e0,e1 = J.gens()
408 sage: x = sum( J.gens() )
414 sage: J = JordanSpinEJA(3)
415 sage: e0,e1,e2 = J.gens()
416 sage: x = sum( J.gens() )
422 An element is invertible if and only if its determinant is
425 sage: set_random_seed()
426 sage: x = random_eja().random_element()
427 sage: x.is_invertible() == (x.det() != 0)
433 p
= P
._charpoly
_coeff
(0)
434 # The _charpoly_coeff function already adds the factor of
435 # -1 to ensure that _charpoly_coeff(0) is really what
436 # appears in front of t^{0} in the charpoly. However,
437 # we want (-1)^r times THAT for the determinant.
438 return ((-1)**r
)*p(*self
.vector())
443 Return the Jordan-multiplicative inverse of this element.
447 We appeal to the quadratic representation as in Koecher's
448 Theorem 12 in Chapter III, Section 5.
452 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
457 The inverse in the spin factor algebra is given in Alizadeh's
460 sage: set_random_seed()
461 sage: n = ZZ.random_element(1,10)
462 sage: J = JordanSpinEJA(n)
463 sage: x = J.random_element()
464 sage: while not x.is_invertible():
465 ....: x = J.random_element()
466 sage: x_vec = x.vector()
468 sage: x_bar = x_vec[1:]
469 sage: coeff = ~(x0^2 - x_bar.inner_product(x_bar))
470 sage: inv_vec = x_vec.parent()([x0] + (-x_bar).list())
471 sage: x_inverse = coeff*inv_vec
472 sage: x.inverse() == J(x_inverse)
477 The identity element is its own inverse::
479 sage: set_random_seed()
480 sage: J = random_eja()
481 sage: J.one().inverse() == J.one()
484 If an element has an inverse, it acts like one::
486 sage: set_random_seed()
487 sage: J = random_eja()
488 sage: x = J.random_element()
489 sage: (not x.is_invertible()) or (x.inverse()*x == J.one())
492 The inverse of the inverse is what we started with::
494 sage: set_random_seed()
495 sage: J = random_eja()
496 sage: x = J.random_element()
497 sage: (not x.is_invertible()) or (x.inverse().inverse() == x)
500 The zero element is never invertible::
502 sage: set_random_seed()
503 sage: J = random_eja().zero().inverse()
504 Traceback (most recent call last):
506 ValueError: element is not invertible
509 if not self
.is_invertible():
510 raise ValueError("element is not invertible")
512 return (~self
.quadratic_representation())(self
)
515 def is_invertible(self
):
517 Return whether or not this element is invertible.
521 The usual way to do this is to check if the determinant is
522 zero, but we need the characteristic polynomial for the
523 determinant. The minimal polynomial is a lot easier to get,
524 so we use Corollary 2 in Chapter V of Koecher to check
525 whether or not the paren't algebra's zero element is a root
526 of this element's minimal polynomial.
528 Beware that we can't use the superclass method, because it
529 relies on the algebra being associative.
533 sage: from mjo.eja.eja_algebra import random_eja
537 The identity element is always invertible::
539 sage: set_random_seed()
540 sage: J = random_eja()
541 sage: J.one().is_invertible()
544 The zero element is never invertible::
546 sage: set_random_seed()
547 sage: J = random_eja()
548 sage: J.zero().is_invertible()
552 zero
= self
.parent().zero()
553 p
= self
.minimal_polynomial()
554 return not (p(zero
) == zero
)
557 def is_nilpotent(self
):
559 Return whether or not some power of this element is zero.
563 We use Theorem 5 in Chapter III of Koecher, which says that
564 an element ``x`` is nilpotent if and only if ``x.operator()``
565 is nilpotent. And it is a basic fact of linear algebra that
566 an operator on an `n`-dimensional space is nilpotent if and
567 only if, when raised to the `n`th power, it equals the zero
568 operator (for example, see Axler Corollary 8.8).
572 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
577 sage: J = JordanSpinEJA(3)
578 sage: x = sum(J.gens())
579 sage: x.is_nilpotent()
584 The identity element is never nilpotent::
586 sage: set_random_seed()
587 sage: random_eja().one().is_nilpotent()
590 The additive identity is always nilpotent::
592 sage: set_random_seed()
593 sage: random_eja().zero().is_nilpotent()
598 zero_operator
= P
.zero().operator()
599 return self
.operator()**P
.dimension() == zero_operator
602 def is_regular(self
):
604 Return whether or not this is a regular element.
608 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
613 The identity element always has degree one, but any element
614 linearly-independent from it is regular::
616 sage: J = JordanSpinEJA(5)
617 sage: J.one().is_regular()
619 sage: e0, e1, e2, e3, e4 = J.gens() # e0 is the identity
620 sage: for x in J.gens():
621 ....: (J.one() + x).is_regular()
630 The zero element should never be regular::
632 sage: set_random_seed()
633 sage: J = random_eja()
634 sage: J.zero().is_regular()
637 The unit element isn't regular unless the algebra happens to
638 consist of only its scalar multiples::
640 sage: set_random_seed()
641 sage: J = random_eja()
642 sage: J.dimension() == 1 or not J.one().is_regular()
646 return self
.degree() == self
.parent().rank()
651 Return the degree of this element, which is defined to be
652 the degree of its minimal polynomial.
656 For now, we skip the messy minimal polynomial computation
657 and instead return the dimension of the vector space spanned
658 by the powers of this element. The latter is a bit more
659 straightforward to compute.
663 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
668 sage: J = JordanSpinEJA(4)
669 sage: J.one().degree()
671 sage: e0,e1,e2,e3 = J.gens()
672 sage: (e0 - e1).degree()
675 In the spin factor algebra (of rank two), all elements that
676 aren't multiples of the identity are regular::
678 sage: set_random_seed()
679 sage: n = ZZ.random_element(1,10)
680 sage: J = JordanSpinEJA(n)
681 sage: x = J.random_element()
682 sage: x == x.coefficient(0)*J.one() or x.degree() == 2
687 The zero and unit elements are both of degree one::
689 sage: set_random_seed()
690 sage: J = random_eja()
691 sage: J.zero().degree()
693 sage: J.one().degree()
696 Our implementation agrees with the definition::
698 sage: set_random_seed()
699 sage: x = random_eja().random_element()
700 sage: x.degree() == x.minimal_polynomial().degree()
704 return self
.span_of_powers().dimension()
707 def left_matrix(self
):
709 Our parent class defines ``left_matrix`` and ``matrix``
710 methods whose names are misleading. We don't want them.
712 raise NotImplementedError("use operator().matrix() instead")
717 def minimal_polynomial(self
):
719 Return the minimal polynomial of this element,
720 as a function of the variable `t`.
724 We restrict ourselves to the associative subalgebra
725 generated by this element, and then return the minimal
726 polynomial of this element's operator matrix (in that
727 subalgebra). This works by Baes Proposition 2.3.16.
731 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
736 The minimal polynomial of the identity and zero elements are
739 sage: set_random_seed()
740 sage: J = random_eja()
741 sage: J.one().minimal_polynomial()
743 sage: J.zero().minimal_polynomial()
746 The degree of an element is (by one definition) the degree
747 of its minimal polynomial::
749 sage: set_random_seed()
750 sage: x = random_eja().random_element()
751 sage: x.degree() == x.minimal_polynomial().degree()
754 The minimal polynomial and the characteristic polynomial coincide
755 and are known (see Alizadeh, Example 11.11) for all elements of
756 the spin factor algebra that aren't scalar multiples of the
759 sage: set_random_seed()
760 sage: n = ZZ.random_element(2,10)
761 sage: J = JordanSpinEJA(n)
762 sage: y = J.random_element()
763 sage: while y == y.coefficient(0)*J.one():
764 ....: y = J.random_element()
765 sage: y0 = y.vector()[0]
766 sage: y_bar = y.vector()[1:]
767 sage: actual = y.minimal_polynomial()
768 sage: t = PolynomialRing(J.base_ring(),'t').gen(0)
769 sage: expected = t^2 - 2*y0*t + (y0^2 - norm(y_bar)^2)
770 sage: bool(actual == expected)
773 The minimal polynomial should always kill its element::
775 sage: set_random_seed()
776 sage: x = random_eja().random_element()
777 sage: p = x.minimal_polynomial()
778 sage: x.apply_univariate_polynomial(p)
782 V
= self
.span_of_powers()
783 assoc_subalg
= self
.subalgebra_generated_by()
784 # Mis-design warning: the basis used for span_of_powers()
785 # and subalgebra_generated_by() must be the same, and in
787 elt
= assoc_subalg(V
.coordinates(self
.vector()))
788 return elt
.operator().minimal_polynomial()
792 def natural_representation(self
):
794 Return a more-natural representation of this element.
796 Every finite-dimensional Euclidean Jordan Algebra is a
797 direct sum of five simple algebras, four of which comprise
798 Hermitian matrices. This method returns the original
799 "natural" representation of this element as a Hermitian
800 matrix, if it has one. If not, you get the usual representation.
804 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
805 ....: QuaternionHermitianEJA)
809 sage: J = ComplexHermitianEJA(3)
812 sage: J.one().natural_representation()
822 sage: J = QuaternionHermitianEJA(3)
825 sage: J.one().natural_representation()
826 [1 0 0 0 0 0 0 0 0 0 0 0]
827 [0 1 0 0 0 0 0 0 0 0 0 0]
828 [0 0 1 0 0 0 0 0 0 0 0 0]
829 [0 0 0 1 0 0 0 0 0 0 0 0]
830 [0 0 0 0 1 0 0 0 0 0 0 0]
831 [0 0 0 0 0 1 0 0 0 0 0 0]
832 [0 0 0 0 0 0 1 0 0 0 0 0]
833 [0 0 0 0 0 0 0 1 0 0 0 0]
834 [0 0 0 0 0 0 0 0 1 0 0 0]
835 [0 0 0 0 0 0 0 0 0 1 0 0]
836 [0 0 0 0 0 0 0 0 0 0 1 0]
837 [0 0 0 0 0 0 0 0 0 0 0 1]
840 B
= self
.parent().natural_basis()
841 W
= B
[0].matrix_space()
842 return W
.linear_combination(zip(self
.vector(), B
))
847 Return the left-multiplication-by-this-element
848 operator on the ambient algebra.
852 sage: from mjo.eja.eja_algebra import random_eja
856 sage: set_random_seed()
857 sage: J = random_eja()
858 sage: x = J.random_element()
859 sage: y = J.random_element()
860 sage: x.operator()(y) == x*y
862 sage: y.operator()(x) == x*y
867 fda_elt
= FiniteDimensionalAlgebraElement(P
, self
)
868 return FiniteDimensionalEuclideanJordanAlgebraOperator(
871 fda_elt
.matrix().transpose() )
874 def quadratic_representation(self
, other
=None):
876 Return the quadratic representation of this element.
880 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
885 The explicit form in the spin factor algebra is given by
886 Alizadeh's Example 11.12::
888 sage: set_random_seed()
889 sage: n = ZZ.random_element(1,10)
890 sage: J = JordanSpinEJA(n)
891 sage: x = J.random_element()
892 sage: x_vec = x.vector()
894 sage: x_bar = x_vec[1:]
895 sage: A = matrix(QQ, 1, [x_vec.inner_product(x_vec)])
896 sage: B = 2*x0*x_bar.row()
897 sage: C = 2*x0*x_bar.column()
898 sage: D = matrix.identity(QQ, n-1)
899 sage: D = (x0^2 - x_bar.inner_product(x_bar))*D
900 sage: D = D + 2*x_bar.tensor_product(x_bar)
901 sage: Q = matrix.block(2,2,[A,B,C,D])
902 sage: Q == x.quadratic_representation().matrix()
905 Test all of the properties from Theorem 11.2 in Alizadeh::
907 sage: set_random_seed()
908 sage: J = random_eja()
909 sage: x = J.random_element()
910 sage: y = J.random_element()
911 sage: Lx = x.operator()
912 sage: Lxx = (x*x).operator()
913 sage: Qx = x.quadratic_representation()
914 sage: Qy = y.quadratic_representation()
915 sage: Qxy = x.quadratic_representation(y)
916 sage: Qex = J.one().quadratic_representation(x)
917 sage: n = ZZ.random_element(10)
918 sage: Qxn = (x^n).quadratic_representation()
922 sage: 2*Qxy == (x+y).quadratic_representation() - Qx - Qy
925 Property 2 (multiply on the right for :trac:`28272`):
927 sage: alpha = QQ.random_element()
928 sage: (alpha*x).quadratic_representation() == Qx*(alpha^2)
933 sage: not x.is_invertible() or ( Qx(x.inverse()) == x )
936 sage: not x.is_invertible() or (
939 ....: x.inverse().quadratic_representation() )
942 sage: Qxy(J.one()) == x*y
947 sage: not x.is_invertible() or (
948 ....: x.quadratic_representation(x.inverse())*Qx
949 ....: == Qx*x.quadratic_representation(x.inverse()) )
952 sage: not x.is_invertible() or (
953 ....: x.quadratic_representation(x.inverse())*Qx
955 ....: 2*x.operator()*Qex - Qx )
958 sage: 2*x.operator()*Qex - Qx == Lxx
963 sage: Qy(x).quadratic_representation() == Qy*Qx*Qy
973 sage: not x.is_invertible() or (
974 ....: Qx*x.inverse().operator() == Lx )
979 sage: not x.operator_commutes_with(y) or (
980 ....: Qx(y)^n == Qxn(y^n) )
986 elif not other
in self
.parent():
987 raise TypeError("'other' must live in the same algebra")
991 return ( L
*M
+ M
*L
- (self
*other
).operator() )
994 def span_of_powers(self
):
996 Return the vector space spanned by successive powers of
999 # The dimension of the subalgebra can't be greater than
1000 # the big algebra, so just put everything into a list
1001 # and let span() get rid of the excess.
1003 # We do the extra ambient_vector_space() in case we're messing
1004 # with polynomials and the direct parent is a module.
1005 V
= self
.parent().vector_space()
1006 return V
.span( (self
**d
).vector() for d
in xrange(V
.dimension()) )
1009 def subalgebra_generated_by(self
):
1011 Return the associative subalgebra of the parent EJA generated
1016 sage: from mjo.eja.eja_algebra import random_eja
1020 sage: set_random_seed()
1021 sage: x = random_eja().random_element()
1022 sage: x.subalgebra_generated_by().is_associative()
1025 Squaring in the subalgebra should work the same as in
1028 sage: set_random_seed()
1029 sage: x = random_eja().random_element()
1030 sage: u = x.subalgebra_generated_by().random_element()
1031 sage: u.operator()(u) == u^2
1035 # First get the subspace spanned by the powers of myself...
1036 V
= self
.span_of_powers()
1037 F
= self
.base_ring()
1039 # Now figure out the entries of the right-multiplication
1040 # matrix for the successive basis elements b0, b1,... of
1043 for b_right
in V
.basis():
1044 eja_b_right
= self
.parent()(b_right
)
1046 # The first row of the right-multiplication matrix by
1047 # b1 is what we get if we apply that matrix to b1. The
1048 # second row of the right multiplication matrix by b1
1049 # is what we get when we apply that matrix to b2...
1051 # IMPORTANT: this assumes that all vectors are COLUMN
1052 # vectors, unlike our superclass (which uses row vectors).
1053 for b_left
in V
.basis():
1054 eja_b_left
= self
.parent()(b_left
)
1055 # Multiply in the original EJA, but then get the
1056 # coordinates from the subalgebra in terms of its
1058 this_row
= V
.coordinates((eja_b_left
*eja_b_right
).vector())
1059 b_right_rows
.append(this_row
)
1060 b_right_matrix
= matrix(F
, b_right_rows
)
1061 mats
.append(b_right_matrix
)
1063 # It's an algebra of polynomials in one element, and EJAs
1064 # are power-associative.
1066 # TODO: choose generator names intelligently.
1068 # The rank is the highest possible degree of a minimal polynomial,
1069 # and is bounded above by the dimension. We know in this case that
1070 # there's an element whose minimal polynomial has the same degree
1071 # as the space's dimension, so that must be its rank too.
1072 return FiniteDimensionalEuclideanJordanAlgebra(
1076 assume_associative
=True,
1080 def subalgebra_idempotent(self
):
1082 Find an idempotent in the associative subalgebra I generate
1083 using Proposition 2.3.5 in Baes.
1087 sage: from mjo.eja.eja_algebra import random_eja
1091 sage: set_random_seed()
1092 sage: J = random_eja()
1093 sage: x = J.random_element()
1094 sage: while x.is_nilpotent():
1095 ....: x = J.random_element()
1096 sage: c = x.subalgebra_idempotent()
1101 if self
.is_nilpotent():
1102 raise ValueError("this only works with non-nilpotent elements!")
1104 V
= self
.span_of_powers()
1105 J
= self
.subalgebra_generated_by()
1106 # Mis-design warning: the basis used for span_of_powers()
1107 # and subalgebra_generated_by() must be the same, and in
1109 u
= J(V
.coordinates(self
.vector()))
1111 # The image of the matrix of left-u^m-multiplication
1112 # will be minimal for some natural number s...
1114 minimal_dim
= V
.dimension()
1115 for i
in xrange(1, V
.dimension()):
1116 this_dim
= (u
**i
).operator().matrix().image().dimension()
1117 if this_dim
< minimal_dim
:
1118 minimal_dim
= this_dim
1121 # Now minimal_matrix should correspond to the smallest
1122 # non-zero subspace in Baes's (or really, Koecher's)
1125 # However, we need to restrict the matrix to work on the
1126 # subspace... or do we? Can't we just solve, knowing that
1127 # A(c) = u^(s+1) should have a solution in the big space,
1130 # Beware, solve_right() means that we're using COLUMN vectors.
1131 # Our FiniteDimensionalAlgebraElement superclass uses rows.
1133 A
= u_next
.operator().matrix()
1134 c_coordinates
= A
.solve_right(u_next
.vector())
1136 # Now c_coordinates is the idempotent we want, but it's in
1137 # the coordinate system of the subalgebra.
1139 # We need the basis for J, but as elements of the parent algebra.
1141 basis
= [self
.parent(v
) for v
in V
.basis()]
1142 return self
.parent().linear_combination(zip(c_coordinates
, basis
))
1147 Return my trace, the sum of my eigenvalues.
1151 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
1152 ....: RealCartesianProductEJA,
1157 sage: J = JordanSpinEJA(3)
1158 sage: x = sum(J.gens())
1164 sage: J = RealCartesianProductEJA(5)
1165 sage: J.one().trace()
1170 The trace of an element is a real number::
1172 sage: set_random_seed()
1173 sage: J = random_eja()
1174 sage: J.random_element().trace() in J.base_ring()
1180 p
= P
._charpoly
_coeff
(r
-1)
1181 # The _charpoly_coeff function already adds the factor of
1182 # -1 to ensure that _charpoly_coeff(r-1) is really what
1183 # appears in front of t^{r-1} in the charpoly. However,
1184 # we want the negative of THAT for the trace.
1185 return -p(*self
.vector())
1188 def trace_inner_product(self
, other
):
1190 Return the trace inner product of myself and ``other``.
1194 sage: from mjo.eja.eja_algebra import random_eja
1198 The trace inner product is commutative::
1200 sage: set_random_seed()
1201 sage: J = random_eja()
1202 sage: x = J.random_element(); y = J.random_element()
1203 sage: x.trace_inner_product(y) == y.trace_inner_product(x)
1206 The trace inner product is bilinear::
1208 sage: set_random_seed()
1209 sage: J = random_eja()
1210 sage: x = J.random_element()
1211 sage: y = J.random_element()
1212 sage: z = J.random_element()
1213 sage: a = QQ.random_element();
1214 sage: actual = (a*(x+z)).trace_inner_product(y)
1215 sage: expected = ( a*x.trace_inner_product(y) +
1216 ....: a*z.trace_inner_product(y) )
1217 sage: actual == expected
1219 sage: actual = x.trace_inner_product(a*(y+z))
1220 sage: expected = ( a*x.trace_inner_product(y) +
1221 ....: a*x.trace_inner_product(z) )
1222 sage: actual == expected
1225 The trace inner product satisfies the compatibility
1226 condition in the definition of a Euclidean Jordan algebra::
1228 sage: set_random_seed()
1229 sage: J = random_eja()
1230 sage: x = J.random_element()
1231 sage: y = J.random_element()
1232 sage: z = J.random_element()
1233 sage: (x*y).trace_inner_product(z) == y.trace_inner_product(x*z)
1237 if not other
in self
.parent():
1238 raise TypeError("'other' must live in the same algebra")
1240 return (self
*other
).trace()