2 Euclidean Jordan Algebras. These are formally-real Jordan Algebras;
3 specifically those where u^2 + v^2 = 0 implies that u = v = 0. They
4 are used in optimization, and have some additional nice methods beyond
5 what can be supported in a general Jordan Algebra.
8 from sage
.categories
.magmatic_algebras
import MagmaticAlgebras
9 from sage
.structure
.element
import is_Matrix
10 from sage
.structure
.category_object
import normalize_names
12 from sage
.algebras
.finite_dimensional_algebras
.finite_dimensional_algebra
import FiniteDimensionalAlgebra
13 from sage
.algebras
.finite_dimensional_algebras
.finite_dimensional_algebra_element
import FiniteDimensionalAlgebraElement
15 class FiniteDimensionalEuclideanJordanAlgebra(FiniteDimensionalAlgebra
):
17 def __classcall_private__(cls
,
21 assume_associative
=False,
25 mult_table
= [b
.base_extend(field
) for b
in mult_table
]
28 if not (is_Matrix(b
) and b
.dimensions() == (n
, n
)):
29 raise ValueError("input is not a multiplication table")
30 mult_table
= tuple(mult_table
)
32 cat
= MagmaticAlgebras(field
).FiniteDimensional().WithBasis()
33 cat
.or_subcategory(category
)
34 if assume_associative
:
35 cat
= cat
.Associative()
37 names
= normalize_names(n
, names
)
39 fda
= super(FiniteDimensionalEuclideanJordanAlgebra
, cls
)
40 return fda
.__classcall
__(cls
,
43 assume_associative
=assume_associative
,
49 def __init__(self
, field
,
52 assume_associative
=False,
56 fda
= super(FiniteDimensionalEuclideanJordanAlgebra
, self
)
65 Return a string representation of ``self``.
67 fmt
= "Euclidean Jordan algebra of degree {} over {}"
68 return fmt
.format(self
.degree(), self
.base_ring())
72 Return the rank of this EJA.
74 if self
._rank
is None:
75 raise ValueError("no rank specified at genesis")
80 class Element(FiniteDimensionalAlgebraElement
):
82 An element of a Euclidean Jordan algebra.
84 Since EJAs are commutative, the "right multiplication" matrix is
85 also the left multiplication matrix and must be symmetric::
87 sage: set_random_seed()
88 sage: n = ZZ.random_element(1,10).abs()
90 sage: J.random_element().matrix().is_symmetric()
93 sage: J.random_element().matrix().is_symmetric()
100 Return ``self`` raised to the power ``n``.
102 Jordan algebras are always power-associative; see for
103 example Faraut and Koranyi, Proposition II.1.2 (ii).
107 We have to override this because our superclass uses row vectors
108 instead of column vectors! We, on the other hand, assume column
113 sage: set_random_seed()
115 sage: x = J.random_element()
116 sage: x.matrix()*x.vector() == (x**2).vector()
126 return A
.element_class(A
, (self
.matrix()**(n
-1))*self
.vector())
129 def characteristic_polynomial(self
):
131 Return my characteristic polynomial (if I'm a regular
134 Eventually this should be implemented in terms of the parent
135 algebra's characteristic polynomial that works for ALL
138 if self
.is_regular():
139 return self
.minimal_polynomial()
141 raise NotImplementedError('irregular element')
146 Return my determinant, the product of my eigenvalues.
151 sage: e0,e1 = J.gens()
156 sage: e0,e1,e2 = J.gens()
157 sage: x = e0 + e1 + e2
162 cs
= self
.characteristic_polynomial().coefficients(sparse
=False)
165 return cs
[0] * (-1)**r
167 raise ValueError('charpoly had no coefficients')
170 def is_nilpotent(self
):
172 Return whether or not some power of this element is zero.
174 The superclass method won't work unless we're in an
175 associative algebra, and we aren't. However, we generate
176 an assocoative subalgebra and we're nilpotent there if and
177 only if we're nilpotent here (probably).
181 The identity element is never nilpotent::
183 sage: set_random_seed()
184 sage: n = ZZ.random_element(2,10).abs()
186 sage: J.one().is_nilpotent()
189 sage: J.one().is_nilpotent()
192 The additive identity is always nilpotent::
194 sage: set_random_seed()
195 sage: n = ZZ.random_element(2,10).abs()
197 sage: J.zero().is_nilpotent()
200 sage: J.zero().is_nilpotent()
204 # The element we're going to call "is_nilpotent()" on.
205 # Either myself, interpreted as an element of a finite-
206 # dimensional algebra, or an element of an associative
210 if self
.parent().is_associative():
211 elt
= FiniteDimensionalAlgebraElement(self
.parent(), self
)
213 V
= self
.span_of_powers()
214 assoc_subalg
= self
.subalgebra_generated_by()
215 # Mis-design warning: the basis used for span_of_powers()
216 # and subalgebra_generated_by() must be the same, and in
218 elt
= assoc_subalg(V
.coordinates(self
.vector()))
220 # Recursive call, but should work since elt lives in an
221 # associative algebra.
222 return elt
.is_nilpotent()
225 def is_regular(self
):
227 Return whether or not this is a regular element.
231 The identity element always has degree one, but any element
232 linearly-independent from it is regular::
235 sage: J.one().is_regular()
237 sage: e0, e1, e2, e3, e4 = J.gens() # e0 is the identity
238 sage: for x in J.gens():
239 ....: (J.one() + x).is_regular()
247 return self
.degree() == self
.parent().rank()
252 Compute the degree of this element the straightforward way
253 according to the definition; by appending powers to a list
254 and figuring out its dimension (that is, whether or not
255 they're linearly dependent).
260 sage: J.one().degree()
262 sage: e0,e1,e2,e3 = J.gens()
263 sage: (e0 - e1).degree()
266 In the spin factor algebra (of rank two), all elements that
267 aren't multiples of the identity are regular::
269 sage: set_random_seed()
270 sage: n = ZZ.random_element(1,10).abs()
272 sage: x = J.random_element()
273 sage: x == x.coefficient(0)*J.one() or x.degree() == 2
277 return self
.span_of_powers().dimension()
282 Return the matrix that represents left- (or right-)
283 multiplication by this element in the parent algebra.
285 We have to override this because the superclass method
286 returns a matrix that acts on row vectors (that is, on
289 fda_elt
= FiniteDimensionalAlgebraElement(self
.parent(), self
)
290 return fda_elt
.matrix().transpose()
293 def minimal_polynomial(self
):
297 sage: set_random_seed()
298 sage: n = ZZ.random_element(1,10).abs()
300 sage: x = J.random_element()
301 sage: x.degree() == x.minimal_polynomial().degree()
306 sage: set_random_seed()
307 sage: n = ZZ.random_element(1,10).abs()
309 sage: x = J.random_element()
310 sage: x.degree() == x.minimal_polynomial().degree()
313 The minimal polynomial and the characteristic polynomial coincide
314 and are known (see Alizadeh, Example 11.11) for all elements of
315 the spin factor algebra that aren't scalar multiples of the
318 sage: set_random_seed()
319 sage: n = ZZ.random_element(2,10).abs()
321 sage: y = J.random_element()
322 sage: while y == y.coefficient(0)*J.one():
323 ....: y = J.random_element()
324 sage: y0 = y.vector()[0]
325 sage: y_bar = y.vector()[1:]
326 sage: actual = y.minimal_polynomial()
327 sage: x = SR.symbol('x', domain='real')
328 sage: expected = x^2 - 2*y0*x + (y0^2 - norm(y_bar)^2)
329 sage: bool(actual == expected)
333 # The element we're going to call "minimal_polynomial()" on.
334 # Either myself, interpreted as an element of a finite-
335 # dimensional algebra, or an element of an associative
339 if self
.parent().is_associative():
340 elt
= FiniteDimensionalAlgebraElement(self
.parent(), self
)
342 V
= self
.span_of_powers()
343 assoc_subalg
= self
.subalgebra_generated_by()
344 # Mis-design warning: the basis used for span_of_powers()
345 # and subalgebra_generated_by() must be the same, and in
347 elt
= assoc_subalg(V
.coordinates(self
.vector()))
349 # Recursive call, but should work since elt lives in an
350 # associative algebra.
351 return elt
.minimal_polynomial()
354 def span_of_powers(self
):
356 Return the vector space spanned by successive powers of
359 # The dimension of the subalgebra can't be greater than
360 # the big algebra, so just put everything into a list
361 # and let span() get rid of the excess.
362 V
= self
.vector().parent()
363 return V
.span( (self
**d
).vector() for d
in xrange(V
.dimension()) )
366 def subalgebra_generated_by(self
):
368 Return the associative subalgebra of the parent EJA generated
373 sage: set_random_seed()
374 sage: n = ZZ.random_element(1,10).abs()
376 sage: x = J.random_element()
377 sage: x.subalgebra_generated_by().is_associative()
380 sage: x = J.random_element()
381 sage: x.subalgebra_generated_by().is_associative()
384 Squaring in the subalgebra should be the same thing as
385 squaring in the superalgebra::
388 sage: x = J.random_element()
389 sage: u = x.subalgebra_generated_by().random_element()
390 sage: u.matrix()*u.vector() == (u**2).vector()
394 # First get the subspace spanned by the powers of myself...
395 V
= self
.span_of_powers()
398 # Now figure out the entries of the right-multiplication
399 # matrix for the successive basis elements b0, b1,... of
402 for b_right
in V
.basis():
403 eja_b_right
= self
.parent()(b_right
)
405 # The first row of the right-multiplication matrix by
406 # b1 is what we get if we apply that matrix to b1. The
407 # second row of the right multiplication matrix by b1
408 # is what we get when we apply that matrix to b2...
410 # IMPORTANT: this assumes that all vectors are COLUMN
411 # vectors, unlike our superclass (which uses row vectors).
412 for b_left
in V
.basis():
413 eja_b_left
= self
.parent()(b_left
)
414 # Multiply in the original EJA, but then get the
415 # coordinates from the subalgebra in terms of its
417 this_row
= V
.coordinates((eja_b_left
*eja_b_right
).vector())
418 b_right_rows
.append(this_row
)
419 b_right_matrix
= matrix(F
, b_right_rows
)
420 mats
.append(b_right_matrix
)
422 # It's an algebra of polynomials in one element, and EJAs
423 # are power-associative.
425 # TODO: choose generator names intelligently.
426 return FiniteDimensionalEuclideanJordanAlgebra(F
, mats
, assume_associative
=True, names
='f')
429 def subalgebra_idempotent(self
):
431 Find an idempotent in the associative subalgebra I generate
432 using Proposition 2.3.5 in Baes.
436 sage: set_random_seed()
438 sage: c = J.random_element().subalgebra_idempotent()
442 sage: c = J.random_element().subalgebra_idempotent()
447 if self
.is_nilpotent():
448 raise ValueError("this only works with non-nilpotent elements!")
450 V
= self
.span_of_powers()
451 J
= self
.subalgebra_generated_by()
452 # Mis-design warning: the basis used for span_of_powers()
453 # and subalgebra_generated_by() must be the same, and in
455 u
= J(V
.coordinates(self
.vector()))
457 # The image of the matrix of left-u^m-multiplication
458 # will be minimal for some natural number s...
460 minimal_dim
= V
.dimension()
461 for i
in xrange(1, V
.dimension()):
462 this_dim
= (u
**i
).matrix().image().dimension()
463 if this_dim
< minimal_dim
:
464 minimal_dim
= this_dim
467 # Now minimal_matrix should correspond to the smallest
468 # non-zero subspace in Baes's (or really, Koecher's)
471 # However, we need to restrict the matrix to work on the
472 # subspace... or do we? Can't we just solve, knowing that
473 # A(c) = u^(s+1) should have a solution in the big space,
476 # Beware, solve_right() means that we're using COLUMN vectors.
477 # Our FiniteDimensionalAlgebraElement superclass uses rows.
480 c_coordinates
= A
.solve_right(u_next
.vector())
482 # Now c_coordinates is the idempotent we want, but it's in
483 # the coordinate system of the subalgebra.
485 # We need the basis for J, but as elements of the parent algebra.
487 basis
= [self
.parent(v
) for v
in V
.basis()]
488 return self
.parent().linear_combination(zip(c_coordinates
, basis
))
493 Return my trace, the sum of my eigenvalues.
498 sage: e0,e1,e2 = J.gens()
499 sage: x = e0 + e1 + e2
504 cs
= self
.characteristic_polynomial().coefficients(sparse
=False)
508 raise ValueError('charpoly had fewer than 2 coefficients')
511 def eja_rn(dimension
, field
=QQ
):
513 Return the Euclidean Jordan Algebra corresponding to the set
514 `R^n` under the Hadamard product.
518 This multiplication table can be verified by hand::
521 sage: e0,e1,e2 = J.gens()
536 # The FiniteDimensionalAlgebra constructor takes a list of
537 # matrices, the ith representing right multiplication by the ith
538 # basis element in the vector space. So if e_1 = (1,0,0), then
539 # right (Hadamard) multiplication of x by e_1 picks out the first
540 # component of x; and likewise for the ith basis element e_i.
541 Qs
= [ matrix(field
, dimension
, dimension
, lambda k
,j
: 1*(k
== j
== i
))
542 for i
in xrange(dimension
) ]
544 return FiniteDimensionalEuclideanJordanAlgebra(field
,Qs
,rank
=dimension
)
547 def eja_ln(dimension
, field
=QQ
):
549 Return the Jordan algebra corresponding to the Lorentz "ice cream"
550 cone of the given ``dimension``.
554 This multiplication table can be verified by hand::
557 sage: e0,e1,e2,e3 = J.gens()
573 In one dimension, this is the reals under multiplication::
582 id_matrix
= identity_matrix(field
,dimension
)
583 for i
in xrange(dimension
):
584 ei
= id_matrix
.column(i
)
585 Qi
= zero_matrix(field
,dimension
)
588 Qi
+= diagonal_matrix(dimension
, [ei
[0]]*dimension
)
589 # The addition of the diagonal matrix adds an extra ei[0] in the
590 # upper-left corner of the matrix.
591 Qi
[0,0] = Qi
[0,0] * ~
field(2)
594 # The rank of the spin factor algebra is two, UNLESS we're in a
595 # one-dimensional ambient space (the rank is bounded by the
596 # ambient dimension).
597 rank
= min(dimension
,2)
598 return FiniteDimensionalEuclideanJordanAlgebra(field
,Qs
,rank
=rank
)
601 def eja_sn(dimension
, field
=QQ
):
603 Return the simple Jordan algebra of ``dimension``-by-``dimension``
604 symmetric matrices over ``field``.
609 sage: e0, e1, e2 = J.gens()
620 # In S^2, for example, we nominally have four coordinates even
621 # though the space is of dimension three only. The vector space V
622 # is supposed to hold the entire long vector, and the subspace W
623 # of V will be spanned by the vectors that arise from symmetric
624 # matrices. Thus for S^2, dim(V) == 4 and dim(W) == 3.
625 V
= VectorSpace(field
, dimension
**2)
627 # The basis of symmetric matrices, as matrices, in their R^(n-by-n)
631 for i
in xrange(dimension
):
632 for j
in xrange(i
+1):
633 Eij
= matrix(field
, dimension
, lambda k
,l
: k
==i
and l
==j
)
637 Sij
= Eij
+ Eij
.transpose()
641 return vector(field
, m
.list())
643 W
= V
.span( mat2vec(s
) for s
in S
)
646 # Brute force the right-multiplication-by-s matrix by looping
647 # through all elements of the basis and doing the computation
648 # to find out what the corresponding row should be.
651 this_row
= mat2vec((s
*t
+ t
*s
)/2)
652 Q_rows
.append(W
.coordinates(this_row
))
653 Q
= matrix(field
,Q_rows
)
656 return FiniteDimensionalEuclideanJordanAlgebra(field
,Qs
,rank
=dimension
)