X-Git-Url: http://gitweb.michael.orlitzky.com/?p=sage.d.git;a=blobdiff_plain;f=mjo%2Feja%2Feja_algebra.py;h=af4080b0807d6ac6848849f23edd237657896413;hp=3027075374a177ba5f72bbd690a2c07dad02d136;hb=6d6af7c2560b2886cd47a2c8f3c0b9d1b843f649;hpb=9861c4860c4bb1158243c06544cb54d1d9e3b411 diff --git a/mjo/eja/eja_algebra.py b/mjo/eja/eja_algebra.py index 3027075..af4080b 100644 --- a/mjo/eja/eja_algebra.py +++ b/mjo/eja/eja_algebra.py @@ -52,6 +52,98 @@ the other algebras. Cartesian products of these are also supported using the usual ``cartesian_product()`` function; as a result, we support (up to isomorphism) all Euclidean Jordan algebras. +At a minimum, the following are required to construct a Euclidean +Jordan algebra: + + * A basis of matrices, column vectors, or MatrixAlgebra elements + * A Jordan product defined on the basis + * Its inner product defined on the basis + +The real numbers form a Euclidean Jordan algebra when both the Jordan +and inner products are the usual multiplication. We use this as our +example, and demonstrate a few ways to construct an EJA. + +First, we can use one-by-one SageMath matrices with algebraic real +entries to represent real numbers. We define the Jordan and inner +products to be essentially real-number multiplication, with the only +difference being that the Jordan product again returns a one-by-one +matrix, whereas the inner product must return a scalar. Our basis for +the one-by-one matrices is of course the set consisting of a single +matrix with its sole entry non-zero:: + + sage: from mjo.eja.eja_algebra import FiniteDimensionalEJA + sage: jp = lambda X,Y: X*Y + sage: ip = lambda X,Y: X[0,0]*Y[0,0] + sage: b1 = matrix(AA, [[1]]) + sage: J1 = FiniteDimensionalEJA((b1,), jp, ip) + sage: J1 + Euclidean Jordan algebra of dimension 1 over Algebraic Real Field + +In fact, any positive scalar multiple of that inner-product would work:: + + sage: ip2 = lambda X,Y: 16*ip(X,Y) + sage: J2 = FiniteDimensionalEJA((b1,), jp, ip2) + sage: J2 + Euclidean Jordan algebra of dimension 1 over Algebraic Real Field + +But beware that your basis will be orthonormalized _with respect to the +given inner-product_ unless you pass ``orthonormalize=False`` to the +constructor. For example:: + + sage: J3 = FiniteDimensionalEJA((b1,), jp, ip2, orthonormalize=False) + sage: J3 + Euclidean Jordan algebra of dimension 1 over Algebraic Real Field + +To see the difference, you can take the first and only basis element +of the resulting algebra, and ask for it to be converted back into +matrix form:: + + sage: J1.basis()[0].to_matrix() + [1] + sage: J2.basis()[0].to_matrix() + [1/4] + sage: J3.basis()[0].to_matrix() + [1] + +Since square roots are used in that process, the default scalar field +that we use is the field of algebraic real numbers, ``AA``. You can +also Use rational numbers, but only if you either pass +``orthonormalize=False`` or know that orthonormalizing your basis +won't stray beyond the rational numbers. The example above would +have worked only because ``sqrt(16) == 4`` is rational. + +Another option for your basis is to use elemebts of a +:class:`MatrixAlgebra`:: + + sage: from mjo.matrix_algebra import MatrixAlgebra + sage: A = MatrixAlgebra(1,AA,AA) + sage: J4 = FiniteDimensionalEJA(A.gens(), jp, ip) + sage: J4 + Euclidean Jordan algebra of dimension 1 over Algebraic Real Field + sage: J4.basis()[0].to_matrix() + +---+ + | 1 | + +---+ + +An easier way to view the entire EJA basis in its original (but +perhaps orthonormalized) matrix form is to use the ``matrix_basis`` +method:: + + sage: J4.matrix_basis() + (+---+ + | 1 | + +---+,) + +In particular, a :class:`MatrixAlgebra` is needed to work around the +fact that matrices in SageMath must have entries in the same +(commutative and associative) ring as its scalars. There are many +Euclidean Jordan algebras whose elements are matrices that violate +those assumptions. The complex, quaternion, and octonion Hermitian +matrices all have entries in a ring (the complex numbers, quaternions, +or octonions...) that differs from the algebra's scalar ring (the real +numbers). Quaternions are also non-commutative; the octonions are +neither commutative nor associative. + SETUP:: sage: from mjo.eja.eja_algebra import random_eja @@ -78,6 +170,17 @@ from mjo.eja.eja_element import FiniteDimensionalEJAElement from mjo.eja.eja_operator import FiniteDimensionalEJAOperator from mjo.eja.eja_utils import _all2list, _mat2vec +def EuclideanJordanAlgebras(field): + r""" + The category of Euclidean Jordan algebras over ``field``, which + must be a subfield of the real numbers. For now this is just a + convenient wrapper around all of the other category axioms that + apply to all EJAs. + """ + category = MagmaticAlgebras(field).FiniteDimensional() + category = category.WithBasis().Unital().Commutative() + return category + class FiniteDimensionalEJA(CombinatorialFreeModule): r""" A finite-dimensional Euclidean Jordan algebra. @@ -104,6 +207,11 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): product. This will be applied to ``basis`` to compute an inner-product table (basically a matrix) for this algebra. + - ``matrix_space`` -- the space that your matrix basis lives in, + or ``None`` (the default). So long as your basis does not have + length zero you can omit this. But in trivial algebras, it is + required. + - ``field`` -- a subfield of the reals (default: ``AA``); the scalar field for the algebra. @@ -128,18 +236,37 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): sage: basis = tuple(b.superalgebra_element() for b in A.basis()) sage: J.subalgebra(basis, orthonormalize=False).is_associative() True - """ Element = FiniteDimensionalEJAElement + @staticmethod + def _check_input_field(field): + if not field.is_subring(RR): + # Note: this does return true for the real algebraic + # field, the rationals, and any quadratic field where + # we've specified a real embedding. + raise ValueError("scalar field is not real") + + @staticmethod + def _check_input_axioms(basis, jordan_product, inner_product): + if not all( jordan_product(bi,bj) == jordan_product(bj,bi) + for bi in basis + for bj in basis ): + raise ValueError("Jordan product is not commutative") + + if not all( inner_product(bi,bj) == inner_product(bj,bi) + for bi in basis + for bj in basis ): + raise ValueError("inner-product is not commutative") + def __init__(self, basis, jordan_product, inner_product, field=AA, + matrix_space=None, orthonormalize=True, associative=None, - cartesian_product=False, check_field=True, check_axioms=True, prefix="b"): @@ -147,31 +274,20 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): n = len(basis) if check_field: - if not field.is_subring(RR): - # Note: this does return true for the real algebraic - # field, the rationals, and any quadratic field where - # we've specified a real embedding. - raise ValueError("scalar field is not real") + self._check_input_field(field) if check_axioms: # Check commutativity of the Jordan and inner-products. # This has to be done before we build the multiplication # and inner-product tables/matrices, because we take # advantage of symmetry in the process. - if not all( jordan_product(bi,bj) == jordan_product(bj,bi) - for bi in basis - for bj in basis ): - raise ValueError("Jordan product is not commutative") - - if not all( inner_product(bi,bj) == inner_product(bj,bi) - for bi in basis - for bj in basis ): - raise ValueError("inner-product is not commutative") - - - category = MagmaticAlgebras(field).FiniteDimensional() - category = category.WithBasis().Unital().Commutative() + self._check_input_axioms(basis, jordan_product, inner_product) + if n <= 1: + # All zero- and one-dimensional algebras are just the real + # numbers with (some positive multiples of) the usual + # multiplication as its Jordan and inner-product. + associative = True if associative is None: # We should figure it out. As with check_axioms, we have to do # this without the help of the _jordan_product_is_associative() @@ -184,14 +300,11 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): for bj in basis for bk in basis) + category = EuclideanJordanAlgebras(field) + if associative: # Element subalgebras can take advantage of this. category = category.Associative() - if cartesian_product: - # Use join() here because otherwise we only get the - # "Cartesian product of..." and not the things themselves. - category = category.join([category, - category.CartesianProducts()]) # Call the superclass constructor so that we can use its from_vector() # method to build our multiplication table. @@ -207,7 +320,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): # as well as a subspace W of V spanned by those (vectorized) # basis elements. The W-coordinates are the coefficients that # we see in things like x = 1*b1 + 2*b2. - vector_basis = basis degree = 0 if n > 0: @@ -217,9 +329,11 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): # written out as "long vectors." V = VectorSpace(field, degree) - # The matrix that will hole the orthonormal -> unorthonormal - # coordinate transformation. - self._deortho_matrix = None + # The matrix that will hold the orthonormal -> unorthonormal + # coordinate transformation. Default to an identity matrix of + # the appropriate size to avoid special cases for None + # everywhere. + self._deortho_matrix = matrix.identity(field,n) if orthonormalize: # Save a copy of the un-orthonormalized basis for later. @@ -231,30 +345,42 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): basis = tuple(gram_schmidt(basis, inner_product)) # Save the (possibly orthonormalized) matrix basis for - # later... + # later, as well as the space that its elements live in. + # In most cases we can deduce the matrix space, but when + # n == 0 (that is, there are no basis elements) we cannot. self._matrix_basis = basis + if matrix_space is None: + self._matrix_space = self._matrix_basis[0].parent() + else: + self._matrix_space = matrix_space # Now create the vector space for the algebra, which will have # its own set of non-ambient coordinates (in terms of the # supplied basis). vector_basis = tuple( V(_all2list(b)) for b in basis ) - W = V.span_of_basis( vector_basis, check=check_axioms) + + # Save the span of our matrix basis (when written out as long + # vectors) because otherwise we'll have to reconstruct it + # every time we want to coerce a matrix into the algebra. + self._matrix_span = V.span_of_basis( vector_basis, check=check_axioms) if orthonormalize: - # Now "W" is the vector space of our algebra coordinates. The - # variables "X1", "X2",... refer to the entries of vectors in - # W. Thus to convert back and forth between the orthonormal - # coordinates and the given ones, we need to stick the original - # basis in W. + # Now "self._matrix_span" is the vector space of our + # algebra coordinates. The variables "X1", "X2",... refer + # to the entries of vectors in self._matrix_span. Thus to + # convert back and forth between the orthonormal + # coordinates and the given ones, we need to stick the + # original basis in self._matrix_span. U = V.span_of_basis( deortho_vector_basis, check=check_axioms) - self._deortho_matrix = matrix( U.coordinate_vector(q) - for q in vector_basis ) + self._deortho_matrix = matrix.column( U.coordinate_vector(q) + for q in vector_basis ) # Now we actually compute the multiplication and inner-product # tables/matrices using the possibly-orthonormalized basis. self._inner_product_matrix = matrix.identity(field, n) - self._multiplication_table = [ [0 for j in range(i+1)] + zed = self.zero() + self._multiplication_table = [ [zed for j in range(i+1)] for i in range(n) ] # Note: the Jordan and inner-products are defined in terms @@ -269,7 +395,7 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): # The jordan product returns a matrixy answer, so we # have to convert it to the algebra coordinates. elt = jordan_product(q_i, q_j) - elt = W.coordinate_vector(V(_all2list(elt))) + elt = self._matrix_span.coordinate_vector(V(_all2list(elt))) self._multiplication_table[i][j] = self.from_vector(elt) if not orthonormalize: @@ -576,8 +702,8 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): def _element_constructor_(self, elt): """ - Construct an element of this algebra from its vector or matrix - representation. + Construct an element of this algebra or a subalgebra from its + EJA element, vector, or matrix representation. This gets called only after the parent element _call_ method fails to find a coercion for the argument. @@ -616,6 +742,16 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): sage: J( (J1.matrix_basis()[1], J2.matrix_basis()[2]) ) b1 + b5 + Subalgebra elements are embedded into the superalgebra:: + + sage: J = JordanSpinEJA(3) + sage: J.one() + b0 + sage: x = sum(J.gens()) + sage: A = x.subalgebra_generated_by() + sage: J(A.one()) + b0 + TESTS: Ensure that we can convert any element back and forth @@ -640,6 +776,7 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): Traceback (most recent call last): ... ValueError: not an element of this algebra + """ msg = "not an element of this algebra" if elt in self.base_ring(): @@ -649,13 +786,16 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): # that the integer 3 belongs to the space of 2-by-2 matrices. raise ValueError(msg) - try: - # Try to convert a vector into a column-matrix... + if hasattr(elt, 'superalgebra_element'): + # Handle subalgebra elements + if elt.parent().superalgebra() == self: + return elt.superalgebra_element() + + if hasattr(elt, 'sparse_vector'): + # Convert a vector into a column-matrix. We check for + # "sparse_vector" and not "column" because matrices also + # have a "column" method. elt = elt.column() - except (AttributeError, TypeError): - # and ignore failure, because we weren't really expecting - # a vector as an argument anyway. - pass if elt not in self.matrix_space(): raise ValueError(msg) @@ -672,15 +812,10 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): # is that we're already converting everything to long vectors, # and that strategy works for tuples as well. # - # We pass check=False because the matrix basis is "guaranteed" - # to be linearly independent... right? Ha ha. - elt = _all2list(elt) - V = VectorSpace(self.base_ring(), len(elt)) - W = V.span_of_basis( (V(_all2list(s)) for s in self.matrix_basis()), - check=False) + elt = self._matrix_span.ambient_vector_space()(_all2list(elt)) try: - coords = W.coordinate_vector(V(elt)) + coords = self._matrix_span.coordinate_vector(elt) except ArithmeticError: # vector is not in free module raise ValueError(msg) @@ -1016,7 +1151,8 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False) sage: J.matrix_space() - Full MatrixSpace of 4 by 4 dense matrices over Rational Field + Module of 2 by 2 matrices with entries in Algebraic Field over + the scalar ring Rational Field sage: J = QuaternionHermitianEJA(1,field=QQ,orthonormalize=False) sage: J.matrix_space() Module of 1 by 1 matrices with entries in Quaternion @@ -1024,10 +1160,7 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): the scalar ring Rational Field """ - if self.is_trivial(): - return MatrixSpace(self.base_ring(), 0) - else: - return self.matrix_basis()[0].parent() + return self._matrix_space @cached_method @@ -1288,7 +1421,7 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): # corresponding to trivial spaces (e.g. it returns only the # eigenspace corresponding to lambda=1 if you take the # decomposition relative to the identity element). - trivial = self.subalgebra(()) + trivial = self.subalgebra((), check_axioms=False) J0 = trivial # eigenvalue zero J5 = VectorSpace(self.base_ring(), 0) # eigenvalue one-half J1 = trivial # eigenvalue one @@ -1604,11 +1737,21 @@ class RationalBasisEJA(FiniteDimensionalEJA): jordan_product, inner_product, field=QQ, + matrix_space=self.matrix_space(), associative=self.is_associative(), orthonormalize=False, check_field=False, check_axioms=False) + def rational_algebra(self): + # Using None as a flag here (rather than just assigning "self" + # to self._rational_algebra by default) feels a little bit + # more sane to me in a garbage-collected environment. + if self._rational_algebra is None: + return self + else: + return self._rational_algebra + @cached_method def _charpoly_coefficients(self): r""" @@ -1633,25 +1776,15 @@ class RationalBasisEJA(FiniteDimensionalEJA): Algebraic Real Field """ - if self._rational_algebra is None: - # There's no need to construct *another* algebra over the - # rationals if this one is already over the - # rationals. Likewise, if we never orthonormalized our - # basis, we might as well just use the given one. + if self.rational_algebra() is self: + # Bypass the hijinks if they won't benefit us. return super()._charpoly_coefficients() # Do the computation over the rationals. The answer will be # the same, because all we've done is a change of basis. # Then, change back from QQ to our real base ring a = ( a_i.change_ring(self.base_ring()) - for a_i in self._rational_algebra._charpoly_coefficients() ) - - if self._deortho_matrix is None: - # This can happen if our base ring was, say, AA and we - # chose not to (or didn't need to) orthonormalize. It's - # still faster to do the computations over QQ even if - # the numbers in the boxes stay the same. - return tuple(a) + for a_i in self.rational_algebra()._charpoly_coefficients() ) # Otherwise, convert the coordinate variables back to the # deorthonormalized ones. @@ -1700,27 +1833,62 @@ class ConcreteEJA(FiniteDimensionalEJA): """ @staticmethod - def _max_random_instance_size(): + def _max_random_instance_dimension(): + r""" + The maximum dimension of any random instance. Ten dimensions seems + to be about the point where everything takes a turn for the + worse. And dimension ten (but not nine) allows the 4-by-4 real + Hermitian matrices, the 2-by-2 quaternion Hermitian matrices, + and the 2-by-2 octonion Hermitian matrices. + """ + return 10 + + @staticmethod + def _max_random_instance_size(max_dimension): """ Return an integer "size" that is an upper bound on the size of - this algebra when it is used in a random test - case. Unfortunately, the term "size" is ambiguous -- when - dealing with `R^n` under either the Hadamard or Jordan spin - product, the "size" refers to the dimension `n`. When dealing - with a matrix algebra (real symmetric or complex/quaternion - Hermitian), it refers to the size of the matrix, which is far - less than the dimension of the underlying vector space. + this algebra when it is used in a random test case. This size + (which can be passed to the algebra's constructor) is itself + based on the ``max_dimension`` parameter. This method must be implemented in each subclass. """ raise NotImplementedError @classmethod - def random_instance(cls, *args, **kwargs): + def random_instance(cls, max_dimension=None, *args, **kwargs): """ - Return a random instance of this type of algebra. + Return a random instance of this type of algebra whose dimension + is less than or equal to the lesser of ``max_dimension`` and + the value returned by ``_max_random_instance_dimension()``. If + the dimension bound is omitted, then only the + ``_max_random_instance_dimension()`` is used as a bound. This method should be implemented in each subclass. + + SETUP:: + + sage: from mjo.eja.eja_algebra import ConcreteEJA + + TESTS: + + Both the class bound and the ``max_dimension`` argument are upper + bounds on the dimension of the algebra returned:: + + sage: from sage.misc.prandom import choice + sage: eja_class = choice(ConcreteEJA.__subclasses__()) + sage: class_max_d = eja_class._max_random_instance_dimension() + sage: J = eja_class.random_instance(max_dimension=20, + ....: field=QQ, + ....: orthonormalize=False) + sage: J.dimension() <= class_max_d + True + sage: J = eja_class.random_instance(max_dimension=2, + ....: field=QQ, + ....: orthonormalize=False) + sage: J.dimension() <= 2 + True + """ from sage.misc.prandom import choice eja_class = choice(cls.__subclasses__()) @@ -1728,10 +1896,90 @@ class ConcreteEJA(FiniteDimensionalEJA): # These all bubble up to the RationalBasisEJA superclass # constructor, so any (kw)args valid there are also valid # here. - return eja_class.random_instance(*args, **kwargs) + return eja_class.random_instance(max_dimension, *args, **kwargs) -class MatrixEJA: +class MatrixEJA(FiniteDimensionalEJA): + @staticmethod + def _denormalized_basis(A): + """ + Returns a basis for the space of complex Hermitian n-by-n matrices. + + Why do we embed these? Basically, because all of numerical linear + algebra assumes that you're working with vectors consisting of `n` + entries from a field and scalars from the same field. There's no way + to tell SageMath that (for example) the vectors contain complex + numbers, while the scalar field is real. + + SETUP:: + + sage: from mjo.hurwitz import (ComplexMatrixAlgebra, + ....: QuaternionMatrixAlgebra, + ....: OctonionMatrixAlgebra) + sage: from mjo.eja.eja_algebra import MatrixEJA + + TESTS:: + + sage: set_random_seed() + sage: n = ZZ.random_element(1,5) + sage: A = MatrixSpace(QQ, n) + sage: B = MatrixEJA._denormalized_basis(A) + sage: all( M.is_hermitian() for M in B) + True + + :: + + sage: set_random_seed() + sage: n = ZZ.random_element(1,5) + sage: A = ComplexMatrixAlgebra(n, scalars=QQ) + sage: B = MatrixEJA._denormalized_basis(A) + sage: all( M.is_hermitian() for M in B) + True + + :: + + sage: set_random_seed() + sage: n = ZZ.random_element(1,5) + sage: A = QuaternionMatrixAlgebra(n, scalars=QQ) + sage: B = MatrixEJA._denormalized_basis(A) + sage: all( M.is_hermitian() for M in B ) + True + + :: + + sage: set_random_seed() + sage: n = ZZ.random_element(1,5) + sage: A = OctonionMatrixAlgebra(n, scalars=QQ) + sage: B = MatrixEJA._denormalized_basis(A) + sage: all( M.is_hermitian() for M in B ) + True + + """ + # These work for real MatrixSpace, whose monomials only have + # two coordinates (because the last one would always be "1"). + es = A.base_ring().gens() + gen = lambda A,m: A.monomial(m[:2]) + + if hasattr(A, 'entry_algebra_gens'): + # We've got a MatrixAlgebra, and its monomials will have + # three coordinates. + es = A.entry_algebra_gens() + gen = lambda A,m: A.monomial(m) + + basis = [] + for i in range(A.nrows()): + for j in range(i+1): + if i == j: + E_ii = gen(A, (i,j,es[0])) + basis.append(E_ii) + else: + for e in es: + E_ij = gen(A, (i,j,e)) + E_ij += E_ij.conjugate_transpose() + basis.append(E_ij) + + return tuple( basis ) + @staticmethod def jordan_product(X,Y): return (X*Y + Y*X)/2 @@ -1741,11 +1989,74 @@ class MatrixEJA: r""" A trace inner-product for matrices that aren't embedded in the reals. It takes MATRICES as arguments, not EJA elements. + + SETUP:: + + sage: from mjo.eja.eja_algebra import (RealSymmetricEJA, + ....: ComplexHermitianEJA, + ....: QuaternionHermitianEJA, + ....: OctonionHermitianEJA) + + EXAMPLES:: + + sage: J = RealSymmetricEJA(2,field=QQ,orthonormalize=False) + sage: I = J.one().to_matrix() + sage: J.trace_inner_product(I, -I) + -2 + + :: + + sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False) + sage: I = J.one().to_matrix() + sage: J.trace_inner_product(I, -I) + -2 + + :: + + sage: J = QuaternionHermitianEJA(2,field=QQ,orthonormalize=False) + sage: I = J.one().to_matrix() + sage: J.trace_inner_product(I, -I) + -2 + + :: + + sage: J = OctonionHermitianEJA(2,field=QQ,orthonormalize=False) + sage: I = J.one().to_matrix() + sage: J.trace_inner_product(I, -I) + -2 + """ - return (X*Y).trace().real() + tr = (X*Y).trace() + if hasattr(tr, 'coefficient'): + # Works for octonions, and has to come first because they + # also have a "real()" method that doesn't return an + # element of the scalar ring. + return tr.coefficient(0) + elif hasattr(tr, 'coefficient_tuple'): + # Works for quaternions. + return tr.coefficient_tuple()[0] + + # Works for real and complex numbers. + return tr.real() -class RealSymmetricEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA): + def __init__(self, matrix_space, **kwargs): + # We know this is a valid EJA, but will double-check + # if the user passes check_axioms=True. + if "check_axioms" not in kwargs: kwargs["check_axioms"] = False + + + super().__init__(self._denormalized_basis(matrix_space), + self.jordan_product, + self.trace_inner_product, + field=matrix_space.base_ring(), + matrix_space=matrix_space, + **kwargs) + + self.rank.set_cache(matrix_space.nrows()) + self.one.set_cache( self(matrix_space.one()) ) + +class RealSymmetricEJA(MatrixEJA, RationalBasisEJA, ConcreteEJA): """ The rank-n simple EJA consisting of real symmetric n-by-n matrices, the usual symmetric Jordan product, and the trace inner @@ -1779,8 +2090,8 @@ class RealSymmetricEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA): The dimension of this algebra is `(n^2 + n) / 2`:: sage: set_random_seed() - sage: n_max = RealSymmetricEJA._max_random_instance_size() - sage: n = ZZ.random_element(1, n_max) + sage: d = RealSymmetricEJA._max_random_instance_dimension() + sage: n = RealSymmetricEJA._max_random_instance_size(d) sage: J = RealSymmetricEJA(n) sage: J.dimension() == (n^2 + n)/2 True @@ -1810,48 +2121,22 @@ class RealSymmetricEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA): Euclidean Jordan algebra of dimension 0 over Algebraic Real Field """ - @classmethod - def _denormalized_basis(cls, n, field): - """ - Return a basis for the space of real symmetric n-by-n matrices. - - SETUP:: - - sage: from mjo.eja.eja_algebra import RealSymmetricEJA - - TESTS:: - - sage: set_random_seed() - sage: n = ZZ.random_element(1,5) - sage: B = RealSymmetricEJA._denormalized_basis(n,ZZ) - sage: all( M.is_symmetric() for M in B) - True - - """ - # The basis of symmetric matrices, as matrices, in their R^(n-by-n) - # coordinates. - S = [] - for i in range(n): - for j in range(i+1): - Eij = matrix(field, n, lambda k,l: k==i and l==j) - if i == j: - Sij = Eij - else: - Sij = Eij + Eij.transpose() - S.append(Sij) - return tuple(S) - - @staticmethod - def _max_random_instance_size(): - return 4 # Dimension 10 + def _max_random_instance_size(max_dimension): + # Obtained by solving d = (n^2 + n)/2. + # The ZZ-int-ZZ thing is just "floor." + return ZZ(int(ZZ(8*max_dimension + 1).sqrt()/2 - 1/2)) @classmethod - def random_instance(cls, **kwargs): + def random_instance(cls, max_dimension=None, *args, **kwargs): """ Return a random instance of this type of algebra. """ - n = ZZ.random_element(cls._max_random_instance_size() + 1) + class_max_d = cls._max_random_instance_dimension() + if (max_dimension is None or max_dimension > class_max_d): + max_dimension = class_max_d + max_size = cls._max_random_instance_size(max_dimension) + n = ZZ.random_element(max_size + 1) return cls(n, **kwargs) def __init__(self, n, field=AA, **kwargs): @@ -1859,27 +2144,17 @@ class RealSymmetricEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA): # if the user passes check_axioms=True. if "check_axioms" not in kwargs: kwargs["check_axioms"] = False - associative = False - if n <= 1: - associative = True + A = MatrixSpace(field, n) + super().__init__(A, **kwargs) - super().__init__(self._denormalized_basis(n,field), - self.jordan_product, - self.trace_inner_product, - field=field, - associative=associative, - **kwargs) - - # TODO: this could be factored out somehow, but is left here - # because the MatrixEJA is not presently a subclass of the - # FDEJA class that defines rank() and one(). - self.rank.set_cache(n) - idV = self.matrix_space().one() - self.one.set_cache(self(idV)) + from mjo.eja.eja_cache import real_symmetric_eja_coeffs + a = real_symmetric_eja_coeffs(self) + if a is not None: + self.rational_algebra()._charpoly_coefficients.set_cache(a) -class ComplexHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA): +class ComplexHermitianEJA(MatrixEJA, RationalBasisEJA, ConcreteEJA): """ The rank-n simple EJA consisting of complex Hermitian n-by-n matrices over the real numbers, the usual symmetric Jordan product, @@ -1892,21 +2167,36 @@ class ComplexHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA): EXAMPLES: - In theory, our "field" can be any subfield of the reals:: + In theory, our "field" can be any subfield of the reals, but we + can't use inexact real fields at the moment because SageMath + doesn't know how to convert their elements into complex numbers, + or even into algebraic reals:: - sage: ComplexHermitianEJA(2, field=RDF, check_axioms=True) - Euclidean Jordan algebra of dimension 4 over Real Double Field - sage: ComplexHermitianEJA(2, field=RR, check_axioms=True) - Euclidean Jordan algebra of dimension 4 over Real Field with - 53 bits of precision + sage: QQbar(RDF(1)) + Traceback (most recent call last): + ... + TypeError: Illegal initializer for algebraic number + sage: AA(RR(1)) + Traceback (most recent call last): + ... + TypeError: Illegal initializer for algebraic number + + This causes the following error when we try to scale a matrix of + complex numbers by an inexact real number:: + + sage: ComplexHermitianEJA(2,field=RR) + Traceback (most recent call last): + ... + TypeError: Unable to coerce entries (=(1.00000000000000, + -0.000000000000000)) to coefficients in Algebraic Real Field TESTS: The dimension of this algebra is `n^2`:: sage: set_random_seed() - sage: n_max = ComplexHermitianEJA._max_random_instance_size() - sage: n = ZZ.random_element(1, n_max) + sage: d = ComplexHermitianEJA._max_random_instance_dimension() + sage: n = ComplexHermitianEJA._max_random_instance_size(d) sage: J = ComplexHermitianEJA(n) sage: J.dimension() == n^2 True @@ -1936,108 +2226,40 @@ class ComplexHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA): Euclidean Jordan algebra of dimension 0 over Algebraic Real Field """ - - @classmethod - def _denormalized_basis(cls, n, field): - """ - Returns a basis for the space of complex Hermitian n-by-n matrices. - - Why do we embed these? Basically, because all of numerical linear - algebra assumes that you're working with vectors consisting of `n` - entries from a field and scalars from the same field. There's no way - to tell SageMath that (for example) the vectors contain complex - numbers, while the scalar field is real. - - SETUP:: - - sage: from mjo.eja.eja_algebra import ComplexHermitianEJA - - TESTS:: - - sage: set_random_seed() - sage: n = ZZ.random_element(1,5) - sage: B = ComplexHermitianEJA._denormalized_basis(n,QQ) - sage: all( M.is_hermitian() for M in B) - True - - """ - from mjo.hurwitz import ComplexMatrixAlgebra - A = ComplexMatrixAlgebra(n, scalars=field) - es = A.entry_algebra_gens() - - basis = [] - for i in range(n): - for j in range(i+1): - if i == j: - E_ii = A.monomial( (i,j,es[0]) ) - basis.append(E_ii) - else: - for e in es: - E_ij = A.monomial( (i,j,e) ) - ec = e.conjugate() - # If the conjugate has a negative sign in front - # of it, (j,i,ec) won't be a monomial! - if (j,i,ec) in A.indices(): - E_ij += A.monomial( (j,i,ec) ) - else: - E_ij -= A.monomial( (j,i,-ec) ) - basis.append(E_ij) - - return tuple( basis ) - - @staticmethod - def trace_inner_product(X,Y): - r""" - SETUP:: - - sage: from mjo.eja.eja_algebra import ComplexHermitianEJA - - TESTS:: - - sage: J = ComplexHermitianEJA(2,field=QQ,orthonormalize=False) - sage: I = J.one().to_matrix() - sage: J.trace_inner_product(I, -I) - -2 - - """ - return (X*Y).trace().real() - def __init__(self, n, field=AA, **kwargs): # We know this is a valid EJA, but will double-check # if the user passes check_axioms=True. if "check_axioms" not in kwargs: kwargs["check_axioms"] = False - associative = False - if n <= 1: - associative = True + from mjo.hurwitz import ComplexMatrixAlgebra + A = ComplexMatrixAlgebra(n, scalars=field) + super().__init__(A, **kwargs) - super().__init__(self._denormalized_basis(n,field), - self.jordan_product, - self.trace_inner_product, - field=field, - associative=associative, - **kwargs) - # TODO: this could be factored out somehow, but is left here - # because the MatrixEJA is not presently a subclass of the - # FDEJA class that defines rank() and one(). - self.rank.set_cache(n) - idV = self.matrix_space().one() - self.one.set_cache(self(idV)) + from mjo.eja.eja_cache import complex_hermitian_eja_coeffs + a = complex_hermitian_eja_coeffs(self) + if a is not None: + self.rational_algebra()._charpoly_coefficients.set_cache(a) @staticmethod - def _max_random_instance_size(): - return 3 # Dimension 9 + def _max_random_instance_size(max_dimension): + # Obtained by solving d = n^2. + # The ZZ-int-ZZ thing is just "floor." + return ZZ(int(ZZ(max_dimension).sqrt())) @classmethod - def random_instance(cls, **kwargs): + def random_instance(cls, max_dimension=None, *args, **kwargs): """ Return a random instance of this type of algebra. """ - n = ZZ.random_element(cls._max_random_instance_size() + 1) + class_max_d = cls._max_random_instance_dimension() + if (max_dimension is None or max_dimension > class_max_d): + max_dimension = class_max_d + max_size = cls._max_random_instance_size(max_dimension) + n = ZZ.random_element(max_size + 1) return cls(n, **kwargs) -class QuaternionHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA): +class QuaternionHermitianEJA(MatrixEJA, RationalBasisEJA, ConcreteEJA): r""" The rank-n simple EJA consisting of self-adjoint n-by-n quaternion matrices, the usual symmetric Jordan product, and the @@ -2063,8 +2285,8 @@ class QuaternionHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA): The dimension of this algebra is `2*n^2 - n`:: sage: set_random_seed() - sage: n_max = QuaternionHermitianEJA._max_random_instance_size() - sage: n = ZZ.random_element(1, n_max) + sage: d = QuaternionHermitianEJA._max_random_instance_dimension() + sage: n = QuaternionHermitianEJA._max_random_instance_size(d) sage: J = QuaternionHermitianEJA(n) sage: J.dimension() == 2*(n^2) - n True @@ -2094,120 +2316,50 @@ class QuaternionHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA): Euclidean Jordan algebra of dimension 0 over Algebraic Real Field """ - @classmethod - def _denormalized_basis(cls, n, field): - """ - Returns a basis for the space of quaternion Hermitian n-by-n matrices. - - Why do we embed these? Basically, because all of numerical - linear algebra assumes that you're working with vectors consisting - of `n` entries from a field and scalars from the same field. There's - no way to tell SageMath that (for example) the vectors contain - complex numbers, while the scalar field is real. - - SETUP:: - - sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA - - TESTS:: - - sage: set_random_seed() - sage: n = ZZ.random_element(1,5) - sage: B = QuaternionHermitianEJA._denormalized_basis(n,QQ) - sage: all( M.is_hermitian() for M in B ) - True - - """ - from mjo.hurwitz import QuaternionMatrixAlgebra - A = QuaternionMatrixAlgebra(n, scalars=field) - es = A.entry_algebra_gens() - - basis = [] - for i in range(n): - for j in range(i+1): - if i == j: - E_ii = A.monomial( (i,j,es[0]) ) - basis.append(E_ii) - else: - for e in es: - E_ij = A.monomial( (i,j,e) ) - ec = e.conjugate() - # If the conjugate has a negative sign in front - # of it, (j,i,ec) won't be a monomial! - if (j,i,ec) in A.indices(): - E_ij += A.monomial( (j,i,ec) ) - else: - E_ij -= A.monomial( (j,i,-ec) ) - basis.append(E_ij) - - return tuple( basis ) - - - @staticmethod - def trace_inner_product(X,Y): - r""" - Overload the superclass method because the quaternions are weird - and we need to use ``coefficient_tuple()`` to get the realpart. - - SETUP:: - - sage: from mjo.eja.eja_algebra import QuaternionHermitianEJA - - TESTS:: - - sage: J = QuaternionHermitianEJA(2,field=QQ,orthonormalize=False) - sage: I = J.one().to_matrix() - sage: J.trace_inner_product(I, -I) - -2 - - """ - return (X*Y).trace().coefficient_tuple()[0] - def __init__(self, n, field=AA, **kwargs): # We know this is a valid EJA, but will double-check # if the user passes check_axioms=True. if "check_axioms" not in kwargs: kwargs["check_axioms"] = False - associative = False - if n <= 1: - associative = True + from mjo.hurwitz import QuaternionMatrixAlgebra + A = QuaternionMatrixAlgebra(n, scalars=field) + super().__init__(A, **kwargs) - super().__init__(self._denormalized_basis(n,field), - self.jordan_product, - self.trace_inner_product, - field=field, - associative=associative, - **kwargs) + from mjo.eja.eja_cache import quaternion_hermitian_eja_coeffs + a = quaternion_hermitian_eja_coeffs(self) + if a is not None: + self.rational_algebra()._charpoly_coefficients.set_cache(a) - # TODO: this could be factored out somehow, but is left here - # because the MatrixEJA is not presently a subclass of the - # FDEJA class that defines rank() and one(). - self.rank.set_cache(n) - idV = self.matrix_space().one() - self.one.set_cache(self(idV)) @staticmethod - def _max_random_instance_size(): + def _max_random_instance_size(max_dimension): r""" The maximum rank of a random QuaternionHermitianEJA. """ - return 2 # Dimension 6 + # Obtained by solving d = 2n^2 - n. + # The ZZ-int-ZZ thing is just "floor." + return ZZ(int(ZZ(8*max_dimension + 1).sqrt()/4 + 1/4)) @classmethod - def random_instance(cls, **kwargs): + def random_instance(cls, max_dimension=None, *args, **kwargs): """ Return a random instance of this type of algebra. """ - n = ZZ.random_element(cls._max_random_instance_size() + 1) + class_max_d = cls._max_random_instance_dimension() + if (max_dimension is None or max_dimension > class_max_d): + max_dimension = class_max_d + max_size = cls._max_random_instance_size(max_dimension) + n = ZZ.random_element(max_size + 1) return cls(n, **kwargs) -class OctonionHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA): +class OctonionHermitianEJA(MatrixEJA, RationalBasisEJA, ConcreteEJA): r""" SETUP:: sage: from mjo.eja.eja_algebra import (FiniteDimensionalEJA, ....: OctonionHermitianEJA) + sage: from mjo.hurwitz import Octonions, OctonionMatrixAlgebra EXAMPLES: @@ -2222,7 +2374,8 @@ class OctonionHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA): After a change-of-basis, the 2-by-2 algebra has the same multiplication table as the ten-dimensional Jordan spin algebra:: - sage: b = OctonionHermitianEJA._denormalized_basis(2,QQ) + sage: A = OctonionMatrixAlgebra(2,Octonions(QQ),QQ) + sage: b = OctonionHermitianEJA._denormalized_basis(A) sage: basis = (b[0] + b[9],) + b[1:9] + (b[0] - b[9],) sage: jp = OctonionHermitianEJA.jordan_product sage: ip = OctonionHermitianEJA.trace_inner_product @@ -2288,18 +2441,31 @@ class OctonionHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA): """ @staticmethod - def _max_random_instance_size(): + def _max_random_instance_size(max_dimension): r""" The maximum rank of a random QuaternionHermitianEJA. """ - return 1 # Dimension 1 + # There's certainly a formula for this, but with only four + # cases to worry about, I'm not that motivated to derive it. + if max_dimension >= 27: + return 3 + elif max_dimension >= 10: + return 2 + elif max_dimension >= 1: + return 1 + else: + return 0 @classmethod - def random_instance(cls, **kwargs): + def random_instance(cls, max_dimension=None, *args, **kwargs): """ Return a random instance of this type of algebra. """ - n = ZZ.random_element(cls._max_random_instance_size() + 1) + class_max_d = cls._max_random_instance_dimension() + if (max_dimension is None or max_dimension > class_max_d): + max_dimension = class_max_d + max_size = cls._max_random_instance_size(max_dimension) + n = ZZ.random_element(max_size + 1) return cls(n, **kwargs) def __init__(self, n, field=AA, **kwargs): @@ -2311,82 +2477,14 @@ class OctonionHermitianEJA(RationalBasisEJA, ConcreteEJA, MatrixEJA): # if the user passes check_axioms=True. if "check_axioms" not in kwargs: kwargs["check_axioms"] = False - super().__init__(self._denormalized_basis(n,field), - self.jordan_product, - self.trace_inner_product, - field=field, - **kwargs) - - # TODO: this could be factored out somehow, but is left here - # because the MatrixEJA is not presently a subclass of the - # FDEJA class that defines rank() and one(). - self.rank.set_cache(n) - idV = self.matrix_space().one() - self.one.set_cache(self(idV)) - - - @classmethod - def _denormalized_basis(cls, n, field): - """ - Returns a basis for the space of octonion Hermitian n-by-n - matrices. - - SETUP:: - - sage: from mjo.eja.eja_algebra import OctonionHermitianEJA - - EXAMPLES:: - - sage: B = OctonionHermitianEJA._denormalized_basis(3,QQ) - sage: all( M.is_hermitian() for M in B ) - True - sage: len(B) - 27 - - """ from mjo.hurwitz import OctonionMatrixAlgebra A = OctonionMatrixAlgebra(n, scalars=field) - es = A.entry_algebra_gens() + super().__init__(A, **kwargs) - basis = [] - for i in range(n): - for j in range(i+1): - if i == j: - E_ii = A.monomial( (i,j,es[0]) ) - basis.append(E_ii) - else: - for e in es: - E_ij = A.monomial( (i,j,e) ) - ec = e.conjugate() - # If the conjugate has a negative sign in front - # of it, (j,i,ec) won't be a monomial! - if (j,i,ec) in A.indices(): - E_ij += A.monomial( (j,i,ec) ) - else: - E_ij -= A.monomial( (j,i,-ec) ) - basis.append(E_ij) - - return tuple( basis ) - - @staticmethod - def trace_inner_product(X,Y): - r""" - The octonions don't know that the reals are embedded in them, - so we have to take the e0 component ourselves. - - SETUP:: - - sage: from mjo.eja.eja_algebra import OctonionHermitianEJA - - TESTS:: - - sage: J = OctonionHermitianEJA(2,field=QQ,orthonormalize=False) - sage: I = J.one().to_matrix() - sage: J.trace_inner_product(I, -I) - -2 - - """ - return (X*Y).trace().coefficient(0) + from mjo.eja.eja_cache import octonion_hermitian_eja_coeffs + a = octonion_hermitian_eja_coeffs(self) + if a is not None: + self.rational_algebra()._charpoly_coefficients.set_cache(a) class AlbertEJA(OctonionHermitianEJA): @@ -2451,13 +2549,14 @@ class HadamardEJA(RationalBasisEJA, ConcreteEJA): (r0, r1, r2) """ def __init__(self, n, field=AA, **kwargs): + MS = MatrixSpace(field, n, 1) + if n == 0: jordan_product = lambda x,y: x inner_product = lambda x,y: x else: def jordan_product(x,y): - P = x.parent() - return P( xi*yi for (xi,yi) in zip(x,y) ) + return MS( xi*yi for (xi,yi) in zip(x,y) ) def inner_product(x,y): return (x.T*y)[0,0] @@ -2471,34 +2570,43 @@ class HadamardEJA(RationalBasisEJA, ConcreteEJA): if "orthonormalize" not in kwargs: kwargs["orthonormalize"] = False if "check_axioms" not in kwargs: kwargs["check_axioms"] = False - column_basis = tuple( b.column() - for b in FreeModule(field, n).basis() ) + column_basis = tuple( MS(b) for b in FreeModule(field, n).basis() ) super().__init__(column_basis, jordan_product, inner_product, field=field, + matrix_space=MS, associative=True, **kwargs) self.rank.set_cache(n) - if n == 0: - self.one.set_cache( self.zero() ) - else: - self.one.set_cache( sum(self.gens()) ) + self.one.set_cache( self.sum(self.gens()) ) @staticmethod - def _max_random_instance_size(): + def _max_random_instance_dimension(): r""" - The maximum dimension of a random HadamardEJA. + There's no reason to go higher than five here. That's + enough to get the point across. """ return 5 + @staticmethod + def _max_random_instance_size(max_dimension): + r""" + The maximum size (=dimension) of a random HadamardEJA. + """ + return max_dimension + @classmethod - def random_instance(cls, **kwargs): + def random_instance(cls, max_dimension=None, *args, **kwargs): """ Return a random instance of this type of algebra. """ - n = ZZ.random_element(cls._max_random_instance_size() + 1) + class_max_d = cls._max_random_instance_dimension() + if (max_dimension is None or max_dimension > class_max_d): + max_dimension = class_max_d + max_size = cls._max_random_instance_size(max_dimension) + n = ZZ.random_element(max_size + 1) return cls(n, **kwargs) @@ -2598,22 +2706,22 @@ class BilinearFormEJA(RationalBasisEJA, ConcreteEJA): # verify things, we'll skip the rest of the checks. if "check_axioms" not in kwargs: kwargs["check_axioms"] = False + n = B.nrows() + MS = MatrixSpace(field, n, 1) + def inner_product(x,y): return (y.T*B*x)[0,0] def jordan_product(x,y): - P = x.parent() x0 = x[0,0] xbar = x[1:,0] y0 = y[0,0] ybar = y[1:,0] z0 = inner_product(y,x) zbar = y0*xbar + x0*ybar - return P([z0] + zbar.list()) + return MS([z0] + zbar.list()) - n = B.nrows() - column_basis = tuple( b.column() - for b in FreeModule(field, n).basis() ) + column_basis = tuple( MS(b) for b in FreeModule(field, n).basis() ) # TODO: I haven't actually checked this, but it seems legit. associative = False @@ -2624,6 +2732,7 @@ class BilinearFormEJA(RationalBasisEJA, ConcreteEJA): jordan_product, inner_product, field=field, + matrix_space=MS, associative=associative, **kwargs) @@ -2631,25 +2740,37 @@ class BilinearFormEJA(RationalBasisEJA, ConcreteEJA): # one-dimensional ambient space (because the rank is bounded # by the ambient dimension). self.rank.set_cache(min(n,2)) - if n == 0: self.one.set_cache( self.zero() ) else: self.one.set_cache( self.monomial(0) ) @staticmethod - def _max_random_instance_size(): + def _max_random_instance_dimension(): r""" - The maximum dimension of a random BilinearFormEJA. + There's no reason to go higher than five here. That's + enough to get the point across. """ return 5 + @staticmethod + def _max_random_instance_size(max_dimension): + r""" + The maximum size (=dimension) of a random BilinearFormEJA. + """ + return max_dimension + @classmethod - def random_instance(cls, **kwargs): + def random_instance(cls, max_dimension=None, *args, **kwargs): """ Return a random instance of this algebra. """ - n = ZZ.random_element(cls._max_random_instance_size() + 1) + class_max_d = cls._max_random_instance_dimension() + if (max_dimension is None or max_dimension > class_max_d): + max_dimension = class_max_d + max_size = cls._max_random_instance_size(max_dimension) + n = ZZ.random_element(max_size + 1) + if n.is_zero(): B = matrix.identity(ZZ, n) return cls(B, **kwargs) @@ -2660,6 +2781,7 @@ class BilinearFormEJA(RationalBasisEJA, ConcreteEJA): alpha = ZZ.zero() while alpha.is_zero(): alpha = ZZ.random_element().abs() + B22 = M.transpose()*M + alpha*I from sage.matrix.special import block_matrix @@ -2732,21 +2854,18 @@ class JordanSpinEJA(BilinearFormEJA): # can pass in a field! super().__init__(B, *args, **kwargs) - @staticmethod - def _max_random_instance_size(): - r""" - The maximum dimension of a random JordanSpinEJA. - """ - return 5 - @classmethod - def random_instance(cls, **kwargs): + def random_instance(cls, max_dimension=None, *args, **kwargs): """ Return a random instance of this type of algebra. Needed here to override the implementation for ``BilinearFormEJA``. """ - n = ZZ.random_element(cls._max_random_instance_size() + 1) + class_max_d = cls._max_random_instance_dimension() + if (max_dimension is None or max_dimension > class_max_d): + max_dimension = class_max_d + max_size = cls._max_random_instance_size(max_dimension) + n = ZZ.random_element(max_size + 1) return cls(n, **kwargs) @@ -2779,10 +2898,11 @@ class TrivialEJA(RationalBasisEJA, ConcreteEJA): 0 """ - def __init__(self, **kwargs): + def __init__(self, field=AA, **kwargs): jordan_product = lambda x,y: x - inner_product = lambda x,y: 0 + inner_product = lambda x,y: field.zero() basis = () + MS = MatrixSpace(field,0) # New defaults for keyword arguments if "orthonormalize" not in kwargs: kwargs["orthonormalize"] = False @@ -2792,6 +2912,8 @@ class TrivialEJA(RationalBasisEJA, ConcreteEJA): jordan_product, inner_product, associative=True, + field=field, + matrix_space=MS, **kwargs) # The rank is zero using my definition, namely the dimension of the @@ -2800,9 +2922,12 @@ class TrivialEJA(RationalBasisEJA, ConcreteEJA): self.one.set_cache( self.zero() ) @classmethod - def random_instance(cls, **kwargs): + def random_instance(cls, max_dimension=None, *args, **kwargs): # We don't take a "size" argument so the superclass method is - # inappropriate for us. + # inappropriate for us. The ``max_dimension`` argument is + # included so that if this method is called generically with a + # ``max_dimension=`` argument, we don't try to pass + # it on to the algebra constructor. return cls(**kwargs) @@ -2818,6 +2943,7 @@ class CartesianProductEJA(FiniteDimensionalEJA): sage: from mjo.eja.eja_algebra import (random_eja, ....: CartesianProductEJA, + ....: ComplexHermitianEJA, ....: HadamardEJA, ....: JordanSpinEJA, ....: RealSymmetricEJA) @@ -2929,6 +3055,28 @@ class CartesianProductEJA(FiniteDimensionalEJA): | b2 || 0 | 0 | b2 | +----++----+----+----+ + The "matrix space" of a Cartesian product always consists of + ordered pairs (or triples, or...) whose components are the + matrix spaces of its factors:: + + sage: J1 = HadamardEJA(2) + sage: J2 = ComplexHermitianEJA(2) + sage: J = cartesian_product([J1,J2]) + sage: J.matrix_space() + The Cartesian product of (Full MatrixSpace of 2 by 1 dense + matrices over Algebraic Real Field, Module of 2 by 2 matrices + with entries in Algebraic Field over the scalar ring Algebraic + Real Field) + sage: J.one().to_matrix()[0] + [1] + [1] + sage: J.one().to_matrix()[1] + +---+---+ + | 1 | 0 | + +---+---+ + | 0 | 1 | + +---+---+ + TESTS: All factors must share the same base field:: @@ -2951,11 +3099,7 @@ class CartesianProductEJA(FiniteDimensionalEJA): sage: expected = J.one() # long time sage: actual == expected # long time True - """ - Element = FiniteDimensionalEJAElement - - def __init__(self, factors, **kwargs): m = len(factors) if m == 0: @@ -2967,52 +3111,97 @@ class CartesianProductEJA(FiniteDimensionalEJA): if not all( J.base_ring() == field for J in factors ): raise ValueError("all factors must share the same base field") + # Figure out the category to use. associative = all( f.is_associative() for f in factors ) + category = EuclideanJordanAlgebras(field) + if associative: category = category.Associative() + category = category.join([category, category.CartesianProducts()]) + + # Compute my matrix space. We don't simply use the + # ``cartesian_product()`` functor here because it acts + # differently on SageMath MatrixSpaces and our custom + # MatrixAlgebras, which are CombinatorialFreeModules. We + # always want the result to be represented (and indexed) as an + # ordered tuple. This category isn't perfect, but is good + # enough for what we need to do. + MS_cat = MagmaticAlgebras(field).FiniteDimensional().WithBasis() + MS_cat = MS_cat.Unital().CartesianProducts() + MS_factors = tuple( J.matrix_space() for J in factors ) + from sage.sets.cartesian_product import CartesianProduct + self._matrix_space = CartesianProduct(MS_factors, MS_cat) - MS = self.matrix_space() - basis = [] - zero = MS.zero() + self._matrix_basis = [] + zero = self._matrix_space.zero() for i in range(m): for b in factors[i].matrix_basis(): z = list(zero) z[i] = b - basis.append(z) + self._matrix_basis.append(z) - basis = tuple( MS(b) for b in basis ) + self._matrix_basis = tuple( self._matrix_space(b) + for b in self._matrix_basis ) + n = len(self._matrix_basis) - # Define jordan/inner products that operate on that matrix_basis. - def jordan_product(x,y): - return MS(tuple( - (factors[i](x[i])*factors[i](y[i])).to_matrix() - for i in range(m) - )) - - def inner_product(x, y): - return sum( - factors[i](x[i]).inner_product(factors[i](y[i])) - for i in range(m) - ) + # We already have what we need for the super-superclass constructor. + CombinatorialFreeModule.__init__(self, + field, + range(n), + prefix="b", + category=category, + bracket=False) - # There's no need to check the field since it already came - # from an EJA. Likewise the axioms are guaranteed to be - # satisfied, unless the guy writing this class sucks. - # - # If you want the basis to be orthonormalized, orthonormalize - # the factors. - FiniteDimensionalEJA.__init__(self, - basis, - jordan_product, - inner_product, - field=field, - orthonormalize=False, - associative=associative, - cartesian_product=True, - check_field=False, - check_axioms=False) + # Now create the vector space for the algebra, which will have + # its own set of non-ambient coordinates (in terms of the + # supplied basis). + degree = sum( f._matrix_span.ambient_vector_space().degree() + for f in factors ) + V = VectorSpace(field, degree) + vector_basis = tuple( V(_all2list(b)) for b in self._matrix_basis ) + + # Save the span of our matrix basis (when written out as long + # vectors) because otherwise we'll have to reconstruct it + # every time we want to coerce a matrix into the algebra. + self._matrix_span = V.span_of_basis( vector_basis, check=False) + + # Since we don't (re)orthonormalize the basis, the FDEJA + # constructor is going to set self._deortho_matrix to the + # identity matrix. Here we set it to the correct value using + # the deortho matrices from our factors. + self._deortho_matrix = matrix.block_diagonal( + [J._deortho_matrix for J in factors] + ) + + self._inner_product_matrix = matrix.block_diagonal( + [J._inner_product_matrix for J in factors] + ) + self._inner_product_matrix._cache = {'hermitian': True} + self._inner_product_matrix.set_immutable() + + # Building the multiplication table is a bit more tricky + # because we have to embed the entries of the factors' + # multiplication tables into the product EJA. + zed = self.zero() + self._multiplication_table = [ [zed for j in range(i+1)] + for i in range(n) ] + # Keep track of an offset that tallies the dimensions of all + # previous factors. If the second factor is dim=2 and if the + # first one is dim=3, then we want to skip the first 3x3 block + # when copying the multiplication table for the second factor. + offset = 0 + for f in range(m): + phi_f = self.cartesian_embedding(f) + factor_dim = factors[f].dimension() + for i in range(factor_dim): + for j in range(i+1): + f_ij = factors[f]._multiplication_table[i][j] + e = phi_f(f_ij) + self._multiplication_table[offset+i][offset+j] = e + offset += factor_dim + + self.rank.set_cache(sum(J.rank() for J in factors)) ones = tuple(J.one().to_matrix() for J in factors) self.one.set_cache(self(ones)) - self.rank.set_cache(sum(J.rank() for J in factors)) def cartesian_factors(self): # Copy/pasted from CombinatorialFreeModule_CartesianProduct. @@ -3030,72 +3219,6 @@ class CartesianProductEJA(FiniteDimensionalEJA): return cartesian_product.symbol.join("%s" % factor for factor in self._sets) - def matrix_space(self): - r""" - Return the space that our matrix basis lives in as a Cartesian - product. - - We don't simply use the ``cartesian_product()`` functor here - because it acts differently on SageMath MatrixSpaces and our - custom MatrixAlgebras, which are CombinatorialFreeModules. We - always want the result to be represented (and indexed) as - an ordered tuple. - - SETUP:: - - sage: from mjo.eja.eja_algebra import (ComplexHermitianEJA, - ....: HadamardEJA, - ....: OctonionHermitianEJA, - ....: RealSymmetricEJA) - - EXAMPLES:: - - sage: J1 = HadamardEJA(1) - sage: J2 = RealSymmetricEJA(2) - sage: J = cartesian_product([J1,J2]) - sage: J.matrix_space() - The Cartesian product of (Full MatrixSpace of 1 by 1 dense - matrices over Algebraic Real Field, Full MatrixSpace of 2 - by 2 dense matrices over Algebraic Real Field) - - :: - - sage: J1 = ComplexHermitianEJA(1) - sage: J2 = ComplexHermitianEJA(1) - sage: J = cartesian_product([J1,J2]) - sage: J.one().to_matrix()[0] - [1 0] - [0 1] - sage: J.one().to_matrix()[1] - [1 0] - [0 1] - - :: - - sage: J1 = OctonionHermitianEJA(1) - sage: J2 = OctonionHermitianEJA(1) - sage: J = cartesian_product([J1,J2]) - sage: J.one().to_matrix()[0] - +----+ - | e0 | - +----+ - sage: J.one().to_matrix()[1] - +----+ - | e0 | - +----+ - - """ - scalars = self.cartesian_factor(0).base_ring() - - # This category isn't perfect, but is good enough for what we - # need to do. - cat = MagmaticAlgebras(scalars).FiniteDimensional().WithBasis() - cat = cat.Unital().CartesianProducts() - factors = tuple( J.matrix_space() for J in self.cartesian_factors() ) - - from sage.sets.cartesian_product import CartesianProduct - return CartesianProduct(factors, cat) - @cached_method def cartesian_projection(self, i): @@ -3297,9 +3420,9 @@ class RationalBasisCartesianProductEJA(CartesianProductEJA, SETUP:: - sage: from mjo.eja.eja_algebra import (HadamardEJA, + sage: from mjo.eja.eja_algebra import (FiniteDimensionalEJA, + ....: HadamardEJA, ....: JordanSpinEJA, - ....: OctonionHermitianEJA, ....: RealSymmetricEJA) EXAMPLES: @@ -3320,41 +3443,74 @@ class RationalBasisCartesianProductEJA(CartesianProductEJA, The ``cartesian_product()`` function only uses the first factor to decide where the result will live; thus we have to be careful to - check that all factors do indeed have a `_rational_algebra` member - before we try to access it:: - - sage: J1 = OctonionHermitianEJA(1) # no rational basis - sage: J2 = HadamardEJA(2) - sage: cartesian_product([J1,J2]) - Euclidean Jordan algebra of dimension 1 over Algebraic Real Field - (+) Euclidean Jordan algebra of dimension 2 over Algebraic Real Field - sage: cartesian_product([J2,J1]) - Euclidean Jordan algebra of dimension 2 over Algebraic Real Field - (+) Euclidean Jordan algebra of dimension 1 over Algebraic Real Field + check that all factors do indeed have a ``rational_algebra()`` method + before we construct an algebra that claims to have a rational basis:: + + sage: J1 = HadamardEJA(2) + sage: jp = lambda X,Y: X*Y + sage: ip = lambda X,Y: X[0,0]*Y[0,0] + sage: b1 = matrix(QQ, [[1]]) + sage: J2 = FiniteDimensionalEJA((b1,), jp, ip) + sage: cartesian_product([J2,J1]) # factor one not RationalBasisEJA + Euclidean Jordan algebra of dimension 1 over Algebraic Real + Field (+) Euclidean Jordan algebra of dimension 2 over Algebraic + Real Field + sage: cartesian_product([J1,J2]) # factor one is RationalBasisEJA + Traceback (most recent call last): + ... + ValueError: factor not a RationalBasisEJA """ def __init__(self, algebras, **kwargs): + if not all( hasattr(r, "rational_algebra") for r in algebras ): + raise ValueError("factor not a RationalBasisEJA") + CartesianProductEJA.__init__(self, algebras, **kwargs) - self._rational_algebra = None - if self.vector_space().base_field() is not QQ: - if all( hasattr(r, "_rational_algebra") for r in algebras ): - self._rational_algebra = cartesian_product([ - r._rational_algebra for r in algebras - ]) + @cached_method + def rational_algebra(self): + if self.base_ring() is QQ: + return self + + return cartesian_product([ + r.rational_algebra() for r in self.cartesian_factors() + ]) RationalBasisEJA.CartesianProduct = RationalBasisCartesianProductEJA -def random_eja(*args, **kwargs): - J1 = ConcreteEJA.random_instance(*args, **kwargs) +def random_eja(max_dimension=None, *args, **kwargs): + r""" - # This might make Cartesian products appear roughly as often as - # any other ConcreteEJA. - if ZZ.random_element(len(ConcreteEJA.__subclasses__()) + 1) == 0: - # Use random_eja() again so we can get more than two factors. - J2 = random_eja(*args, **kwargs) - J = cartesian_product([J1,J2]) - return J - else: + SETUP:: + + sage: from mjo.eja.eja_algebra import random_eja + + TESTS:: + + sage: set_random_seed() + sage: n = ZZ.random_element(1,5) + sage: J = random_eja(max_dimension=n, field=QQ, orthonormalize=False) + sage: J.dimension() <= n + True + + """ + # Use the ConcreteEJA default as the total upper bound (regardless + # of any whether or not any individual factors set a lower limit). + if max_dimension is None: + max_dimension = ConcreteEJA._max_random_instance_dimension() + J1 = ConcreteEJA.random_instance(max_dimension, *args, **kwargs) + + + # Roll the dice to see if we attempt a Cartesian product. + dice_roll = ZZ.random_element(len(ConcreteEJA.__subclasses__()) + 1) + new_max_dimension = max_dimension - J1.dimension() + if new_max_dimension == 0 or dice_roll != 0: + # If it's already as big as we're willing to tolerate, just + # return it and don't worry about Cartesian products. return J1 + else: + # Use random_eja() again so we can get more than two factors + # if the sub-call also Decides on a cartesian product. + J2 = random_eja(new_max_dimension, *args, **kwargs) + return cartesian_product([J1,J2])