2 Representations and constructions for Euclidean Jordan algebras.
4 A Euclidean Jordan algebra is a Jordan algebra that has some
7 1. It is finite-dimensional.
8 2. Its scalar field is the real numbers.
9 3a. An inner product is defined on it, and...
10 3b. That inner product is compatible with the Jordan product
11 in the sense that `<x*y,z> = <y,x*z>` for all elements
12 `x,y,z` in the algebra.
14 Every Euclidean Jordan algebra is formally-real: for any two elements
15 `x` and `y` in the algebra, `x^{2} + y^{2} = 0` implies that `x = y =
16 0`. Conversely, every finite-dimensional formally-real Jordan algebra
17 can be made into a Euclidean Jordan algebra with an appropriate choice
20 Formally-real Jordan algebras were originally studied as a framework
21 for quantum mechanics. Today, Euclidean Jordan algebras are crucial in
22 symmetric cone optimization, since every symmetric cone arises as the
23 cone of squares in some Euclidean Jordan algebra.
25 It is known that every Euclidean Jordan algebra decomposes into an
26 orthogonal direct sum (essentially, a Cartesian product) of simple
27 algebras, and that moreover, up to Jordan-algebra isomorphism, there
28 are only five families of simple algebras. We provide constructions
29 for these simple algebras:
31 * :class:`BilinearFormEJA`
32 * :class:`RealSymmetricEJA`
33 * :class:`ComplexHermitianEJA`
34 * :class:`QuaternionHermitianEJA`
35 * :class:`OctonionHermitianEJA`
37 In addition to these, we provide two other example constructions,
39 * :class:`JordanSpinEJA`
40 * :class:`HadamardEJA`
44 The Jordan spin algebra is a bilinear form algebra where the bilinear
45 form is the identity. The Hadamard EJA is simply a Cartesian product
46 of one-dimensional spin algebras. The Albert EJA is simply a special
47 case of the :class:`OctonionHermitianEJA` where the matrices are
48 three-by-three and the resulting space has dimension 27. And
49 last/least, the trivial EJA is exactly what you think it is; it could
50 also be obtained by constructing a dimension-zero instance of any of
51 the other algebras. Cartesian products of these are also supported
52 using the usual ``cartesian_product()`` function; as a result, we
53 support (up to isomorphism) all Euclidean Jordan algebras.
57 sage: from mjo.eja.eja_algebra import random_eja
62 Euclidean Jordan algebra of dimension...
65 from sage
.algebras
.quatalg
.quaternion_algebra
import QuaternionAlgebra
66 from sage
.categories
.magmatic_algebras
import MagmaticAlgebras
67 from sage
.categories
.sets_cat
import cartesian_product
68 from sage
.combinat
.free_module
import CombinatorialFreeModule
69 from sage
.matrix
.constructor
import matrix
70 from sage
.matrix
.matrix_space
import MatrixSpace
71 from sage
.misc
.cachefunc
import cached_method
72 from sage
.misc
.table
import table
73 from sage
.modules
.free_module
import FreeModule
, VectorSpace
74 from sage
.rings
.all
import (ZZ
, QQ
, AA
, QQbar
, RR
, RLF
, CLF
,
77 from mjo
.eja
.eja_element
import FiniteDimensionalEJAElement
78 from mjo
.eja
.eja_operator
import FiniteDimensionalEJAOperator
79 from mjo
.eja
.eja_utils
import _all2list
, _mat2vec
81 class FiniteDimensionalEJA(CombinatorialFreeModule
):
83 A finite-dimensional Euclidean Jordan algebra.
87 - ``basis`` -- a tuple; a tuple of basis elements in "matrix
88 form," which must be the same form as the arguments to
89 ``jordan_product`` and ``inner_product``. In reality, "matrix
90 form" can be either vectors, matrices, or a Cartesian product
91 (ordered tuple) of vectors or matrices. All of these would
92 ideally be vector spaces in sage with no special-casing
93 needed; but in reality we turn vectors into column-matrices
94 and Cartesian products `(a,b)` into column matrices
95 `(a,b)^{T}` after converting `a` and `b` themselves.
97 - ``jordan_product`` -- a function; afunction of two ``basis``
98 elements (in matrix form) that returns their jordan product,
99 also in matrix form; this will be applied to ``basis`` to
100 compute a multiplication table for the algebra.
102 - ``inner_product`` -- a function; a function of two ``basis``
103 elements (in matrix form) that returns their inner
104 product. This will be applied to ``basis`` to compute an
105 inner-product table (basically a matrix) for this algebra.
107 - ``field`` -- a subfield of the reals (default: ``AA``); the scalar
108 field for the algebra.
110 - ``orthonormalize`` -- boolean (default: ``True``); whether or
111 not to orthonormalize the basis. Doing so is expensive and
112 generally rules out using the rationals as your ``field``, but
113 is required for spectral decompositions.
117 sage: from mjo.eja.eja_algebra import random_eja
121 We should compute that an element subalgebra is associative even
122 if we circumvent the element method::
124 sage: set_random_seed()
125 sage: J = random_eja(field=QQ,orthonormalize=False)
126 sage: x = J.random_element()
127 sage: A = x.subalgebra_generated_by(orthonormalize=False)
128 sage: basis = tuple(b.superalgebra_element() for b in A.basis())
129 sage: J.subalgebra(basis, orthonormalize=False).is_associative()
133 Element
= FiniteDimensionalEJAElement
142 cartesian_product
=False,
150 if not field
.is_subring(RR
):
151 # Note: this does return true for the real algebraic
152 # field, the rationals, and any quadratic field where
153 # we've specified a real embedding.
154 raise ValueError("scalar field is not real")
157 # Check commutativity of the Jordan and inner-products.
158 # This has to be done before we build the multiplication
159 # and inner-product tables/matrices, because we take
160 # advantage of symmetry in the process.
161 if not all( jordan_product(bi
,bj
) == jordan_product(bj
,bi
)
164 raise ValueError("Jordan product is not commutative")
166 if not all( inner_product(bi
,bj
) == inner_product(bj
,bi
)
169 raise ValueError("inner-product is not commutative")
172 category
= MagmaticAlgebras(field
).FiniteDimensional()
173 category
= category
.WithBasis().Unital().Commutative()
175 if associative
is None:
176 # We should figure it out. As with check_axioms, we have to do
177 # this without the help of the _jordan_product_is_associative()
178 # method because we need to know the category before we
179 # initialize the algebra.
180 associative
= all( jordan_product(jordan_product(bi
,bj
),bk
)
182 jordan_product(bi
,jordan_product(bj
,bk
))
188 # Element subalgebras can take advantage of this.
189 category
= category
.Associative()
190 if cartesian_product
:
191 # Use join() here because otherwise we only get the
192 # "Cartesian product of..." and not the things themselves.
193 category
= category
.join([category
,
194 category
.CartesianProducts()])
196 # Call the superclass constructor so that we can use its from_vector()
197 # method to build our multiplication table.
198 CombinatorialFreeModule
.__init
__(self
,
205 # Now comes all of the hard work. We'll be constructing an
206 # ambient vector space V that our (vectorized) basis lives in,
207 # as well as a subspace W of V spanned by those (vectorized)
208 # basis elements. The W-coordinates are the coefficients that
209 # we see in things like x = 1*b1 + 2*b2.
214 degree
= len(_all2list(basis
[0]))
216 # Build an ambient space that fits our matrix basis when
217 # written out as "long vectors."
218 V
= VectorSpace(field
, degree
)
220 # The matrix that will hole the orthonormal -> unorthonormal
221 # coordinate transformation.
222 self
._deortho
_matrix
= None
225 # Save a copy of the un-orthonormalized basis for later.
226 # Convert it to ambient V (vector) coordinates while we're
227 # at it, because we'd have to do it later anyway.
228 deortho_vector_basis
= tuple( V(_all2list(b
)) for b
in basis
)
230 from mjo
.eja
.eja_utils
import gram_schmidt
231 basis
= tuple(gram_schmidt(basis
, inner_product
))
233 # Save the (possibly orthonormalized) matrix basis for
235 self
._matrix
_basis
= basis
237 # Now create the vector space for the algebra, which will have
238 # its own set of non-ambient coordinates (in terms of the
240 vector_basis
= tuple( V(_all2list(b
)) for b
in basis
)
241 W
= V
.span_of_basis( vector_basis
, check
=check_axioms
)
244 # Now "W" is the vector space of our algebra coordinates. The
245 # variables "X1", "X2",... refer to the entries of vectors in
246 # W. Thus to convert back and forth between the orthonormal
247 # coordinates and the given ones, we need to stick the original
249 U
= V
.span_of_basis( deortho_vector_basis
, check
=check_axioms
)
250 self
._deortho
_matrix
= matrix( U
.coordinate_vector(q
)
251 for q
in vector_basis
)
254 # Now we actually compute the multiplication and inner-product
255 # tables/matrices using the possibly-orthonormalized basis.
256 self
._inner
_product
_matrix
= matrix
.identity(field
, n
)
257 self
._multiplication
_table
= [ [0 for j
in range(i
+1)]
260 # Note: the Jordan and inner-products are defined in terms
261 # of the ambient basis. It's important that their arguments
262 # are in ambient coordinates as well.
265 # ortho basis w.r.t. ambient coords
269 # The jordan product returns a matrixy answer, so we
270 # have to convert it to the algebra coordinates.
271 elt
= jordan_product(q_i
, q_j
)
272 elt
= W
.coordinate_vector(V(_all2list(elt
)))
273 self
._multiplication
_table
[i
][j
] = self
.from_vector(elt
)
275 if not orthonormalize
:
276 # If we're orthonormalizing the basis with respect
277 # to an inner-product, then the inner-product
278 # matrix with respect to the resulting basis is
279 # just going to be the identity.
280 ip
= inner_product(q_i
, q_j
)
281 self
._inner
_product
_matrix
[i
,j
] = ip
282 self
._inner
_product
_matrix
[j
,i
] = ip
284 self
._inner
_product
_matrix
._cache
= {'hermitian': True}
285 self
._inner
_product
_matrix
.set_immutable()
288 if not self
._is
_jordanian
():
289 raise ValueError("Jordan identity does not hold")
290 if not self
._inner
_product
_is
_associative
():
291 raise ValueError("inner product is not associative")
294 def _coerce_map_from_base_ring(self
):
296 Disable the map from the base ring into the algebra.
298 Performing a nonsense conversion like this automatically
299 is counterpedagogical. The fallback is to try the usual
300 element constructor, which should also fail.
304 sage: from mjo.eja.eja_algebra import random_eja
308 sage: set_random_seed()
309 sage: J = random_eja()
311 Traceback (most recent call last):
313 ValueError: not an element of this algebra
319 def product_on_basis(self
, i
, j
):
321 Returns the Jordan product of the `i` and `j`th basis elements.
323 This completely defines the Jordan product on the algebra, and
324 is used direclty by our superclass machinery to implement
329 sage: from mjo.eja.eja_algebra import random_eja
333 sage: set_random_seed()
334 sage: J = random_eja()
335 sage: n = J.dimension()
338 sage: bi_bj = J.zero()*J.zero()
340 ....: i = ZZ.random_element(n)
341 ....: j = ZZ.random_element(n)
342 ....: bi = J.monomial(i)
343 ....: bj = J.monomial(j)
344 ....: bi_bj = J.product_on_basis(i,j)
349 # We only stored the lower-triangular portion of the
350 # multiplication table.
352 return self
._multiplication
_table
[i
][j
]
354 return self
._multiplication
_table
[j
][i
]
356 def inner_product(self
, x
, y
):
358 The inner product associated with this Euclidean Jordan algebra.
360 Defaults to the trace inner product, but can be overridden by
361 subclasses if they are sure that the necessary properties are
366 sage: from mjo.eja.eja_algebra import (random_eja,
368 ....: BilinearFormEJA)
372 Our inner product is "associative," which means the following for
373 a symmetric bilinear form::
375 sage: set_random_seed()
376 sage: J = random_eja()
377 sage: x,y,z = J.random_elements(3)
378 sage: (x*y).inner_product(z) == y.inner_product(x*z)
383 Ensure that this is the usual inner product for the algebras
386 sage: set_random_seed()
387 sage: J = HadamardEJA.random_instance()
388 sage: x,y = J.random_elements(2)
389 sage: actual = x.inner_product(y)
390 sage: expected = x.to_vector().inner_product(y.to_vector())
391 sage: actual == expected
394 Ensure that this is one-half of the trace inner-product in a
395 BilinearFormEJA that isn't just the reals (when ``n`` isn't
396 one). This is in Faraut and Koranyi, and also my "On the
399 sage: set_random_seed()
400 sage: J = BilinearFormEJA.random_instance()
401 sage: n = J.dimension()
402 sage: x = J.random_element()
403 sage: y = J.random_element()
404 sage: (n == 1) or (x.inner_product(y) == (x*y).trace()/2)
408 B
= self
._inner
_product
_matrix
409 return (B
*x
.to_vector()).inner_product(y
.to_vector())
412 def is_associative(self
):
414 Return whether or not this algebra's Jordan product is associative.
418 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
422 sage: J = ComplexHermitianEJA(3, field=QQ, orthonormalize=False)
423 sage: J.is_associative()
425 sage: x = sum(J.gens())
426 sage: A = x.subalgebra_generated_by(orthonormalize=False)
427 sage: A.is_associative()
431 return "Associative" in self
.category().axioms()
433 def _is_commutative(self
):
435 Whether or not this algebra's multiplication table is commutative.
437 This method should of course always return ``True``, unless
438 this algebra was constructed with ``check_axioms=False`` and
439 passed an invalid multiplication table.
441 return all( x
*y
== y
*x
for x
in self
.gens() for y
in self
.gens() )
443 def _is_jordanian(self
):
445 Whether or not this algebra's multiplication table respects the
446 Jordan identity `(x^{2})(xy) = x(x^{2}y)`.
448 We only check one arrangement of `x` and `y`, so for a
449 ``True`` result to be truly true, you should also check
450 :meth:`_is_commutative`. This method should of course always
451 return ``True``, unless this algebra was constructed with
452 ``check_axioms=False`` and passed an invalid multiplication table.
454 return all( (self
.monomial(i
)**2)*(self
.monomial(i
)*self
.monomial(j
))
456 (self
.monomial(i
))*((self
.monomial(i
)**2)*self
.monomial(j
))
457 for i
in range(self
.dimension())
458 for j
in range(self
.dimension()) )
460 def _jordan_product_is_associative(self
):
462 Return whether or not this algebra's Jordan product is
463 associative; that is, whether or not `x*(y*z) = (x*y)*z`
466 This method should agree with :meth:`is_associative` unless
467 you lied about the value of the ``associative`` parameter
468 when you constructed the algebra.
472 sage: from mjo.eja.eja_algebra import (random_eja,
473 ....: RealSymmetricEJA,
474 ....: ComplexHermitianEJA,
475 ....: QuaternionHermitianEJA)
479 sage: J = RealSymmetricEJA(4, orthonormalize=False)
480 sage: J._jordan_product_is_associative()
482 sage: x = sum(J.gens())
483 sage: A = x.subalgebra_generated_by()
484 sage: A._jordan_product_is_associative()
489 sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
490 sage: J._jordan_product_is_associative()
492 sage: x = sum(J.gens())
493 sage: A = x.subalgebra_generated_by(orthonormalize=False)
494 sage: A._jordan_product_is_associative()
499 sage: J = QuaternionHermitianEJA(2)
500 sage: J._jordan_product_is_associative()
502 sage: x = sum(J.gens())
503 sage: A = x.subalgebra_generated_by()
504 sage: A._jordan_product_is_associative()
509 The values we've presupplied to the constructors agree with
512 sage: set_random_seed()
513 sage: J = random_eja()
514 sage: J.is_associative() == J._jordan_product_is_associative()
520 # Used to check whether or not something is zero.
523 # I don't know of any examples that make this magnitude
524 # necessary because I don't know how to make an
525 # associative algebra when the element subalgebra
526 # construction is unreliable (as it is over RDF; we can't
527 # find the degree of an element because we can't compute
528 # the rank of a matrix). But even multiplication of floats
529 # is non-associative, so *some* epsilon is needed... let's
530 # just take the one from _inner_product_is_associative?
533 for i
in range(self
.dimension()):
534 for j
in range(self
.dimension()):
535 for k
in range(self
.dimension()):
539 diff
= (x
*y
)*z
- x
*(y
*z
)
541 if diff
.norm() > epsilon
:
546 def _inner_product_is_associative(self
):
548 Return whether or not this algebra's inner product `B` is
549 associative; that is, whether or not `B(xy,z) = B(x,yz)`.
551 This method should of course always return ``True``, unless
552 this algebra was constructed with ``check_axioms=False`` and
553 passed an invalid Jordan or inner-product.
557 # Used to check whether or not something is zero.
560 # This choice is sufficient to allow the construction of
561 # QuaternionHermitianEJA(2, field=RDF) with check_axioms=True.
564 for i
in range(self
.dimension()):
565 for j
in range(self
.dimension()):
566 for k
in range(self
.dimension()):
570 diff
= (x
*y
).inner_product(z
) - x
.inner_product(y
*z
)
572 if diff
.abs() > epsilon
:
577 def _element_constructor_(self
, elt
):
579 Construct an element of this algebra from its vector or matrix
582 This gets called only after the parent element _call_ method
583 fails to find a coercion for the argument.
587 sage: from mjo.eja.eja_algebra import (random_eja,
590 ....: RealSymmetricEJA)
594 The identity in `S^n` is converted to the identity in the EJA::
596 sage: J = RealSymmetricEJA(3)
597 sage: I = matrix.identity(QQ,3)
598 sage: J(I) == J.one()
601 This skew-symmetric matrix can't be represented in the EJA::
603 sage: J = RealSymmetricEJA(3)
604 sage: A = matrix(QQ,3, lambda i,j: i-j)
606 Traceback (most recent call last):
608 ValueError: not an element of this algebra
610 Tuples work as well, provided that the matrix basis for the
611 algebra consists of them::
613 sage: J1 = HadamardEJA(3)
614 sage: J2 = RealSymmetricEJA(2)
615 sage: J = cartesian_product([J1,J2])
616 sage: J( (J1.matrix_basis()[1], J2.matrix_basis()[2]) )
621 Ensure that we can convert any element back and forth
622 faithfully between its matrix and algebra representations::
624 sage: set_random_seed()
625 sage: J = random_eja()
626 sage: x = J.random_element()
627 sage: J(x.to_matrix()) == x
630 We cannot coerce elements between algebras just because their
631 matrix representations are compatible::
633 sage: J1 = HadamardEJA(3)
634 sage: J2 = JordanSpinEJA(3)
636 Traceback (most recent call last):
638 ValueError: not an element of this algebra
640 Traceback (most recent call last):
642 ValueError: not an element of this algebra
644 msg
= "not an element of this algebra"
645 if elt
in self
.base_ring():
646 # Ensure that no base ring -> algebra coercion is performed
647 # by this method. There's some stupidity in sage that would
648 # otherwise propagate to this method; for example, sage thinks
649 # that the integer 3 belongs to the space of 2-by-2 matrices.
650 raise ValueError(msg
)
653 # Try to convert a vector into a column-matrix...
655 except (AttributeError, TypeError):
656 # and ignore failure, because we weren't really expecting
657 # a vector as an argument anyway.
660 if elt
not in self
.matrix_space():
661 raise ValueError(msg
)
663 # Thanks for nothing! Matrix spaces aren't vector spaces in
664 # Sage, so we have to figure out its matrix-basis coordinates
665 # ourselves. We use the basis space's ring instead of the
666 # element's ring because the basis space might be an algebraic
667 # closure whereas the base ring of the 3-by-3 identity matrix
668 # could be QQ instead of QQbar.
670 # And, we also have to handle Cartesian product bases (when
671 # the matrix basis consists of tuples) here. The "good news"
672 # is that we're already converting everything to long vectors,
673 # and that strategy works for tuples as well.
675 # We pass check=False because the matrix basis is "guaranteed"
676 # to be linearly independent... right? Ha ha.
678 V
= VectorSpace(self
.base_ring(), len(elt
))
679 W
= V
.span_of_basis( (V(_all2list(s
)) for s
in self
.matrix_basis()),
683 coords
= W
.coordinate_vector(V(elt
))
684 except ArithmeticError: # vector is not in free module
685 raise ValueError(msg
)
687 return self
.from_vector(coords
)
691 Return a string representation of ``self``.
695 sage: from mjo.eja.eja_algebra import JordanSpinEJA
699 Ensure that it says what we think it says::
701 sage: JordanSpinEJA(2, field=AA)
702 Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
703 sage: JordanSpinEJA(3, field=RDF)
704 Euclidean Jordan algebra of dimension 3 over Real Double Field
707 fmt
= "Euclidean Jordan algebra of dimension {} over {}"
708 return fmt
.format(self
.dimension(), self
.base_ring())
712 def characteristic_polynomial_of(self
):
714 Return the algebra's "characteristic polynomial of" function,
715 which is itself a multivariate polynomial that, when evaluated
716 at the coordinates of some algebra element, returns that
717 element's characteristic polynomial.
719 The resulting polynomial has `n+1` variables, where `n` is the
720 dimension of this algebra. The first `n` variables correspond to
721 the coordinates of an algebra element: when evaluated at the
722 coordinates of an algebra element with respect to a certain
723 basis, the result is a univariate polynomial (in the one
724 remaining variable ``t``), namely the characteristic polynomial
729 sage: from mjo.eja.eja_algebra import JordanSpinEJA, TrivialEJA
733 The characteristic polynomial in the spin algebra is given in
734 Alizadeh, Example 11.11::
736 sage: J = JordanSpinEJA(3)
737 sage: p = J.characteristic_polynomial_of(); p
738 X1^2 - X2^2 - X3^2 + (-2*t)*X1 + t^2
739 sage: xvec = J.one().to_vector()
743 By definition, the characteristic polynomial is a monic
744 degree-zero polynomial in a rank-zero algebra. Note that
745 Cayley-Hamilton is indeed satisfied since the polynomial
746 ``1`` evaluates to the identity element of the algebra on
749 sage: J = TrivialEJA()
750 sage: J.characteristic_polynomial_of()
757 # The list of coefficient polynomials a_0, a_1, a_2, ..., a_(r-1).
758 a
= self
._charpoly
_coefficients
()
760 # We go to a bit of trouble here to reorder the
761 # indeterminates, so that it's easier to evaluate the
762 # characteristic polynomial at x's coordinates and get back
763 # something in terms of t, which is what we want.
764 S
= PolynomialRing(self
.base_ring(),'t')
768 S
= PolynomialRing(S
, R
.variable_names())
771 return (t
**r
+ sum( a
[k
]*(t
**k
) for k
in range(r
) ))
773 def coordinate_polynomial_ring(self
):
775 The multivariate polynomial ring in which this algebra's
776 :meth:`characteristic_polynomial_of` lives.
780 sage: from mjo.eja.eja_algebra import (HadamardEJA,
781 ....: RealSymmetricEJA)
785 sage: J = HadamardEJA(2)
786 sage: J.coordinate_polynomial_ring()
787 Multivariate Polynomial Ring in X1, X2...
788 sage: J = RealSymmetricEJA(3,field=QQ,orthonormalize=False)
789 sage: J.coordinate_polynomial_ring()
790 Multivariate Polynomial Ring in X1, X2, X3, X4, X5, X6...
793 var_names
= tuple( "X%d" % z
for z
in range(1, self
.dimension()+1) )
794 return PolynomialRing(self
.base_ring(), var_names
)
796 def inner_product(self
, x
, y
):
798 The inner product associated with this Euclidean Jordan algebra.
800 Defaults to the trace inner product, but can be overridden by
801 subclasses if they are sure that the necessary properties are
806 sage: from mjo.eja.eja_algebra import (random_eja,
808 ....: BilinearFormEJA)
812 Our inner product is "associative," which means the following for
813 a symmetric bilinear form::
815 sage: set_random_seed()
816 sage: J = random_eja()
817 sage: x,y,z = J.random_elements(3)
818 sage: (x*y).inner_product(z) == y.inner_product(x*z)
823 Ensure that this is the usual inner product for the algebras
826 sage: set_random_seed()
827 sage: J = HadamardEJA.random_instance()
828 sage: x,y = J.random_elements(2)
829 sage: actual = x.inner_product(y)
830 sage: expected = x.to_vector().inner_product(y.to_vector())
831 sage: actual == expected
834 Ensure that this is one-half of the trace inner-product in a
835 BilinearFormEJA that isn't just the reals (when ``n`` isn't
836 one). This is in Faraut and Koranyi, and also my "On the
839 sage: set_random_seed()
840 sage: J = BilinearFormEJA.random_instance()
841 sage: n = J.dimension()
842 sage: x = J.random_element()
843 sage: y = J.random_element()
844 sage: (n == 1) or (x.inner_product(y) == (x*y).trace()/2)
847 B
= self
._inner
_product
_matrix
848 return (B
*x
.to_vector()).inner_product(y
.to_vector())
851 def is_trivial(self
):
853 Return whether or not this algebra is trivial.
855 A trivial algebra contains only the zero element.
859 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
864 sage: J = ComplexHermitianEJA(3)
870 sage: J = TrivialEJA()
875 return self
.dimension() == 0
878 def multiplication_table(self
):
880 Return a visual representation of this algebra's multiplication
881 table (on basis elements).
885 sage: from mjo.eja.eja_algebra import JordanSpinEJA
889 sage: J = JordanSpinEJA(4)
890 sage: J.multiplication_table()
891 +----++----+----+----+----+
892 | * || b0 | b1 | b2 | b3 |
893 +====++====+====+====+====+
894 | b0 || b0 | b1 | b2 | b3 |
895 +----++----+----+----+----+
896 | b1 || b1 | b0 | 0 | 0 |
897 +----++----+----+----+----+
898 | b2 || b2 | 0 | b0 | 0 |
899 +----++----+----+----+----+
900 | b3 || b3 | 0 | 0 | b0 |
901 +----++----+----+----+----+
905 # Prepend the header row.
906 M
= [["*"] + list(self
.gens())]
908 # And to each subsequent row, prepend an entry that belongs to
909 # the left-side "header column."
910 M
+= [ [self
.monomial(i
)] + [ self
.monomial(i
)*self
.monomial(j
)
914 return table(M
, header_row
=True, header_column
=True, frame
=True)
917 def matrix_basis(self
):
919 Return an (often more natural) representation of this algebras
920 basis as an ordered tuple of matrices.
922 Every finite-dimensional Euclidean Jordan Algebra is a, up to
923 Jordan isomorphism, a direct sum of five simple
924 algebras---four of which comprise Hermitian matrices. And the
925 last type of algebra can of course be thought of as `n`-by-`1`
926 column matrices (ambiguusly called column vectors) to avoid
927 special cases. As a result, matrices (and column vectors) are
928 a natural representation format for Euclidean Jordan algebra
931 But, when we construct an algebra from a basis of matrices,
932 those matrix representations are lost in favor of coordinate
933 vectors *with respect to* that basis. We could eventually
934 convert back if we tried hard enough, but having the original
935 representations handy is valuable enough that we simply store
936 them and return them from this method.
938 Why implement this for non-matrix algebras? Avoiding special
939 cases for the :class:`BilinearFormEJA` pays with simplicity in
940 its own right. But mainly, we would like to be able to assume
941 that elements of a :class:`CartesianProductEJA` can be displayed
942 nicely, without having to have special classes for direct sums
943 one of whose components was a matrix algebra.
947 sage: from mjo.eja.eja_algebra import (JordanSpinEJA,
948 ....: RealSymmetricEJA)
952 sage: J = RealSymmetricEJA(2)
954 Finite family {0: b0, 1: b1, 2: b2}
955 sage: J.matrix_basis()
957 [1 0] [ 0 0.7071067811865475?] [0 0]
958 [0 0], [0.7071067811865475? 0], [0 1]
963 sage: J = JordanSpinEJA(2)
965 Finite family {0: b0, 1: b1}
966 sage: J.matrix_basis()
972 return self
._matrix
_basis
975 def matrix_space(self
):
977 Return the matrix space in which this algebra's elements live, if
978 we think of them as matrices (including column vectors of the
981 "By default" this will be an `n`-by-`1` column-matrix space,
982 except when the algebra is trivial. There it's `n`-by-`n`
983 (where `n` is zero), to ensure that two elements of the matrix
984 space (empty matrices) can be multiplied. For algebras of
985 matrices, this returns the space in which their
986 real embeddings live.
990 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
992 ....: QuaternionHermitianEJA,
997 By default, the matrix representation is just a column-matrix
998 equivalent to the vector representation::
1000 sage: J = JordanSpinEJA(3)
1001 sage: J.matrix_space()
1002 Full MatrixSpace of 3 by 1 dense matrices over Algebraic
1005 The matrix representation in the trivial algebra is
1006 zero-by-zero instead of the usual `n`-by-one::
1008 sage: J = TrivialEJA()
1009 sage: J.matrix_space()
1010 Full MatrixSpace of 0 by 0 dense matrices over Algebraic
1013 The matrix space for complex/quaternion Hermitian matrix EJA
1014 is the space in which their real-embeddings live, not the
1015 original complex/quaternion matrix space::
1017 sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
1018 sage: J.matrix_space()
1019 Full MatrixSpace of 4 by 4 dense matrices over Rational Field
1020 sage: J = QuaternionHermitianEJA(1,field=QQ,orthonormalize=False)
1021 sage: J.matrix_space()
1022 Module of 1 by 1 matrices with entries in Quaternion
1023 Algebra (-1, -1) with base ring Rational Field over
1024 the scalar ring Rational Field
1027 if self
.is_trivial():
1028 return MatrixSpace(self
.base_ring(), 0)
1030 return self
.matrix_basis()[0].parent()
1036 Return the unit element of this algebra.
1040 sage: from mjo.eja.eja_algebra import (HadamardEJA,
1045 We can compute unit element in the Hadamard EJA::
1047 sage: J = HadamardEJA(5)
1049 b0 + b1 + b2 + b3 + b4
1051 The unit element in the Hadamard EJA is inherited in the
1052 subalgebras generated by its elements::
1054 sage: J = HadamardEJA(5)
1056 b0 + b1 + b2 + b3 + b4
1057 sage: x = sum(J.gens())
1058 sage: A = x.subalgebra_generated_by(orthonormalize=False)
1061 sage: A.one().superalgebra_element()
1062 b0 + b1 + b2 + b3 + b4
1066 The identity element acts like the identity, regardless of
1067 whether or not we orthonormalize::
1069 sage: set_random_seed()
1070 sage: J = random_eja()
1071 sage: x = J.random_element()
1072 sage: J.one()*x == x and x*J.one() == x
1074 sage: A = x.subalgebra_generated_by()
1075 sage: y = A.random_element()
1076 sage: A.one()*y == y and y*A.one() == y
1081 sage: set_random_seed()
1082 sage: J = random_eja(field=QQ, orthonormalize=False)
1083 sage: x = J.random_element()
1084 sage: J.one()*x == x and x*J.one() == x
1086 sage: A = x.subalgebra_generated_by(orthonormalize=False)
1087 sage: y = A.random_element()
1088 sage: A.one()*y == y and y*A.one() == y
1091 The matrix of the unit element's operator is the identity,
1092 regardless of the base field and whether or not we
1095 sage: set_random_seed()
1096 sage: J = random_eja()
1097 sage: actual = J.one().operator().matrix()
1098 sage: expected = matrix.identity(J.base_ring(), J.dimension())
1099 sage: actual == expected
1101 sage: x = J.random_element()
1102 sage: A = x.subalgebra_generated_by()
1103 sage: actual = A.one().operator().matrix()
1104 sage: expected = matrix.identity(A.base_ring(), A.dimension())
1105 sage: actual == expected
1110 sage: set_random_seed()
1111 sage: J = random_eja(field=QQ, orthonormalize=False)
1112 sage: actual = J.one().operator().matrix()
1113 sage: expected = matrix.identity(J.base_ring(), J.dimension())
1114 sage: actual == expected
1116 sage: x = J.random_element()
1117 sage: A = x.subalgebra_generated_by(orthonormalize=False)
1118 sage: actual = A.one().operator().matrix()
1119 sage: expected = matrix.identity(A.base_ring(), A.dimension())
1120 sage: actual == expected
1123 Ensure that the cached unit element (often precomputed by
1124 hand) agrees with the computed one::
1126 sage: set_random_seed()
1127 sage: J = random_eja()
1128 sage: cached = J.one()
1129 sage: J.one.clear_cache()
1130 sage: J.one() == cached
1135 sage: set_random_seed()
1136 sage: J = random_eja(field=QQ, orthonormalize=False)
1137 sage: cached = J.one()
1138 sage: J.one.clear_cache()
1139 sage: J.one() == cached
1143 # We can brute-force compute the matrices of the operators
1144 # that correspond to the basis elements of this algebra.
1145 # If some linear combination of those basis elements is the
1146 # algebra identity, then the same linear combination of
1147 # their matrices has to be the identity matrix.
1149 # Of course, matrices aren't vectors in sage, so we have to
1150 # appeal to the "long vectors" isometry.
1151 oper_vecs
= [ _mat2vec(g
.operator().matrix()) for g
in self
.gens() ]
1153 # Now we use basic linear algebra to find the coefficients,
1154 # of the matrices-as-vectors-linear-combination, which should
1155 # work for the original algebra basis too.
1156 A
= matrix(self
.base_ring(), oper_vecs
)
1158 # We used the isometry on the left-hand side already, but we
1159 # still need to do it for the right-hand side. Recall that we
1160 # wanted something that summed to the identity matrix.
1161 b
= _mat2vec( matrix
.identity(self
.base_ring(), self
.dimension()) )
1163 # Now if there's an identity element in the algebra, this
1164 # should work. We solve on the left to avoid having to
1165 # transpose the matrix "A".
1166 return self
.from_vector(A
.solve_left(b
))
1169 def peirce_decomposition(self
, c
):
1171 The Peirce decomposition of this algebra relative to the
1174 In the future, this can be extended to a complete system of
1175 orthogonal idempotents.
1179 - ``c`` -- an idempotent of this algebra.
1183 A triple (J0, J5, J1) containing two subalgebras and one subspace
1186 - ``J0`` -- the algebra on the eigenspace of ``c.operator()``
1187 corresponding to the eigenvalue zero.
1189 - ``J5`` -- the eigenspace (NOT a subalgebra) of ``c.operator()``
1190 corresponding to the eigenvalue one-half.
1192 - ``J1`` -- the algebra on the eigenspace of ``c.operator()``
1193 corresponding to the eigenvalue one.
1195 These are the only possible eigenspaces for that operator, and this
1196 algebra is a direct sum of them. The spaces ``J0`` and ``J1`` are
1197 orthogonal, and are subalgebras of this algebra with the appropriate
1202 sage: from mjo.eja.eja_algebra import random_eja, RealSymmetricEJA
1206 The canonical example comes from the symmetric matrices, which
1207 decompose into diagonal and off-diagonal parts::
1209 sage: J = RealSymmetricEJA(3)
1210 sage: C = matrix(QQ, [ [1,0,0],
1214 sage: J0,J5,J1 = J.peirce_decomposition(c)
1216 Euclidean Jordan algebra of dimension 1...
1218 Vector space of degree 6 and dimension 2...
1220 Euclidean Jordan algebra of dimension 3...
1221 sage: J0.one().to_matrix()
1225 sage: orig_df = AA.options.display_format
1226 sage: AA.options.display_format = 'radical'
1227 sage: J.from_vector(J5.basis()[0]).to_matrix()
1231 sage: J.from_vector(J5.basis()[1]).to_matrix()
1235 sage: AA.options.display_format = orig_df
1236 sage: J1.one().to_matrix()
1243 Every algebra decomposes trivially with respect to its identity
1246 sage: set_random_seed()
1247 sage: J = random_eja()
1248 sage: J0,J5,J1 = J.peirce_decomposition(J.one())
1249 sage: J0.dimension() == 0 and J5.dimension() == 0
1251 sage: J1.superalgebra() == J and J1.dimension() == J.dimension()
1254 The decomposition is into eigenspaces, and its components are
1255 therefore necessarily orthogonal. Moreover, the identity
1256 elements in the two subalgebras are the projections onto their
1257 respective subspaces of the superalgebra's identity element::
1259 sage: set_random_seed()
1260 sage: J = random_eja()
1261 sage: x = J.random_element()
1262 sage: if not J.is_trivial():
1263 ....: while x.is_nilpotent():
1264 ....: x = J.random_element()
1265 sage: c = x.subalgebra_idempotent()
1266 sage: J0,J5,J1 = J.peirce_decomposition(c)
1268 sage: for (w,y,z) in zip(J0.basis(), J5.basis(), J1.basis()):
1269 ....: w = w.superalgebra_element()
1270 ....: y = J.from_vector(y)
1271 ....: z = z.superalgebra_element()
1272 ....: ipsum += w.inner_product(y).abs()
1273 ....: ipsum += w.inner_product(z).abs()
1274 ....: ipsum += y.inner_product(z).abs()
1277 sage: J1(c) == J1.one()
1279 sage: J0(J.one() - c) == J0.one()
1283 if not c
.is_idempotent():
1284 raise ValueError("element is not idempotent: %s" % c
)
1286 # Default these to what they should be if they turn out to be
1287 # trivial, because eigenspaces_left() won't return eigenvalues
1288 # corresponding to trivial spaces (e.g. it returns only the
1289 # eigenspace corresponding to lambda=1 if you take the
1290 # decomposition relative to the identity element).
1291 trivial
= self
.subalgebra(())
1292 J0
= trivial
# eigenvalue zero
1293 J5
= VectorSpace(self
.base_ring(), 0) # eigenvalue one-half
1294 J1
= trivial
# eigenvalue one
1296 for (eigval
, eigspace
) in c
.operator().matrix().right_eigenspaces():
1297 if eigval
== ~
(self
.base_ring()(2)):
1300 gens
= tuple( self
.from_vector(b
) for b
in eigspace
.basis() )
1301 subalg
= self
.subalgebra(gens
, check_axioms
=False)
1307 raise ValueError("unexpected eigenvalue: %s" % eigval
)
1312 def random_element(self
, thorough
=False):
1314 Return a random element of this algebra.
1316 Our algebra superclass method only returns a linear
1317 combination of at most two basis elements. We instead
1318 want the vector space "random element" method that
1319 returns a more diverse selection.
1323 - ``thorough`` -- (boolean; default False) whether or not we
1324 should generate irrational coefficients for the random
1325 element when our base ring is irrational; this slows the
1326 algebra operations to a crawl, but any truly random method
1330 # For a general base ring... maybe we can trust this to do the
1331 # right thing? Unlikely, but.
1332 V
= self
.vector_space()
1333 v
= V
.random_element()
1335 if self
.base_ring() is AA
:
1336 # The "random element" method of the algebraic reals is
1337 # stupid at the moment, and only returns integers between
1338 # -2 and 2, inclusive:
1340 # https://trac.sagemath.org/ticket/30875
1342 # Instead, we implement our own "random vector" method,
1343 # and then coerce that into the algebra. We use the vector
1344 # space degree here instead of the dimension because a
1345 # subalgebra could (for example) be spanned by only two
1346 # vectors, each with five coordinates. We need to
1347 # generate all five coordinates.
1349 v
*= QQbar
.random_element().real()
1351 v
*= QQ
.random_element()
1353 return self
.from_vector(V
.coordinate_vector(v
))
1355 def random_elements(self
, count
, thorough
=False):
1357 Return ``count`` random elements as a tuple.
1361 - ``thorough`` -- (boolean; default False) whether or not we
1362 should generate irrational coefficients for the random
1363 elements when our base ring is irrational; this slows the
1364 algebra operations to a crawl, but any truly random method
1369 sage: from mjo.eja.eja_algebra import JordanSpinEJA
1373 sage: J = JordanSpinEJA(3)
1374 sage: x,y,z = J.random_elements(3)
1375 sage: all( [ x in J, y in J, z in J ])
1377 sage: len( J.random_elements(10) ) == 10
1381 return tuple( self
.random_element(thorough
)
1382 for idx
in range(count
) )
1386 def _charpoly_coefficients(self
):
1388 The `r` polynomial coefficients of the "characteristic polynomial
1393 sage: from mjo.eja.eja_algebra import random_eja
1397 The theory shows that these are all homogeneous polynomials of
1400 sage: set_random_seed()
1401 sage: J = random_eja()
1402 sage: all(p.is_homogeneous() for p in J._charpoly_coefficients())
1406 n
= self
.dimension()
1407 R
= self
.coordinate_polynomial_ring()
1409 F
= R
.fraction_field()
1412 # From a result in my book, these are the entries of the
1413 # basis representation of L_x.
1414 return sum( vars[k
]*self
.monomial(k
).operator().matrix()[i
,j
]
1417 L_x
= matrix(F
, n
, n
, L_x_i_j
)
1420 if self
.rank
.is_in_cache():
1422 # There's no need to pad the system with redundant
1423 # columns if we *know* they'll be redundant.
1426 # Compute an extra power in case the rank is equal to
1427 # the dimension (otherwise, we would stop at x^(r-1)).
1428 x_powers
= [ (L_x
**k
)*self
.one().to_vector()
1429 for k
in range(n
+1) ]
1430 A
= matrix
.column(F
, x_powers
[:n
])
1431 AE
= A
.extended_echelon_form()
1438 # The theory says that only the first "r" coefficients are
1439 # nonzero, and they actually live in the original polynomial
1440 # ring and not the fraction field. We negate them because in
1441 # the actual characteristic polynomial, they get moved to the
1442 # other side where x^r lives. We don't bother to trim A_rref
1443 # down to a square matrix and solve the resulting system,
1444 # because the upper-left r-by-r portion of A_rref is
1445 # guaranteed to be the identity matrix, so e.g.
1447 # A_rref.solve_right(Y)
1449 # would just be returning Y.
1450 return (-E
*b
)[:r
].change_ring(R
)
1455 Return the rank of this EJA.
1457 This is a cached method because we know the rank a priori for
1458 all of the algebras we can construct. Thus we can avoid the
1459 expensive ``_charpoly_coefficients()`` call unless we truly
1460 need to compute the whole characteristic polynomial.
1464 sage: from mjo.eja.eja_algebra import (HadamardEJA,
1465 ....: JordanSpinEJA,
1466 ....: RealSymmetricEJA,
1467 ....: ComplexHermitianEJA,
1468 ....: QuaternionHermitianEJA,
1473 The rank of the Jordan spin algebra is always two::
1475 sage: JordanSpinEJA(2).rank()
1477 sage: JordanSpinEJA(3).rank()
1479 sage: JordanSpinEJA(4).rank()
1482 The rank of the `n`-by-`n` Hermitian real, complex, or
1483 quaternion matrices is `n`::
1485 sage: RealSymmetricEJA(4).rank()
1487 sage: ComplexHermitianEJA(3).rank()
1489 sage: QuaternionHermitianEJA(2).rank()
1494 Ensure that every EJA that we know how to construct has a
1495 positive integer rank, unless the algebra is trivial in
1496 which case its rank will be zero::
1498 sage: set_random_seed()
1499 sage: J = random_eja()
1503 sage: r > 0 or (r == 0 and J.is_trivial())
1506 Ensure that computing the rank actually works, since the ranks
1507 of all simple algebras are known and will be cached by default::
1509 sage: set_random_seed() # long time
1510 sage: J = random_eja() # long time
1511 sage: cached = J.rank() # long time
1512 sage: J.rank.clear_cache() # long time
1513 sage: J.rank() == cached # long time
1517 return len(self
._charpoly
_coefficients
())
1520 def subalgebra(self
, basis
, **kwargs
):
1522 Create a subalgebra of this algebra from the given basis.
1524 from mjo
.eja
.eja_subalgebra
import FiniteDimensionalEJASubalgebra
1525 return FiniteDimensionalEJASubalgebra(self
, basis
, **kwargs
)
1528 def vector_space(self
):
1530 Return the vector space that underlies this algebra.
1534 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1538 sage: J = RealSymmetricEJA(2)
1539 sage: J.vector_space()
1540 Vector space of dimension 3 over...
1543 return self
.zero().to_vector().parent().ambient_vector_space()
1547 class RationalBasisEJA(FiniteDimensionalEJA
):
1549 Algebras whose supplied basis elements have all rational entries.
1553 sage: from mjo.eja.eja_algebra import BilinearFormEJA
1557 The supplied basis is orthonormalized by default::
1559 sage: B = matrix(QQ, [[1, 0, 0], [0, 25, -32], [0, -32, 41]])
1560 sage: J = BilinearFormEJA(B)
1561 sage: J.matrix_basis()
1578 # Abuse the check_field parameter to check that the entries of
1579 # out basis (in ambient coordinates) are in the field QQ.
1580 # Use _all2list to get the vector coordinates of octonion
1581 # entries and not the octonions themselves (which are not
1583 if not all( all(b_i
in QQ
for b_i
in _all2list(b
))
1585 raise TypeError("basis not rational")
1587 super().__init
__(basis
,
1591 check_field
=check_field
,
1594 self
._rational
_algebra
= None
1596 # There's no point in constructing the extra algebra if this
1597 # one is already rational.
1599 # Note: the same Jordan and inner-products work here,
1600 # because they are necessarily defined with respect to
1601 # ambient coordinates and not any particular basis.
1602 self
._rational
_algebra
= FiniteDimensionalEJA(
1607 associative
=self
.is_associative(),
1608 orthonormalize
=False,
1613 def _charpoly_coefficients(self
):
1617 sage: from mjo.eja.eja_algebra import (BilinearFormEJA,
1618 ....: JordanSpinEJA)
1622 The base ring of the resulting polynomial coefficients is what
1623 it should be, and not the rationals (unless the algebra was
1624 already over the rationals)::
1626 sage: J = JordanSpinEJA(3)
1627 sage: J._charpoly_coefficients()
1628 (X1^2 - X2^2 - X3^2, -2*X1)
1629 sage: a0 = J._charpoly_coefficients()[0]
1631 Algebraic Real Field
1632 sage: a0.base_ring()
1633 Algebraic Real Field
1636 if self
._rational
_algebra
is None:
1637 # There's no need to construct *another* algebra over the
1638 # rationals if this one is already over the
1639 # rationals. Likewise, if we never orthonormalized our
1640 # basis, we might as well just use the given one.
1641 return super()._charpoly
_coefficients
()
1643 # Do the computation over the rationals. The answer will be
1644 # the same, because all we've done is a change of basis.
1645 # Then, change back from QQ to our real base ring
1646 a
= ( a_i
.change_ring(self
.base_ring())
1647 for a_i
in self
._rational
_algebra
._charpoly
_coefficients
() )
1649 if self
._deortho
_matrix
is None:
1650 # This can happen if our base ring was, say, AA and we
1651 # chose not to (or didn't need to) orthonormalize. It's
1652 # still faster to do the computations over QQ even if
1653 # the numbers in the boxes stay the same.
1656 # Otherwise, convert the coordinate variables back to the
1657 # deorthonormalized ones.
1658 R
= self
.coordinate_polynomial_ring()
1659 from sage
.modules
.free_module_element
import vector
1660 X
= vector(R
, R
.gens())
1661 BX
= self
._deortho
_matrix
*X
1663 subs_dict
= { X[i]: BX[i] for i in range(len(X)) }
1664 return tuple( a_i
.subs(subs_dict
) for a_i
in a
)
1666 class ConcreteEJA(FiniteDimensionalEJA
):
1668 A class for the Euclidean Jordan algebras that we know by name.
1670 These are the Jordan algebras whose basis, multiplication table,
1671 rank, and so on are known a priori. More to the point, they are
1672 the Euclidean Jordan algebras for which we are able to conjure up
1673 a "random instance."
1677 sage: from mjo.eja.eja_algebra import ConcreteEJA
1681 Our basis is normalized with respect to the algebra's inner
1682 product, unless we specify otherwise::
1684 sage: set_random_seed()
1685 sage: J = ConcreteEJA.random_instance()
1686 sage: all( b.norm() == 1 for b in J.gens() )
1689 Since our basis is orthonormal with respect to the algebra's inner
1690 product, and since we know that this algebra is an EJA, any
1691 left-multiplication operator's matrix will be symmetric because
1692 natural->EJA basis representation is an isometry and within the
1693 EJA the operator is self-adjoint by the Jordan axiom::
1695 sage: set_random_seed()
1696 sage: J = ConcreteEJA.random_instance()
1697 sage: x = J.random_element()
1698 sage: x.operator().is_self_adjoint()
1703 def _max_random_instance_size():
1705 Return an integer "size" that is an upper bound on the size of
1706 this algebra when it is used in a random test
1707 case. Unfortunately, the term "size" is ambiguous -- when
1708 dealing with `R^n` under either the Hadamard or Jordan spin
1709 product, the "size" refers to the dimension `n`. When dealing
1710 with a matrix algebra (real symmetric or complex/quaternion
1711 Hermitian), it refers to the size of the matrix, which is far
1712 less than the dimension of the underlying vector space.
1714 This method must be implemented in each subclass.
1716 raise NotImplementedError
1719 def random_instance(cls
, *args
, **kwargs
):
1721 Return a random instance of this type of algebra.
1723 This method should be implemented in each subclass.
1725 from sage
.misc
.prandom
import choice
1726 eja_class
= choice(cls
.__subclasses
__())
1728 # These all bubble up to the RationalBasisEJA superclass
1729 # constructor, so any (kw)args valid there are also valid
1731 return eja_class
.random_instance(*args
, **kwargs
)
1736 def jordan_product(X
,Y
):
1737 return (X
*Y
+ Y
*X
)/2
1740 def trace_inner_product(X
,Y
):
1742 A trace inner-product for matrices that aren't embedded in the
1743 reals. It takes MATRICES as arguments, not EJA elements.
1745 return (X
*Y
).trace().real()
1748 class RealSymmetricEJA(RationalBasisEJA
, ConcreteEJA
, MatrixEJA
):
1750 The rank-n simple EJA consisting of real symmetric n-by-n
1751 matrices, the usual symmetric Jordan product, and the trace inner
1752 product. It has dimension `(n^2 + n)/2` over the reals.
1756 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1760 sage: J = RealSymmetricEJA(2)
1761 sage: b0, b1, b2 = J.gens()
1769 In theory, our "field" can be any subfield of the reals::
1771 sage: RealSymmetricEJA(2, field=RDF, check_axioms=True)
1772 Euclidean Jordan algebra of dimension 3 over Real Double Field
1773 sage: RealSymmetricEJA(2, field=RR, check_axioms=True)
1774 Euclidean Jordan algebra of dimension 3 over Real Field with
1775 53 bits of precision
1779 The dimension of this algebra is `(n^2 + n) / 2`::
1781 sage: set_random_seed()
1782 sage: n_max = RealSymmetricEJA._max_random_instance_size()
1783 sage: n = ZZ.random_element(1, n_max)
1784 sage: J = RealSymmetricEJA(n)
1785 sage: J.dimension() == (n^2 + n)/2
1788 The Jordan multiplication is what we think it is::
1790 sage: set_random_seed()
1791 sage: J = RealSymmetricEJA.random_instance()
1792 sage: x,y = J.random_elements(2)
1793 sage: actual = (x*y).to_matrix()
1794 sage: X = x.to_matrix()
1795 sage: Y = y.to_matrix()
1796 sage: expected = (X*Y + Y*X)/2
1797 sage: actual == expected
1799 sage: J(expected) == x*y
1802 We can change the generator prefix::
1804 sage: RealSymmetricEJA(3, prefix='q').gens()
1805 (q0, q1, q2, q3, q4, q5)
1807 We can construct the (trivial) algebra of rank zero::
1809 sage: RealSymmetricEJA(0)
1810 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
1814 def _denormalized_basis(cls
, n
, field
):
1816 Return a basis for the space of real symmetric n-by-n matrices.
1820 sage: from mjo.eja.eja_algebra import RealSymmetricEJA
1824 sage: set_random_seed()
1825 sage: n = ZZ.random_element(1,5)
1826 sage: B = RealSymmetricEJA._denormalized_basis(n,ZZ)
1827 sage: all( M.is_symmetric() for M in B)
1831 # The basis of symmetric matrices, as matrices, in their R^(n-by-n)
1835 for j
in range(i
+1):
1836 Eij
= matrix(field
, n
, lambda k
,l
: k
==i
and l
==j
)
1840 Sij
= Eij
+ Eij
.transpose()
1846 def _max_random_instance_size():
1847 return 4 # Dimension 10
1850 def random_instance(cls
, **kwargs
):
1852 Return a random instance of this type of algebra.
1854 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
() + 1)
1855 return cls(n
, **kwargs
)
1857 def __init__(self
, n
, field
=AA
, **kwargs
):
1858 # We know this is a valid EJA, but will double-check
1859 # if the user passes check_axioms=True.
1860 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
1866 super().__init
__(self
._denormalized
_basis
(n
,field
),
1867 self
.jordan_product
,
1868 self
.trace_inner_product
,
1870 associative
=associative
,
1873 # TODO: this could be factored out somehow, but is left here
1874 # because the MatrixEJA is not presently a subclass of the
1875 # FDEJA class that defines rank() and one().
1876 self
.rank
.set_cache(n
)
1877 idV
= self
.matrix_space().one()
1878 self
.one
.set_cache(self(idV
))
1882 class ComplexHermitianEJA(RationalBasisEJA
, ConcreteEJA
, MatrixEJA
):
1884 The rank-n simple EJA consisting of complex Hermitian n-by-n
1885 matrices over the real numbers, the usual symmetric Jordan product,
1886 and the real-part-of-trace inner product. It has dimension `n^2` over
1891 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1895 In theory, our "field" can be any subfield of the reals::
1897 sage: ComplexHermitianEJA(2, field=RDF, check_axioms=True)
1898 Euclidean Jordan algebra of dimension 4 over Real Double Field
1899 sage: ComplexHermitianEJA(2, field=RR, check_axioms=True)
1900 Euclidean Jordan algebra of dimension 4 over Real Field with
1901 53 bits of precision
1905 The dimension of this algebra is `n^2`::
1907 sage: set_random_seed()
1908 sage: n_max = ComplexHermitianEJA._max_random_instance_size()
1909 sage: n = ZZ.random_element(1, n_max)
1910 sage: J = ComplexHermitianEJA(n)
1911 sage: J.dimension() == n^2
1914 The Jordan multiplication is what we think it is::
1916 sage: set_random_seed()
1917 sage: J = ComplexHermitianEJA.random_instance()
1918 sage: x,y = J.random_elements(2)
1919 sage: actual = (x*y).to_matrix()
1920 sage: X = x.to_matrix()
1921 sage: Y = y.to_matrix()
1922 sage: expected = (X*Y + Y*X)/2
1923 sage: actual == expected
1925 sage: J(expected) == x*y
1928 We can change the generator prefix::
1930 sage: ComplexHermitianEJA(2, prefix='z').gens()
1933 We can construct the (trivial) algebra of rank zero::
1935 sage: ComplexHermitianEJA(0)
1936 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
1941 def _denormalized_basis(cls
, n
, field
):
1943 Returns a basis for the space of complex Hermitian n-by-n matrices.
1945 Why do we embed these? Basically, because all of numerical linear
1946 algebra assumes that you're working with vectors consisting of `n`
1947 entries from a field and scalars from the same field. There's no way
1948 to tell SageMath that (for example) the vectors contain complex
1949 numbers, while the scalar field is real.
1953 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1957 sage: set_random_seed()
1958 sage: n = ZZ.random_element(1,5)
1959 sage: B = ComplexHermitianEJA._denormalized_basis(n,QQ)
1960 sage: all( M.is_hermitian() for M in B)
1964 from mjo
.hurwitz
import ComplexMatrixAlgebra
1965 A
= ComplexMatrixAlgebra(n
, scalars
=field
)
1966 es
= A
.entry_algebra_gens()
1970 for j
in range(i
+1):
1972 E_ii
= A
.monomial( (i
,j
,es
[0]) )
1976 E_ij
= A
.monomial( (i
,j
,e
) )
1978 # If the conjugate has a negative sign in front
1979 # of it, (j,i,ec) won't be a monomial!
1980 if (j
,i
,ec
) in A
.indices():
1981 E_ij
+= A
.monomial( (j
,i
,ec
) )
1983 E_ij
-= A
.monomial( (j
,i
,-ec
) )
1986 return tuple( basis
)
1989 def trace_inner_product(X
,Y
):
1993 sage: from mjo.eja.eja_algebra import ComplexHermitianEJA
1997 sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False)
1998 sage: I = J.one().to_matrix()
1999 sage: J.trace_inner_product(I, -I)
2003 return (X
*Y
).trace().real()
2005 def __init__(self
, n
, field
=AA
, **kwargs
):
2006 # We know this is a valid EJA, but will double-check
2007 # if the user passes check_axioms=True.
2008 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2014 super().__init
__(self
._denormalized
_basis
(n
,field
),
2015 self
.jordan_product
,
2016 self
.trace_inner_product
,
2018 associative
=associative
,
2020 # TODO: this could be factored out somehow, but is left here
2021 # because the MatrixEJA is not presently a subclass of the
2022 # FDEJA class that defines rank() and one().
2023 self
.rank
.set_cache(n
)
2024 idV
= self
.matrix_space().one()
2025 self
.one
.set_cache(self(idV
))
2028 def _max_random_instance_size():
2029 return 3 # Dimension 9
2032 def random_instance(cls
, **kwargs
):
2034 Return a random instance of this type of algebra.
2036 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
() + 1)
2037 return cls(n
, **kwargs
)
2040 class QuaternionHermitianEJA(RationalBasisEJA
, ConcreteEJA
, MatrixEJA
):
2042 The rank-n simple EJA consisting of self-adjoint n-by-n quaternion
2043 matrices, the usual symmetric Jordan product, and the
2044 real-part-of-trace inner product. It has dimension `2n^2 - n` over
2049 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
2053 In theory, our "field" can be any subfield of the reals::
2055 sage: QuaternionHermitianEJA(2, field=RDF, check_axioms=True)
2056 Euclidean Jordan algebra of dimension 6 over Real Double Field
2057 sage: QuaternionHermitianEJA(2, field=RR, check_axioms=True)
2058 Euclidean Jordan algebra of dimension 6 over Real Field with
2059 53 bits of precision
2063 The dimension of this algebra is `2*n^2 - n`::
2065 sage: set_random_seed()
2066 sage: n_max = QuaternionHermitianEJA._max_random_instance_size()
2067 sage: n = ZZ.random_element(1, n_max)
2068 sage: J = QuaternionHermitianEJA(n)
2069 sage: J.dimension() == 2*(n^2) - n
2072 The Jordan multiplication is what we think it is::
2074 sage: set_random_seed()
2075 sage: J = QuaternionHermitianEJA.random_instance()
2076 sage: x,y = J.random_elements(2)
2077 sage: actual = (x*y).to_matrix()
2078 sage: X = x.to_matrix()
2079 sage: Y = y.to_matrix()
2080 sage: expected = (X*Y + Y*X)/2
2081 sage: actual == expected
2083 sage: J(expected) == x*y
2086 We can change the generator prefix::
2088 sage: QuaternionHermitianEJA(2, prefix='a').gens()
2089 (a0, a1, a2, a3, a4, a5)
2091 We can construct the (trivial) algebra of rank zero::
2093 sage: QuaternionHermitianEJA(0)
2094 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2098 def _denormalized_basis(cls
, n
, field
):
2100 Returns a basis for the space of quaternion Hermitian n-by-n matrices.
2102 Why do we embed these? Basically, because all of numerical
2103 linear algebra assumes that you're working with vectors consisting
2104 of `n` entries from a field and scalars from the same field. There's
2105 no way to tell SageMath that (for example) the vectors contain
2106 complex numbers, while the scalar field is real.
2110 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
2114 sage: set_random_seed()
2115 sage: n = ZZ.random_element(1,5)
2116 sage: B = QuaternionHermitianEJA._denormalized_basis(n,QQ)
2117 sage: all( M.is_hermitian() for M in B )
2121 from mjo
.hurwitz
import QuaternionMatrixAlgebra
2122 A
= QuaternionMatrixAlgebra(n
, scalars
=field
)
2123 es
= A
.entry_algebra_gens()
2127 for j
in range(i
+1):
2129 E_ii
= A
.monomial( (i
,j
,es
[0]) )
2133 E_ij
= A
.monomial( (i
,j
,e
) )
2135 # If the conjugate has a negative sign in front
2136 # of it, (j,i,ec) won't be a monomial!
2137 if (j
,i
,ec
) in A
.indices():
2138 E_ij
+= A
.monomial( (j
,i
,ec
) )
2140 E_ij
-= A
.monomial( (j
,i
,-ec
) )
2143 return tuple( basis
)
2147 def trace_inner_product(X
,Y
):
2149 Overload the superclass method because the quaternions are weird
2150 and we need to use ``coefficient_tuple()`` to get the realpart.
2154 sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA
2158 sage: J = QuaternionHermitianEJA(2,field=QQ,orthonormalize=False)
2159 sage: I = J.one().to_matrix()
2160 sage: J.trace_inner_product(I, -I)
2164 return (X
*Y
).trace().coefficient_tuple()[0]
2166 def __init__(self
, n
, field
=AA
, **kwargs
):
2167 # We know this is a valid EJA, but will double-check
2168 # if the user passes check_axioms=True.
2169 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2175 super().__init
__(self
._denormalized
_basis
(n
,field
),
2176 self
.jordan_product
,
2177 self
.trace_inner_product
,
2179 associative
=associative
,
2182 # TODO: this could be factored out somehow, but is left here
2183 # because the MatrixEJA is not presently a subclass of the
2184 # FDEJA class that defines rank() and one().
2185 self
.rank
.set_cache(n
)
2186 idV
= self
.matrix_space().one()
2187 self
.one
.set_cache(self(idV
))
2191 def _max_random_instance_size():
2193 The maximum rank of a random QuaternionHermitianEJA.
2195 return 2 # Dimension 6
2198 def random_instance(cls
, **kwargs
):
2200 Return a random instance of this type of algebra.
2202 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
() + 1)
2203 return cls(n
, **kwargs
)
2205 class OctonionHermitianEJA(RationalBasisEJA
, ConcreteEJA
, MatrixEJA
):
2209 sage: from mjo.eja.eja_algebra import (FiniteDimensionalEJA,
2210 ....: OctonionHermitianEJA)
2214 The 3-by-3 algebra satisfies the axioms of an EJA::
2216 sage: OctonionHermitianEJA(3, # long time
2217 ....: field=QQ, # long time
2218 ....: orthonormalize=False, # long time
2219 ....: check_axioms=True) # long time
2220 Euclidean Jordan algebra of dimension 27 over Rational Field
2222 After a change-of-basis, the 2-by-2 algebra has the same
2223 multiplication table as the ten-dimensional Jordan spin algebra::
2225 sage: b = OctonionHermitianEJA._denormalized_basis(2,QQ)
2226 sage: basis = (b[0] + b[9],) + b[1:9] + (b[0] - b[9],)
2227 sage: jp = OctonionHermitianEJA.jordan_product
2228 sage: ip = OctonionHermitianEJA.trace_inner_product
2229 sage: J = FiniteDimensionalEJA(basis,
2233 ....: orthonormalize=False)
2234 sage: J.multiplication_table()
2235 +----++----+----+----+----+----+----+----+----+----+----+
2236 | * || b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7 | b8 | b9 |
2237 +====++====+====+====+====+====+====+====+====+====+====+
2238 | b0 || b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7 | b8 | b9 |
2239 +----++----+----+----+----+----+----+----+----+----+----+
2240 | b1 || b1 | b0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2241 +----++----+----+----+----+----+----+----+----+----+----+
2242 | b2 || b2 | 0 | b0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2243 +----++----+----+----+----+----+----+----+----+----+----+
2244 | b3 || b3 | 0 | 0 | b0 | 0 | 0 | 0 | 0 | 0 | 0 |
2245 +----++----+----+----+----+----+----+----+----+----+----+
2246 | b4 || b4 | 0 | 0 | 0 | b0 | 0 | 0 | 0 | 0 | 0 |
2247 +----++----+----+----+----+----+----+----+----+----+----+
2248 | b5 || b5 | 0 | 0 | 0 | 0 | b0 | 0 | 0 | 0 | 0 |
2249 +----++----+----+----+----+----+----+----+----+----+----+
2250 | b6 || b6 | 0 | 0 | 0 | 0 | 0 | b0 | 0 | 0 | 0 |
2251 +----++----+----+----+----+----+----+----+----+----+----+
2252 | b7 || b7 | 0 | 0 | 0 | 0 | 0 | 0 | b0 | 0 | 0 |
2253 +----++----+----+----+----+----+----+----+----+----+----+
2254 | b8 || b8 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | b0 | 0 |
2255 +----++----+----+----+----+----+----+----+----+----+----+
2256 | b9 || b9 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | b0 |
2257 +----++----+----+----+----+----+----+----+----+----+----+
2261 We can actually construct the 27-dimensional Albert algebra,
2262 and we get the right unit element if we recompute it::
2264 sage: J = OctonionHermitianEJA(3, # long time
2265 ....: field=QQ, # long time
2266 ....: orthonormalize=False) # long time
2267 sage: J.one.clear_cache() # long time
2268 sage: J.one() # long time
2270 sage: J.one().to_matrix() # long time
2279 The 2-by-2 algebra is isomorphic to the ten-dimensional Jordan
2280 spin algebra, but just to be sure, we recompute its rank::
2282 sage: J = OctonionHermitianEJA(2, # long time
2283 ....: field=QQ, # long time
2284 ....: orthonormalize=False) # long time
2285 sage: J.rank.clear_cache() # long time
2286 sage: J.rank() # long time
2291 def _max_random_instance_size():
2293 The maximum rank of a random QuaternionHermitianEJA.
2295 return 1 # Dimension 1
2298 def random_instance(cls
, **kwargs
):
2300 Return a random instance of this type of algebra.
2302 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
() + 1)
2303 return cls(n
, **kwargs
)
2305 def __init__(self
, n
, field
=AA
, **kwargs
):
2307 # Otherwise we don't get an EJA.
2308 raise ValueError("n cannot exceed 3")
2310 # We know this is a valid EJA, but will double-check
2311 # if the user passes check_axioms=True.
2312 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2314 super().__init
__(self
._denormalized
_basis
(n
,field
),
2315 self
.jordan_product
,
2316 self
.trace_inner_product
,
2320 # TODO: this could be factored out somehow, but is left here
2321 # because the MatrixEJA is not presently a subclass of the
2322 # FDEJA class that defines rank() and one().
2323 self
.rank
.set_cache(n
)
2324 idV
= self
.matrix_space().one()
2325 self
.one
.set_cache(self(idV
))
2329 def _denormalized_basis(cls
, n
, field
):
2331 Returns a basis for the space of octonion Hermitian n-by-n
2336 sage: from mjo.eja.eja_algebra import OctonionHermitianEJA
2340 sage: B = OctonionHermitianEJA._denormalized_basis(3,QQ)
2341 sage: all( M.is_hermitian() for M in B )
2347 from mjo
.hurwitz
import OctonionMatrixAlgebra
2348 A
= OctonionMatrixAlgebra(n
, scalars
=field
)
2349 es
= A
.entry_algebra_gens()
2353 for j
in range(i
+1):
2355 E_ii
= A
.monomial( (i
,j
,es
[0]) )
2359 E_ij
= A
.monomial( (i
,j
,e
) )
2361 # If the conjugate has a negative sign in front
2362 # of it, (j,i,ec) won't be a monomial!
2363 if (j
,i
,ec
) in A
.indices():
2364 E_ij
+= A
.monomial( (j
,i
,ec
) )
2366 E_ij
-= A
.monomial( (j
,i
,-ec
) )
2369 return tuple( basis
)
2372 def trace_inner_product(X
,Y
):
2374 The octonions don't know that the reals are embedded in them,
2375 so we have to take the e0 component ourselves.
2379 sage: from mjo.eja.eja_algebra import OctonionHermitianEJA
2383 sage: J = OctonionHermitianEJA(2,field=QQ,orthonormalize=False)
2384 sage: I = J.one().to_matrix()
2385 sage: J.trace_inner_product(I, -I)
2389 return (X
*Y
).trace().coefficient(0)
2392 class AlbertEJA(OctonionHermitianEJA
):
2394 The Albert algebra is the algebra of three-by-three Hermitian
2395 matrices whose entries are octonions.
2399 sage: from mjo.eja.eja_algebra import AlbertEJA
2403 sage: AlbertEJA(field=QQ, orthonormalize=False)
2404 Euclidean Jordan algebra of dimension 27 over Rational Field
2405 sage: AlbertEJA() # long time
2406 Euclidean Jordan algebra of dimension 27 over Algebraic Real Field
2409 def __init__(self
, *args
, **kwargs
):
2410 super().__init
__(3, *args
, **kwargs
)
2413 class HadamardEJA(RationalBasisEJA
, ConcreteEJA
):
2415 Return the Euclidean Jordan algebra on `R^n` with the Hadamard
2416 (pointwise real-number multiplication) Jordan product and the
2417 usual inner-product.
2419 This is nothing more than the Cartesian product of ``n`` copies of
2420 the one-dimensional Jordan spin algebra, and is the most common
2421 example of a non-simple Euclidean Jordan algebra.
2425 sage: from mjo.eja.eja_algebra import HadamardEJA
2429 This multiplication table can be verified by hand::
2431 sage: J = HadamardEJA(3)
2432 sage: b0,b1,b2 = J.gens()
2448 We can change the generator prefix::
2450 sage: HadamardEJA(3, prefix='r').gens()
2453 def __init__(self
, n
, field
=AA
, **kwargs
):
2455 jordan_product
= lambda x
,y
: x
2456 inner_product
= lambda x
,y
: x
2458 def jordan_product(x
,y
):
2460 return P( xi
*yi
for (xi
,yi
) in zip(x
,y
) )
2462 def inner_product(x
,y
):
2465 # New defaults for keyword arguments. Don't orthonormalize
2466 # because our basis is already orthonormal with respect to our
2467 # inner-product. Don't check the axioms, because we know this
2468 # is a valid EJA... but do double-check if the user passes
2469 # check_axioms=True. Note: we DON'T override the "check_field"
2470 # default here, because the user can pass in a field!
2471 if "orthonormalize" not in kwargs
: kwargs
["orthonormalize"] = False
2472 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2474 column_basis
= tuple( b
.column()
2475 for b
in FreeModule(field
, n
).basis() )
2476 super().__init
__(column_basis
,
2482 self
.rank
.set_cache(n
)
2485 self
.one
.set_cache( self
.zero() )
2487 self
.one
.set_cache( sum(self
.gens()) )
2490 def _max_random_instance_size():
2492 The maximum dimension of a random HadamardEJA.
2497 def random_instance(cls
, **kwargs
):
2499 Return a random instance of this type of algebra.
2501 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
() + 1)
2502 return cls(n
, **kwargs
)
2505 class BilinearFormEJA(RationalBasisEJA
, ConcreteEJA
):
2507 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
2508 with the half-trace inner product and jordan product ``x*y =
2509 (<Bx,y>,y_bar>, x0*y_bar + y0*x_bar)`` where `B = 1 \times B22` is
2510 a symmetric positive-definite "bilinear form" matrix. Its
2511 dimension is the size of `B`, and it has rank two in dimensions
2512 larger than two. It reduces to the ``JordanSpinEJA`` when `B` is
2513 the identity matrix of order ``n``.
2515 We insist that the one-by-one upper-left identity block of `B` be
2516 passed in as well so that we can be passed a matrix of size zero
2517 to construct a trivial algebra.
2521 sage: from mjo.eja.eja_algebra import (BilinearFormEJA,
2522 ....: JordanSpinEJA)
2526 When no bilinear form is specified, the identity matrix is used,
2527 and the resulting algebra is the Jordan spin algebra::
2529 sage: B = matrix.identity(AA,3)
2530 sage: J0 = BilinearFormEJA(B)
2531 sage: J1 = JordanSpinEJA(3)
2532 sage: J0.multiplication_table() == J0.multiplication_table()
2535 An error is raised if the matrix `B` does not correspond to a
2536 positive-definite bilinear form::
2538 sage: B = matrix.random(QQ,2,3)
2539 sage: J = BilinearFormEJA(B)
2540 Traceback (most recent call last):
2542 ValueError: bilinear form is not positive-definite
2543 sage: B = matrix.zero(QQ,3)
2544 sage: J = BilinearFormEJA(B)
2545 Traceback (most recent call last):
2547 ValueError: bilinear form is not positive-definite
2551 We can create a zero-dimensional algebra::
2553 sage: B = matrix.identity(AA,0)
2554 sage: J = BilinearFormEJA(B)
2558 We can check the multiplication condition given in the Jordan, von
2559 Neumann, and Wigner paper (and also discussed on my "On the
2560 symmetry..." paper). Note that this relies heavily on the standard
2561 choice of basis, as does anything utilizing the bilinear form
2562 matrix. We opt not to orthonormalize the basis, because if we
2563 did, we would have to normalize the `s_{i}` in a similar manner::
2565 sage: set_random_seed()
2566 sage: n = ZZ.random_element(5)
2567 sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular')
2568 sage: B11 = matrix.identity(QQ,1)
2569 sage: B22 = M.transpose()*M
2570 sage: B = block_matrix(2,2,[ [B11,0 ],
2572 sage: J = BilinearFormEJA(B, orthonormalize=False)
2573 sage: eis = VectorSpace(M.base_ring(), M.ncols()).basis()
2574 sage: V = J.vector_space()
2575 sage: sis = [ J( V([0] + (M.inverse()*ei).list()).column() )
2576 ....: for ei in eis ]
2577 sage: actual = [ sis[i]*sis[j]
2578 ....: for i in range(n-1)
2579 ....: for j in range(n-1) ]
2580 sage: expected = [ J.one() if i == j else J.zero()
2581 ....: for i in range(n-1)
2582 ....: for j in range(n-1) ]
2583 sage: actual == expected
2587 def __init__(self
, B
, field
=AA
, **kwargs
):
2588 # The matrix "B" is supplied by the user in most cases,
2589 # so it makes sense to check whether or not its positive-
2590 # definite unless we are specifically asked not to...
2591 if ("check_axioms" not in kwargs
) or kwargs
["check_axioms"]:
2592 if not B
.is_positive_definite():
2593 raise ValueError("bilinear form is not positive-definite")
2595 # However, all of the other data for this EJA is computed
2596 # by us in manner that guarantees the axioms are
2597 # satisfied. So, again, unless we are specifically asked to
2598 # verify things, we'll skip the rest of the checks.
2599 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2601 def inner_product(x
,y
):
2602 return (y
.T
*B
*x
)[0,0]
2604 def jordan_product(x
,y
):
2610 z0
= inner_product(y
,x
)
2611 zbar
= y0
*xbar
+ x0
*ybar
2612 return P([z0
] + zbar
.list())
2615 column_basis
= tuple( b
.column()
2616 for b
in FreeModule(field
, n
).basis() )
2618 # TODO: I haven't actually checked this, but it seems legit.
2623 super().__init
__(column_basis
,
2627 associative
=associative
,
2630 # The rank of this algebra is two, unless we're in a
2631 # one-dimensional ambient space (because the rank is bounded
2632 # by the ambient dimension).
2633 self
.rank
.set_cache(min(n
,2))
2636 self
.one
.set_cache( self
.zero() )
2638 self
.one
.set_cache( self
.monomial(0) )
2641 def _max_random_instance_size():
2643 The maximum dimension of a random BilinearFormEJA.
2648 def random_instance(cls
, **kwargs
):
2650 Return a random instance of this algebra.
2652 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
() + 1)
2654 B
= matrix
.identity(ZZ
, n
)
2655 return cls(B
, **kwargs
)
2657 B11
= matrix
.identity(ZZ
, 1)
2658 M
= matrix
.random(ZZ
, n
-1)
2659 I
= matrix
.identity(ZZ
, n
-1)
2661 while alpha
.is_zero():
2662 alpha
= ZZ
.random_element().abs()
2663 B22
= M
.transpose()*M
+ alpha
*I
2665 from sage
.matrix
.special
import block_matrix
2666 B
= block_matrix(2,2, [ [B11
, ZZ(0) ],
2669 return cls(B
, **kwargs
)
2672 class JordanSpinEJA(BilinearFormEJA
):
2674 The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)``
2675 with the usual inner product and jordan product ``x*y =
2676 (<x,y>, x0*y_bar + y0*x_bar)``. It has dimension `n` over
2681 sage: from mjo.eja.eja_algebra import JordanSpinEJA
2685 This multiplication table can be verified by hand::
2687 sage: J = JordanSpinEJA(4)
2688 sage: b0,b1,b2,b3 = J.gens()
2704 We can change the generator prefix::
2706 sage: JordanSpinEJA(2, prefix='B').gens()
2711 Ensure that we have the usual inner product on `R^n`::
2713 sage: set_random_seed()
2714 sage: J = JordanSpinEJA.random_instance()
2715 sage: x,y = J.random_elements(2)
2716 sage: actual = x.inner_product(y)
2717 sage: expected = x.to_vector().inner_product(y.to_vector())
2718 sage: actual == expected
2722 def __init__(self
, n
, *args
, **kwargs
):
2723 # This is a special case of the BilinearFormEJA with the
2724 # identity matrix as its bilinear form.
2725 B
= matrix
.identity(ZZ
, n
)
2727 # Don't orthonormalize because our basis is already
2728 # orthonormal with respect to our inner-product.
2729 if "orthonormalize" not in kwargs
: kwargs
["orthonormalize"] = False
2731 # But also don't pass check_field=False here, because the user
2732 # can pass in a field!
2733 super().__init
__(B
, *args
, **kwargs
)
2736 def _max_random_instance_size():
2738 The maximum dimension of a random JordanSpinEJA.
2743 def random_instance(cls
, **kwargs
):
2745 Return a random instance of this type of algebra.
2747 Needed here to override the implementation for ``BilinearFormEJA``.
2749 n
= ZZ
.random_element(cls
._max
_random
_instance
_size
() + 1)
2750 return cls(n
, **kwargs
)
2753 class TrivialEJA(RationalBasisEJA
, ConcreteEJA
):
2755 The trivial Euclidean Jordan algebra consisting of only a zero element.
2759 sage: from mjo.eja.eja_algebra import TrivialEJA
2763 sage: J = TrivialEJA()
2770 sage: 7*J.one()*12*J.one()
2772 sage: J.one().inner_product(J.one())
2774 sage: J.one().norm()
2776 sage: J.one().subalgebra_generated_by()
2777 Euclidean Jordan algebra of dimension 0 over Algebraic Real Field
2782 def __init__(self
, **kwargs
):
2783 jordan_product
= lambda x
,y
: x
2784 inner_product
= lambda x
,y
: 0
2787 # New defaults for keyword arguments
2788 if "orthonormalize" not in kwargs
: kwargs
["orthonormalize"] = False
2789 if "check_axioms" not in kwargs
: kwargs
["check_axioms"] = False
2791 super().__init
__(basis
,
2797 # The rank is zero using my definition, namely the dimension of the
2798 # largest subalgebra generated by any element.
2799 self
.rank
.set_cache(0)
2800 self
.one
.set_cache( self
.zero() )
2803 def random_instance(cls
, **kwargs
):
2804 # We don't take a "size" argument so the superclass method is
2805 # inappropriate for us.
2806 return cls(**kwargs
)
2809 class CartesianProductEJA(FiniteDimensionalEJA
):
2811 The external (orthogonal) direct sum of two or more Euclidean
2812 Jordan algebras. Every Euclidean Jordan algebra decomposes into an
2813 orthogonal direct sum of simple Euclidean Jordan algebras which is
2814 then isometric to a Cartesian product, so no generality is lost by
2815 providing only this construction.
2819 sage: from mjo.eja.eja_algebra import (random_eja,
2820 ....: CartesianProductEJA,
2822 ....: JordanSpinEJA,
2823 ....: RealSymmetricEJA)
2827 The Jordan product is inherited from our factors and implemented by
2828 our CombinatorialFreeModule Cartesian product superclass::
2830 sage: set_random_seed()
2831 sage: J1 = HadamardEJA(2)
2832 sage: J2 = RealSymmetricEJA(2)
2833 sage: J = cartesian_product([J1,J2])
2834 sage: x,y = J.random_elements(2)
2838 The ability to retrieve the original factors is implemented by our
2839 CombinatorialFreeModule Cartesian product superclass::
2841 sage: J1 = HadamardEJA(2, field=QQ)
2842 sage: J2 = JordanSpinEJA(3, field=QQ)
2843 sage: J = cartesian_product([J1,J2])
2844 sage: J.cartesian_factors()
2845 (Euclidean Jordan algebra of dimension 2 over Rational Field,
2846 Euclidean Jordan algebra of dimension 3 over Rational Field)
2848 You can provide more than two factors::
2850 sage: J1 = HadamardEJA(2)
2851 sage: J2 = JordanSpinEJA(3)
2852 sage: J3 = RealSymmetricEJA(3)
2853 sage: cartesian_product([J1,J2,J3])
2854 Euclidean Jordan algebra of dimension 2 over Algebraic Real
2855 Field (+) Euclidean Jordan algebra of dimension 3 over Algebraic
2856 Real Field (+) Euclidean Jordan algebra of dimension 6 over
2857 Algebraic Real Field
2859 Rank is additive on a Cartesian product::
2861 sage: J1 = HadamardEJA(1)
2862 sage: J2 = RealSymmetricEJA(2)
2863 sage: J = cartesian_product([J1,J2])
2864 sage: J1.rank.clear_cache()
2865 sage: J2.rank.clear_cache()
2866 sage: J.rank.clear_cache()
2869 sage: J.rank() == J1.rank() + J2.rank()
2872 The same rank computation works over the rationals, with whatever
2875 sage: J1 = HadamardEJA(1, field=QQ, orthonormalize=False)
2876 sage: J2 = RealSymmetricEJA(2, field=QQ, orthonormalize=False)
2877 sage: J = cartesian_product([J1,J2])
2878 sage: J1.rank.clear_cache()
2879 sage: J2.rank.clear_cache()
2880 sage: J.rank.clear_cache()
2883 sage: J.rank() == J1.rank() + J2.rank()
2886 The product algebra will be associative if and only if all of its
2887 components are associative::
2889 sage: J1 = HadamardEJA(2)
2890 sage: J1.is_associative()
2892 sage: J2 = HadamardEJA(3)
2893 sage: J2.is_associative()
2895 sage: J3 = RealSymmetricEJA(3)
2896 sage: J3.is_associative()
2898 sage: CP1 = cartesian_product([J1,J2])
2899 sage: CP1.is_associative()
2901 sage: CP2 = cartesian_product([J1,J3])
2902 sage: CP2.is_associative()
2905 Cartesian products of Cartesian products work::
2907 sage: J1 = JordanSpinEJA(1)
2908 sage: J2 = JordanSpinEJA(1)
2909 sage: J3 = JordanSpinEJA(1)
2910 sage: J = cartesian_product([J1,cartesian_product([J2,J3])])
2911 sage: J.multiplication_table()
2912 +----++----+----+----+
2913 | * || b0 | b1 | b2 |
2914 +====++====+====+====+
2915 | b0 || b0 | 0 | 0 |
2916 +----++----+----+----+
2917 | b1 || 0 | b1 | 0 |
2918 +----++----+----+----+
2919 | b2 || 0 | 0 | b2 |
2920 +----++----+----+----+
2921 sage: HadamardEJA(3).multiplication_table()
2922 +----++----+----+----+
2923 | * || b0 | b1 | b2 |
2924 +====++====+====+====+
2925 | b0 || b0 | 0 | 0 |
2926 +----++----+----+----+
2927 | b1 || 0 | b1 | 0 |
2928 +----++----+----+----+
2929 | b2 || 0 | 0 | b2 |
2930 +----++----+----+----+
2934 All factors must share the same base field::
2936 sage: J1 = HadamardEJA(2, field=QQ)
2937 sage: J2 = RealSymmetricEJA(2)
2938 sage: CartesianProductEJA((J1,J2))
2939 Traceback (most recent call last):
2941 ValueError: all factors must share the same base field
2943 The cached unit element is the same one that would be computed::
2945 sage: set_random_seed() # long time
2946 sage: J1 = random_eja() # long time
2947 sage: J2 = random_eja() # long time
2948 sage: J = cartesian_product([J1,J2]) # long time
2949 sage: actual = J.one() # long time
2950 sage: J.one.clear_cache() # long time
2951 sage: expected = J.one() # long time
2952 sage: actual == expected # long time
2956 Element
= FiniteDimensionalEJAElement
2959 def __init__(self
, factors
, **kwargs
):
2964 self
._sets
= factors
2966 field
= factors
[0].base_ring()
2967 if not all( J
.base_ring() == field
for J
in factors
):
2968 raise ValueError("all factors must share the same base field")
2970 associative
= all( f
.is_associative() for f
in factors
)
2972 MS
= self
.matrix_space()
2976 for b
in factors
[i
].matrix_basis():
2981 basis
= tuple( MS(b
) for b
in basis
)
2983 # Define jordan/inner products that operate on that matrix_basis.
2984 def jordan_product(x
,y
):
2986 (factors
[i
](x
[i
])*factors
[i
](y
[i
])).to_matrix()
2990 def inner_product(x
, y
):
2992 factors
[i
](x
[i
]).inner_product(factors
[i
](y
[i
]))
2996 # There's no need to check the field since it already came
2997 # from an EJA. Likewise the axioms are guaranteed to be
2998 # satisfied, unless the guy writing this class sucks.
3000 # If you want the basis to be orthonormalized, orthonormalize
3002 FiniteDimensionalEJA
.__init
__(self
,
3007 orthonormalize
=False,
3008 associative
=associative
,
3009 cartesian_product
=True,
3013 ones
= tuple(J
.one().to_matrix() for J
in factors
)
3014 self
.one
.set_cache(self(ones
))
3015 self
.rank
.set_cache(sum(J
.rank() for J
in factors
))
3017 def cartesian_factors(self
):
3018 # Copy/pasted from CombinatorialFreeModule_CartesianProduct.
3021 def cartesian_factor(self
, i
):
3023 Return the ``i``th factor of this algebra.
3025 return self
._sets
[i
]
3028 # Copy/pasted from CombinatorialFreeModule_CartesianProduct.
3029 from sage
.categories
.cartesian_product
import cartesian_product
3030 return cartesian_product
.symbol
.join("%s" % factor
3031 for factor
in self
._sets
)
3033 def matrix_space(self
):
3035 Return the space that our matrix basis lives in as a Cartesian
3038 We don't simply use the ``cartesian_product()`` functor here
3039 because it acts differently on SageMath MatrixSpaces and our
3040 custom MatrixAlgebras, which are CombinatorialFreeModules. We
3041 always want the result to be represented (and indexed) as
3046 sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA,
3048 ....: OctonionHermitianEJA,
3049 ....: RealSymmetricEJA)
3053 sage: J1 = HadamardEJA(1)
3054 sage: J2 = RealSymmetricEJA(2)
3055 sage: J = cartesian_product([J1,J2])
3056 sage: J.matrix_space()
3057 The Cartesian product of (Full MatrixSpace of 1 by 1 dense
3058 matrices over Algebraic Real Field, Full MatrixSpace of 2
3059 by 2 dense matrices over Algebraic Real Field)
3063 sage: J1 = ComplexHermitianEJA(1)
3064 sage: J2 = ComplexHermitianEJA(1)
3065 sage: J = cartesian_product([J1,J2])
3066 sage: J.one().to_matrix()[0]
3069 sage: J.one().to_matrix()[1]
3075 sage: J1 = OctonionHermitianEJA(1)
3076 sage: J2 = OctonionHermitianEJA(1)
3077 sage: J = cartesian_product([J1,J2])
3078 sage: J.one().to_matrix()[0]
3082 sage: J.one().to_matrix()[1]
3088 scalars
= self
.cartesian_factor(0).base_ring()
3090 # This category isn't perfect, but is good enough for what we
3092 cat
= MagmaticAlgebras(scalars
).FiniteDimensional().WithBasis()
3093 cat
= cat
.Unital().CartesianProducts()
3094 factors
= tuple( J
.matrix_space() for J
in self
.cartesian_factors() )
3096 from sage
.sets
.cartesian_product
import CartesianProduct
3097 return CartesianProduct(factors
, cat
)
3101 def cartesian_projection(self
, i
):
3105 sage: from mjo.eja.eja_algebra import (random_eja,
3106 ....: JordanSpinEJA,
3108 ....: RealSymmetricEJA,
3109 ....: ComplexHermitianEJA)
3113 The projection morphisms are Euclidean Jordan algebra
3116 sage: J1 = HadamardEJA(2)
3117 sage: J2 = RealSymmetricEJA(2)
3118 sage: J = cartesian_product([J1,J2])
3119 sage: J.cartesian_projection(0)
3120 Linear operator between finite-dimensional Euclidean Jordan
3121 algebras represented by the matrix:
3124 Domain: Euclidean Jordan algebra of dimension 2 over Algebraic
3125 Real Field (+) Euclidean Jordan algebra of dimension 3 over
3126 Algebraic Real Field
3127 Codomain: Euclidean Jordan algebra of dimension 2 over Algebraic
3129 sage: J.cartesian_projection(1)
3130 Linear operator between finite-dimensional Euclidean Jordan
3131 algebras represented by the matrix:
3135 Domain: Euclidean Jordan algebra of dimension 2 over Algebraic
3136 Real Field (+) Euclidean Jordan algebra of dimension 3 over
3137 Algebraic Real Field
3138 Codomain: Euclidean Jordan algebra of dimension 3 over Algebraic
3141 The projections work the way you'd expect on the vector
3142 representation of an element::
3144 sage: J1 = JordanSpinEJA(2)
3145 sage: J2 = ComplexHermitianEJA(2)
3146 sage: J = cartesian_product([J1,J2])
3147 sage: pi_left = J.cartesian_projection(0)
3148 sage: pi_right = J.cartesian_projection(1)
3149 sage: pi_left(J.one()).to_vector()
3151 sage: pi_right(J.one()).to_vector()
3153 sage: J.one().to_vector()
3158 The answer never changes::
3160 sage: set_random_seed()
3161 sage: J1 = random_eja()
3162 sage: J2 = random_eja()
3163 sage: J = cartesian_product([J1,J2])
3164 sage: P0 = J.cartesian_projection(0)
3165 sage: P1 = J.cartesian_projection(0)
3170 offset
= sum( self
.cartesian_factor(k
).dimension()
3172 Ji
= self
.cartesian_factor(i
)
3173 Pi
= self
._module
_morphism
(lambda j
: Ji
.monomial(j
- offset
),
3176 return FiniteDimensionalEJAOperator(self
,Ji
,Pi
.matrix())
3179 def cartesian_embedding(self
, i
):
3183 sage: from mjo.eja.eja_algebra import (random_eja,
3184 ....: JordanSpinEJA,
3186 ....: RealSymmetricEJA)
3190 The embedding morphisms are Euclidean Jordan algebra
3193 sage: J1 = HadamardEJA(2)
3194 sage: J2 = RealSymmetricEJA(2)
3195 sage: J = cartesian_product([J1,J2])
3196 sage: J.cartesian_embedding(0)
3197 Linear operator between finite-dimensional Euclidean Jordan
3198 algebras represented by the matrix:
3204 Domain: Euclidean Jordan algebra of dimension 2 over
3205 Algebraic Real Field
3206 Codomain: Euclidean Jordan algebra of dimension 2 over
3207 Algebraic Real Field (+) Euclidean Jordan algebra of
3208 dimension 3 over Algebraic Real Field
3209 sage: J.cartesian_embedding(1)
3210 Linear operator between finite-dimensional Euclidean Jordan
3211 algebras represented by the matrix:
3217 Domain: Euclidean Jordan algebra of dimension 3 over
3218 Algebraic Real Field
3219 Codomain: Euclidean Jordan algebra of dimension 2 over
3220 Algebraic Real Field (+) Euclidean Jordan algebra of
3221 dimension 3 over Algebraic Real Field
3223 The embeddings work the way you'd expect on the vector
3224 representation of an element::
3226 sage: J1 = JordanSpinEJA(3)
3227 sage: J2 = RealSymmetricEJA(2)
3228 sage: J = cartesian_product([J1,J2])
3229 sage: iota_left = J.cartesian_embedding(0)
3230 sage: iota_right = J.cartesian_embedding(1)
3231 sage: iota_left(J1.zero()) == J.zero()
3233 sage: iota_right(J2.zero()) == J.zero()
3235 sage: J1.one().to_vector()
3237 sage: iota_left(J1.one()).to_vector()
3239 sage: J2.one().to_vector()
3241 sage: iota_right(J2.one()).to_vector()
3243 sage: J.one().to_vector()
3248 The answer never changes::
3250 sage: set_random_seed()
3251 sage: J1 = random_eja()
3252 sage: J2 = random_eja()
3253 sage: J = cartesian_product([J1,J2])
3254 sage: E0 = J.cartesian_embedding(0)
3255 sage: E1 = J.cartesian_embedding(0)
3259 Composing a projection with the corresponding inclusion should
3260 produce the identity map, and mismatching them should produce
3263 sage: set_random_seed()
3264 sage: J1 = random_eja()
3265 sage: J2 = random_eja()
3266 sage: J = cartesian_product([J1,J2])
3267 sage: iota_left = J.cartesian_embedding(0)
3268 sage: iota_right = J.cartesian_embedding(1)
3269 sage: pi_left = J.cartesian_projection(0)
3270 sage: pi_right = J.cartesian_projection(1)
3271 sage: pi_left*iota_left == J1.one().operator()
3273 sage: pi_right*iota_right == J2.one().operator()
3275 sage: (pi_left*iota_right).is_zero()
3277 sage: (pi_right*iota_left).is_zero()
3281 offset
= sum( self
.cartesian_factor(k
).dimension()
3283 Ji
= self
.cartesian_factor(i
)
3284 Ei
= Ji
._module
_morphism
(lambda j
: self
.monomial(j
+ offset
),
3286 return FiniteDimensionalEJAOperator(Ji
,self
,Ei
.matrix())
3290 FiniteDimensionalEJA
.CartesianProduct
= CartesianProductEJA
3292 class RationalBasisCartesianProductEJA(CartesianProductEJA
,
3295 A separate class for products of algebras for which we know a
3300 sage: from mjo.eja.eja_algebra import (HadamardEJA,
3301 ....: JordanSpinEJA,
3302 ....: OctonionHermitianEJA,
3303 ....: RealSymmetricEJA)
3307 This gives us fast characteristic polynomial computations in
3308 product algebras, too::
3311 sage: J1 = JordanSpinEJA(2)
3312 sage: J2 = RealSymmetricEJA(3)
3313 sage: J = cartesian_product([J1,J2])
3314 sage: J.characteristic_polynomial_of().degree()
3321 The ``cartesian_product()`` function only uses the first factor to
3322 decide where the result will live; thus we have to be careful to
3323 check that all factors do indeed have a `_rational_algebra` member
3324 before we try to access it::
3326 sage: J1 = OctonionHermitianEJA(1) # no rational basis
3327 sage: J2 = HadamardEJA(2)
3328 sage: cartesian_product([J1,J2])
3329 Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
3330 (+) Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
3331 sage: cartesian_product([J2,J1])
3332 Euclidean Jordan algebra of dimension 2 over Algebraic Real Field
3333 (+) Euclidean Jordan algebra of dimension 1 over Algebraic Real Field
3336 def __init__(self
, algebras
, **kwargs
):
3337 CartesianProductEJA
.__init
__(self
, algebras
, **kwargs
)
3339 self
._rational
_algebra
= None
3340 if self
.vector_space().base_field() is not QQ
:
3341 if all( hasattr(r
, "_rational_algebra") for r
in algebras
):
3342 self
._rational
_algebra
= cartesian_product([
3343 r
._rational
_algebra
for r
in algebras
3347 RationalBasisEJA
.CartesianProduct
= RationalBasisCartesianProductEJA
3349 def random_eja(*args
, **kwargs
):
3350 J1
= ConcreteEJA
.random_instance(*args
, **kwargs
)
3352 # This might make Cartesian products appear roughly as often as
3353 # any other ConcreteEJA.
3354 if ZZ
.random_element(len(ConcreteEJA
.__subclasses
__()) + 1) == 0:
3355 # Use random_eja() again so we can get more than two factors.
3356 J2
= random_eja(*args
, **kwargs
)
3357 J
= cartesian_product([J1
,J2
])