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::
387 sage: set_random_seed()
389 sage: x = J.random_element()
390 sage: u = x.subalgebra_generated_by().random_element()
391 sage: u.matrix()*u.vector() == (u**2).vector()
395 # First get the subspace spanned by the powers of myself...
396 V
= self
.span_of_powers()
399 # Now figure out the entries of the right-multiplication
400 # matrix for the successive basis elements b0, b1,... of
403 for b_right
in V
.basis():
404 eja_b_right
= self
.parent()(b_right
)
406 # The first row of the right-multiplication matrix by
407 # b1 is what we get if we apply that matrix to b1. The
408 # second row of the right multiplication matrix by b1
409 # is what we get when we apply that matrix to b2...
411 # IMPORTANT: this assumes that all vectors are COLUMN
412 # vectors, unlike our superclass (which uses row vectors).
413 for b_left
in V
.basis():
414 eja_b_left
= self
.parent()(b_left
)
415 # Multiply in the original EJA, but then get the
416 # coordinates from the subalgebra in terms of its
418 this_row
= V
.coordinates((eja_b_left
*eja_b_right
).vector())
419 b_right_rows
.append(this_row
)
420 b_right_matrix
= matrix(F
, b_right_rows
)
421 mats
.append(b_right_matrix
)
423 # It's an algebra of polynomials in one element, and EJAs
424 # are power-associative.
426 # TODO: choose generator names intelligently.
427 return FiniteDimensionalEuclideanJordanAlgebra(F
, mats
, assume_associative
=True, names
='f')
430 def subalgebra_idempotent(self
):
432 Find an idempotent in the associative subalgebra I generate
433 using Proposition 2.3.5 in Baes.
437 sage: set_random_seed()
439 sage: c = J.random_element().subalgebra_idempotent()
443 sage: c = J.random_element().subalgebra_idempotent()
448 if self
.is_nilpotent():
449 raise ValueError("this only works with non-nilpotent elements!")
451 V
= self
.span_of_powers()
452 J
= self
.subalgebra_generated_by()
453 # Mis-design warning: the basis used for span_of_powers()
454 # and subalgebra_generated_by() must be the same, and in
456 u
= J(V
.coordinates(self
.vector()))
458 # The image of the matrix of left-u^m-multiplication
459 # will be minimal for some natural number s...
461 minimal_dim
= V
.dimension()
462 for i
in xrange(1, V
.dimension()):
463 this_dim
= (u
**i
).matrix().image().dimension()
464 if this_dim
< minimal_dim
:
465 minimal_dim
= this_dim
468 # Now minimal_matrix should correspond to the smallest
469 # non-zero subspace in Baes's (or really, Koecher's)
472 # However, we need to restrict the matrix to work on the
473 # subspace... or do we? Can't we just solve, knowing that
474 # A(c) = u^(s+1) should have a solution in the big space,
477 # Beware, solve_right() means that we're using COLUMN vectors.
478 # Our FiniteDimensionalAlgebraElement superclass uses rows.
481 c_coordinates
= A
.solve_right(u_next
.vector())
483 # Now c_coordinates is the idempotent we want, but it's in
484 # the coordinate system of the subalgebra.
486 # We need the basis for J, but as elements of the parent algebra.
488 basis
= [self
.parent(v
) for v
in V
.basis()]
489 return self
.parent().linear_combination(zip(c_coordinates
, basis
))
494 Return my trace, the sum of my eigenvalues.
499 sage: e0,e1,e2 = J.gens()
500 sage: x = e0 + e1 + e2
505 cs
= self
.characteristic_polynomial().coefficients(sparse
=False)
509 raise ValueError('charpoly had fewer than 2 coefficients')
512 def eja_rn(dimension
, field
=QQ
):
514 Return the Euclidean Jordan Algebra corresponding to the set
515 `R^n` under the Hadamard product.
519 This multiplication table can be verified by hand::
522 sage: e0,e1,e2 = J.gens()
537 # The FiniteDimensionalAlgebra constructor takes a list of
538 # matrices, the ith representing right multiplication by the ith
539 # basis element in the vector space. So if e_1 = (1,0,0), then
540 # right (Hadamard) multiplication of x by e_1 picks out the first
541 # component of x; and likewise for the ith basis element e_i.
542 Qs
= [ matrix(field
, dimension
, dimension
, lambda k
,j
: 1*(k
== j
== i
))
543 for i
in xrange(dimension
) ]
545 return FiniteDimensionalEuclideanJordanAlgebra(field
,Qs
,rank
=dimension
)
548 def eja_ln(dimension
, field
=QQ
):
550 Return the Jordan algebra corresponding to the Lorentz "ice cream"
551 cone of the given ``dimension``.
555 This multiplication table can be verified by hand::
558 sage: e0,e1,e2,e3 = J.gens()
574 In one dimension, this is the reals under multiplication::
583 id_matrix
= identity_matrix(field
,dimension
)
584 for i
in xrange(dimension
):
585 ei
= id_matrix
.column(i
)
586 Qi
= zero_matrix(field
,dimension
)
589 Qi
+= diagonal_matrix(dimension
, [ei
[0]]*dimension
)
590 # The addition of the diagonal matrix adds an extra ei[0] in the
591 # upper-left corner of the matrix.
592 Qi
[0,0] = Qi
[0,0] * ~
field(2)
595 # The rank of the spin factor algebra is two, UNLESS we're in a
596 # one-dimensional ambient space (the rank is bounded by the
597 # ambient dimension).
598 rank
= min(dimension
,2)
599 return FiniteDimensionalEuclideanJordanAlgebra(field
,Qs
,rank
=rank
)
602 def eja_sn(dimension
, field
=QQ
):
604 Return the simple Jordan algebra of ``dimension``-by-``dimension``
605 symmetric matrices over ``field``.
610 sage: e0, e1, e2 = J.gens()
621 # In S^2, for example, we nominally have four coordinates even
622 # though the space is of dimension three only. The vector space V
623 # is supposed to hold the entire long vector, and the subspace W
624 # of V will be spanned by the vectors that arise from symmetric
625 # matrices. Thus for S^2, dim(V) == 4 and dim(W) == 3.
626 V
= VectorSpace(field
, dimension
**2)
628 # The basis of symmetric matrices, as matrices, in their R^(n-by-n)
632 for i
in xrange(dimension
):
633 for j
in xrange(i
+1):
634 Eij
= matrix(field
, dimension
, lambda k
,l
: k
==i
and l
==j
)
638 Sij
= Eij
+ Eij
.transpose()
642 return vector(field
, m
.list())
644 W
= V
.span( mat2vec(s
) for s
in S
)
647 # Brute force the multiplication-by-s matrix by looping
648 # through all elements of the basis and doing the computation
649 # to find out what the corresponding row should be. BEWARE:
650 # these multiplication tables won't be symmetric! It therefore
651 # becomes REALLY IMPORTANT that the underlying algebra
652 # constructor uses ROW vectors and not COLUMN vectors. That's
653 # why we're computing rows here and not columns.
656 this_row
= mat2vec((s
*t
+ t
*s
)/2)
657 Q_rows
.append(W
.coordinates(this_row
))
658 Q
= matrix(field
,Q_rows
)
661 return FiniteDimensionalEuclideanJordanAlgebra(field
,Qs
,rank
=dimension
)
666 Return a "random" finite-dimensional Euclidean Jordan Algebra.
670 For now, we choose a random natural number ``n`` (greater than zero)
671 and then give you back one of the following:
673 * The cartesian product of the rational numbers ``n`` times; this is
674 ``QQ^n`` with the Hadamard product.
676 * The Jordan spin algebra on ``QQ^n``.
678 * The ``n``-by-``n`` rational symmetric matrices with the symmetric
681 Later this might be extended to return Cartesian products of the
687 Euclidean Jordan algebra of degree...
690 n
= ZZ
.random_element(1,10).abs()
691 constructor
= choice([eja_rn
, eja_ln
, eja_sn
])
692 return constructor(dimension
=n
, field
=QQ
)