X-Git-Url: http://gitweb.michael.orlitzky.com/?p=sage.d.git;a=blobdiff_plain;f=mjo%2Feja%2Feja_algebra.py;h=d3eac4f6d3bfbad50f6bbc4b371aaa9d39f8859b;hp=ca3df5fea55949225a51f8255a2c13a33c37d9f2;hb=HEAD;hpb=7f55521ab4652d3ca10cd085f8e9d41bf149e8e5 diff --git a/mjo/eja/eja_algebra.py b/mjo/eja/eja_algebra.py index ca3df5f..adcc343 100644 --- a/mjo/eja/eja_algebra.py +++ b/mjo/eja/eja_algebra.py @@ -1,4 +1,4 @@ -""" +r""" Representations and constructions for Euclidean Jordan algebras. A Euclidean Jordan algebra is a Jordan algebra that has some @@ -34,12 +34,13 @@ for these simple algebras: * :class:`QuaternionHermitianEJA` * :class:`OctonionHermitianEJA` -In addition to these, we provide two other example constructions, +In addition to these, we provide a few other example constructions, * :class:`JordanSpinEJA` * :class:`HadamardEJA` * :class:`AlbertEJA` * :class:`TrivialEJA` + * :class:`ComplexSkewSymmetricEJA` The Jordan spin algebra is a bilinear form algebra where the bilinear form is the identity. The Hadamard EJA is simply a Cartesian product @@ -52,6 +53,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 EJA + 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 = EJA((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 = EJA((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 = EJA((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 = EJA(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 @@ -74,11 +167,23 @@ from sage.modules.free_module import FreeModule, VectorSpace from sage.rings.all import (ZZ, QQ, AA, QQbar, RR, RLF, CLF, PolynomialRing, QuadraticField) -from mjo.eja.eja_element import FiniteDimensionalEJAElement -from mjo.eja.eja_operator import FiniteDimensionalEJAOperator -from mjo.eja.eja_utils import _all2list, _mat2vec +from mjo.eja.eja_element import (CartesianProductEJAElement, + EJAElement) +from mjo.eja.eja_operator import EJAOperator +from mjo.eja.eja_utils import _all2list + +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): +class EJA(CombinatorialFreeModule): r""" A finite-dimensional Euclidean Jordan algebra. @@ -104,6 +209,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. @@ -121,25 +231,43 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): We should compute that an element subalgebra is associative even if we circumvent the element method:: - sage: set_random_seed() sage: J = random_eja(field=QQ,orthonormalize=False) sage: x = J.random_element() sage: A = x.subalgebra_generated_by(orthonormalize=False) sage: basis = tuple(b.superalgebra_element() for b in A.basis()) sage: J.subalgebra(basis, orthonormalize=False).is_associative() True - """ - Element = FiniteDimensionalEJAElement + Element = EJAElement + + @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 +275,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 +301,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 +321,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 +330,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 +346,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 "X0", "X1",... 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 +396,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: @@ -305,7 +432,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): TESTS:: - sage: set_random_seed() sage: J = random_eja() sage: J(1) Traceback (most recent call last): @@ -330,7 +456,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): TESTS:: - sage: set_random_seed() sage: J = random_eja() sage: n = J.dimension() sage: bi = J.zero() @@ -372,7 +497,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): Our inner product is "associative," which means the following for a symmetric bilinear form:: - sage: set_random_seed() sage: J = random_eja() sage: x,y,z = J.random_elements(3) sage: (x*y).inner_product(z) == y.inner_product(x*z) @@ -383,7 +507,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): Ensure that this is the usual inner product for the algebras over `R^n`:: - sage: set_random_seed() sage: J = HadamardEJA.random_instance() sage: x,y = J.random_elements(2) sage: actual = x.inner_product(y) @@ -396,7 +519,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): one). This is in Faraut and Koranyi, and also my "On the symmetry..." paper:: - sage: set_random_seed() sage: J = BilinearFormEJA.random_instance() sage: n = J.dimension() sage: x = J.random_element() @@ -509,7 +631,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): The values we've presupplied to the constructors agree with the computation:: - sage: set_random_seed() sage: J = random_eja() sage: J.is_associative() == J._jordan_product_is_associative() True @@ -576,8 +697,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,12 +737,21 @@ 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 faithfully between its matrix and algebra representations:: - sage: set_random_seed() sage: J = random_eja() sage: x = J.random_element() sage: J(x.to_matrix()) == x @@ -640,6 +770,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 +780,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 +806,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) @@ -735,7 +864,7 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): sage: J = JordanSpinEJA(3) sage: p = J.characteristic_polynomial_of(); p - X1^2 - X2^2 - X3^2 + (-2*t)*X1 + t^2 + X0^2 - X1^2 - X2^2 + (-2*t)*X0 + t^2 sage: xvec = J.one().to_vector() sage: p(*xvec) t^2 - 2*t + 1 @@ -784,13 +913,13 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): sage: J = HadamardEJA(2) sage: J.coordinate_polynomial_ring() - Multivariate Polynomial Ring in X1, X2... + Multivariate Polynomial Ring in X0, X1... sage: J = RealSymmetricEJA(3,field=QQ,orthonormalize=False) sage: J.coordinate_polynomial_ring() - Multivariate Polynomial Ring in X1, X2, X3, X4, X5, X6... + Multivariate Polynomial Ring in X0, X1, X2, X3, X4, X5... """ - var_names = tuple( "X%d" % z for z in range(1, self.dimension()+1) ) + var_names = tuple( "X%d" % z for z in range(self.dimension()) ) return PolynomialRing(self.base_ring(), var_names) def inner_product(self, x, y): @@ -812,7 +941,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): Our inner product is "associative," which means the following for a symmetric bilinear form:: - sage: set_random_seed() sage: J = random_eja() sage: x,y,z = J.random_elements(3) sage: (x*y).inner_product(z) == y.inner_product(x*z) @@ -823,7 +951,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): Ensure that this is the usual inner product for the algebras over `R^n`:: - sage: set_random_seed() sage: J = HadamardEJA.random_instance() sage: x,y = J.random_elements(2) sage: actual = x.inner_product(y) @@ -836,7 +963,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): one). This is in Faraut and Koranyi, and also my "On the symmetry..." paper:: - sage: set_random_seed() sage: J = BilinearFormEJA.random_instance() sage: n = J.dimension() sage: x = J.random_element() @@ -1016,16 +1142,16 @@ 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() - Full MatrixSpace of 4 by 4 dense matrices over Rational Field + Module of 1 by 1 matrices with entries in Quaternion + Algebra (-1, -1) with base ring Rational Field over + 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 @@ -1064,19 +1190,17 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): The identity element acts like the identity, regardless of whether or not we orthonormalize:: - sage: set_random_seed() sage: J = random_eja() sage: x = J.random_element() sage: J.one()*x == x and x*J.one() == x True - sage: A = x.subalgebra_generated_by() + sage: A = x.subalgebra_generated_by(orthonormalize=False) sage: y = A.random_element() sage: A.one()*y == y and y*A.one() == y True :: - sage: set_random_seed() sage: J = random_eja(field=QQ, orthonormalize=False) sage: x = J.random_element() sage: J.one()*x == x and x*J.one() == x @@ -1090,14 +1214,13 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): regardless of the base field and whether or not we orthonormalize:: - sage: set_random_seed() sage: J = random_eja() sage: actual = J.one().operator().matrix() sage: expected = matrix.identity(J.base_ring(), J.dimension()) sage: actual == expected True sage: x = J.random_element() - sage: A = x.subalgebra_generated_by() + sage: A = x.subalgebra_generated_by(orthonormalize=False) sage: actual = A.one().operator().matrix() sage: expected = matrix.identity(A.base_ring(), A.dimension()) sage: actual == expected @@ -1105,7 +1228,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): :: - sage: set_random_seed() sage: J = random_eja(field=QQ, orthonormalize=False) sage: actual = J.one().operator().matrix() sage: expected = matrix.identity(J.base_ring(), J.dimension()) @@ -1121,7 +1243,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): Ensure that the cached unit element (often precomputed by hand) agrees with the computed one:: - sage: set_random_seed() sage: J = random_eja() sage: cached = J.one() sage: J.one.clear_cache() @@ -1130,7 +1251,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): :: - sage: set_random_seed() sage: J = random_eja(field=QQ, orthonormalize=False) sage: cached = J.one() sage: J.one.clear_cache() @@ -1146,7 +1266,9 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): # # Of course, matrices aren't vectors in sage, so we have to # appeal to the "long vectors" isometry. - oper_vecs = [ _mat2vec(g.operator().matrix()) for g in self.gens() ] + + V = VectorSpace(self.base_ring(), self.dimension()**2) + oper_vecs = [ V(g.operator().matrix().list()) for g in self.gens() ] # Now we use basic linear algebra to find the coefficients, # of the matrices-as-vectors-linear-combination, which should @@ -1156,7 +1278,7 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): # We used the isometry on the left-hand side already, but we # still need to do it for the right-hand side. Recall that we # wanted something that summed to the identity matrix. - b = _mat2vec( matrix.identity(self.base_ring(), self.dimension()) ) + b = V( matrix.identity(self.base_ring(), self.dimension()).list() ) # Now if there's an identity element in the algebra, this # should work. We solve on the left to avoid having to @@ -1241,7 +1363,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): Every algebra decomposes trivially with respect to its identity element:: - sage: set_random_seed() sage: J = random_eja() sage: J0,J5,J1 = J.peirce_decomposition(J.one()) sage: J0.dimension() == 0 and J5.dimension() == 0 @@ -1254,7 +1375,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): elements in the two subalgebras are the projections onto their respective subspaces of the superalgebra's identity element:: - sage: set_random_seed() sage: J = random_eja() sage: x = J.random_element() sage: if not J.is_trivial(): @@ -1286,7 +1406,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 @@ -1328,26 +1448,13 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): # For a general base ring... maybe we can trust this to do the # right thing? Unlikely, but. V = self.vector_space() - v = V.random_element() - - if self.base_ring() is AA: - # The "random element" method of the algebraic reals is - # stupid at the moment, and only returns integers between - # -2 and 2, inclusive: - # - # https://trac.sagemath.org/ticket/30875 - # - # Instead, we implement our own "random vector" method, - # and then coerce that into the algebra. We use the vector - # space degree here instead of the dimension because a - # subalgebra could (for example) be spanned by only two - # vectors, each with five coordinates. We need to - # generate all five coordinates. - if thorough: - v *= QQbar.random_element().real() - else: - v *= QQ.random_element() + if self.base_ring() is AA and not thorough: + # Now that AA generates actually random random elements + # (post Trac 30875), we only need to de-thorough the + # randomness when asked to. + V = V.change_ring(QQ) + v = V.random_element() return self.from_vector(V.coordinate_vector(v)) def random_elements(self, count, thorough=False): @@ -1380,6 +1487,64 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): for idx in range(count) ) + def operator_polynomial_matrix(self): + r""" + Return the matrix of polynomials (over this algebra's + :meth:`coordinate_polynomial_ring`) that, when evaluated at + the basis coordinates of an element `x`, produces the basis + representation of `L_{x}`. + + SETUP:: + + sage: from mjo.eja.eja_algebra import (HadamardEJA, + ....: JordanSpinEJA) + + EXAMPLES:: + + sage: J = HadamardEJA(4) + sage: L_x = J.operator_polynomial_matrix() + sage: L_x + [X0 0 0 0] + [ 0 X1 0 0] + [ 0 0 X2 0] + [ 0 0 0 X3] + sage: x = J.one() + sage: d = zip(J.coordinate_polynomial_ring().gens(), x.to_vector()) + sage: L_x.subs(dict(d)) + [1 0 0 0] + [0 1 0 0] + [0 0 1 0] + [0 0 0 1] + + :: + + sage: J = JordanSpinEJA(4) + sage: L_x = J.operator_polynomial_matrix() + sage: L_x + [X0 X1 X2 X3] + [X1 X0 0 0] + [X2 0 X0 0] + [X3 0 0 X0] + sage: x = J.one() + sage: d = zip(J.coordinate_polynomial_ring().gens(), x.to_vector()) + sage: L_x.subs(dict(d)) + [1 0 0 0] + [0 1 0 0] + [0 0 1 0] + [0 0 0 1] + + """ + R = self.coordinate_polynomial_ring() + + def L_x_i_j(i,j): + # From a result in my book, these are the entries of the + # basis representation of L_x. + return sum( v*self.monomial(k).operator().matrix()[i,j] + for (k,v) in enumerate(R.gens()) ) + + n = self.dimension() + return matrix(R, n, n, L_x_i_j) + @cached_method def _charpoly_coefficients(self): r""" @@ -1395,7 +1560,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): The theory shows that these are all homogeneous polynomials of a known degree:: - sage: set_random_seed() sage: J = random_eja() sage: all(p.is_homogeneous() for p in J._charpoly_coefficients()) True @@ -1403,16 +1567,9 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): """ n = self.dimension() R = self.coordinate_polynomial_ring() - vars = R.gens() F = R.fraction_field() - def L_x_i_j(i,j): - # From a result in my book, these are the entries of the - # basis representation of L_x. - return sum( vars[k]*self.monomial(k).operator().matrix()[i,j] - for k in range(n) ) - - L_x = matrix(F, n, n, L_x_i_j) + L_x = self.operator_polynomial_matrix() r = None if self.rank.is_in_cache(): @@ -1493,7 +1650,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): positive integer rank, unless the algebra is trivial in which case its rank will be zero:: - sage: set_random_seed() sage: J = random_eja() sage: r = J.rank() sage: r in ZZ @@ -1504,7 +1660,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): Ensure that computing the rank actually works, since the ranks of all simple algebras are known and will be cached by default:: - sage: set_random_seed() # long time sage: J = random_eja() # long time sage: cached = J.rank() # long time sage: J.rank.clear_cache() # long time @@ -1519,8 +1674,8 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): r""" Create a subalgebra of this algebra from the given basis. """ - from mjo.eja.eja_subalgebra import FiniteDimensionalEJASubalgebra - return FiniteDimensionalEJASubalgebra(self, basis, **kwargs) + from mjo.eja.eja_subalgebra import EJASubalgebra + return EJASubalgebra(self, basis, **kwargs) def vector_space(self): @@ -1542,7 +1697,7 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): -class RationalBasisEJA(FiniteDimensionalEJA): +class RationalBasisEJA(EJA): r""" Algebras whose supplied basis elements have all rational entries. @@ -1575,7 +1730,11 @@ class RationalBasisEJA(FiniteDimensionalEJA): if check_field: # Abuse the check_field parameter to check that the entries of # out basis (in ambient coordinates) are in the field QQ. - if not all( all(b_i in QQ for b_i in b.list()) for b in basis ): + # Use _all2list to get the vector coordinates of octonion + # entries and not the octonions themselves (which are not + # rational). + if not all( all(b_i in QQ for b_i in _all2list(b)) + for b in basis ): raise TypeError("basis not rational") super().__init__(basis, @@ -1593,16 +1752,26 @@ class RationalBasisEJA(FiniteDimensionalEJA): # Note: the same Jordan and inner-products work here, # because they are necessarily defined with respect to # ambient coordinates and not any particular basis. - self._rational_algebra = FiniteDimensionalEJA( + self._rational_algebra = EJA( basis, 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""" @@ -1619,7 +1788,7 @@ class RationalBasisEJA(FiniteDimensionalEJA): sage: J = JordanSpinEJA(3) sage: J._charpoly_coefficients() - (X1^2 - X2^2 - X3^2, -2*X1) + (X0^2 - X1^2 - X2^2, -2*X0) sage: a0 = J._charpoly_coefficients()[0] sage: J.base_ring() Algebraic Real Field @@ -1627,28 +1796,17 @@ 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 + # Do the computation over the rationals. a = ( a_i.change_ring(self.base_ring()) - for a_i in self._rational_algebra._charpoly_coefficients() ) + 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) - - # Otherwise, convert the coordinate variables back to the - # deorthonormalized ones. + # Convert our coordinate variables into deorthonormalized ones + # and substitute them into the deorthonormalized charpoly + # coefficients. R = self.coordinate_polynomial_ring() from sage.modules.free_module_element import vector X = vector(R, R.gens()) @@ -1657,7 +1815,7 @@ class RationalBasisEJA(FiniteDimensionalEJA): subs_dict = { X[i]: BX[i] for i in range(len(X)) } return tuple( a_i.subs(subs_dict) for a_i in a ) -class ConcreteEJA(FiniteDimensionalEJA): +class ConcreteEJA(EJA): r""" A class for the Euclidean Jordan algebras that we know by name. @@ -1675,7 +1833,6 @@ class ConcreteEJA(FiniteDimensionalEJA): Our basis is normalized with respect to the algebra's inner product, unless we specify otherwise:: - sage: set_random_seed() sage: J = ConcreteEJA.random_instance() sage: all( b.norm() == 1 for b in J.gens() ) True @@ -1686,7 +1843,6 @@ class ConcreteEJA(FiniteDimensionalEJA): natural->EJA basis representation is an isometry and within the EJA the operator is self-adjoint by the Jordan axiom:: - sage: set_random_seed() sage: J = ConcreteEJA.random_instance() sage: x = J.random_element() sage: x.operator().is_self_adjoint() @@ -1694,27 +1850,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__()) @@ -1722,117 +1913,162 @@ 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 HermitianMatrixEJA(EJA): @staticmethod - def jordan_product(X,Y): - return (X*Y + Y*X)/2 - - @staticmethod - def trace_inner_product(X,Y): - r""" - A trace inner-product for matrices that aren't embedded in the - reals. It takes MATRICES as arguments, not EJA elements. + def _denormalized_basis(A): """ - return (X*Y).trace().real() + Returns a basis for the given Hermitian matrix space. -class RealEmbeddedMatrixEJA(MatrixEJA): - @staticmethod - def dimension_over_reals(): - r""" - The dimension of this matrix's base ring over the reals. + 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:: - The reals are dimension one over themselves, obviously; that's - just `\mathbb{R}^{1}`. Likewise, the complex numbers `a + bi` - have dimension two. Finally, the quaternions have dimension - four over the reals. + sage: from mjo.hurwitz import (ComplexMatrixAlgebra, + ....: QuaternionMatrixAlgebra, + ....: OctonionMatrixAlgebra) + sage: from mjo.eja.eja_algebra import HermitianMatrixEJA - This is used to determine the size of the matrix returned from - :meth:`real_embed`, among other things. - """ - raise NotImplementedError + TESTS:: - @classmethod - def real_embed(cls,M): - """ - Embed the matrix ``M`` into a space of real matrices. + sage: n = ZZ.random_element(1,5) + sage: A = MatrixSpace(QQ, n) + sage: B = HermitianMatrixEJA._denormalized_basis(A) + sage: all( M.is_hermitian() for M in B) + True - The matrix ``M`` can have entries in any field at the moment: - the real numbers, complex numbers, or quaternions. And although - they are not a field, we can probably support octonions at some - point, too. This function returns a real matrix that "acts like" - the original with respect to matrix multiplication; i.e. + :: - real_embed(M*N) = real_embed(M)*real_embed(N) + sage: n = ZZ.random_element(1,5) + sage: A = ComplexMatrixAlgebra(n, scalars=QQ) + sage: B = HermitianMatrixEJA._denormalized_basis(A) + sage: all( M.is_hermitian() for M in B) + True - """ - if M.ncols() != M.nrows(): - raise ValueError("the matrix 'M' must be square") - return M + :: + + sage: n = ZZ.random_element(1,5) + sage: A = QuaternionMatrixAlgebra(n, scalars=QQ) + sage: B = HermitianMatrixEJA._denormalized_basis(A) + sage: all( M.is_hermitian() for M in B ) + True + :: + + sage: n = ZZ.random_element(1,5) + sage: A = OctonionMatrixAlgebra(n, scalars=QQ) + sage: B = HermitianMatrixEJA._denormalized_basis(A) + sage: all( M.is_hermitian() for M in B ) + True - @classmethod - def real_unembed(cls,M): - """ - The inverse of :meth:`real_embed`. """ - if M.ncols() != M.nrows(): - raise ValueError("the matrix 'M' must be square") - if not ZZ(M.nrows()).mod(cls.dimension_over_reals()).is_zero(): - raise ValueError("the matrix 'M' must be a real embedding") - return M + # 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) - @classmethod - def trace_inner_product(cls,X,Y): + 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 + + @staticmethod + def trace_inner_product(X,Y): r""" - Compute the trace inner-product of two real-embeddings. + 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 (ComplexHermitianEJA, - ....: QuaternionHermitianEJA) + sage: from mjo.eja.eja_algebra import (RealSymmetricEJA, + ....: ComplexHermitianEJA, + ....: QuaternionHermitianEJA, + ....: OctonionHermitianEJA) EXAMPLES:: - sage: set_random_seed() - sage: J = ComplexHermitianEJA.random_instance() - sage: x,y = J.random_elements(2) - sage: Xe = x.to_matrix() - sage: Ye = y.to_matrix() - sage: X = J.real_unembed(Xe) - sage: Y = J.real_unembed(Ye) - sage: expected = (X*Y).trace().real() - sage: actual = J.trace_inner_product(Xe,Ye) - sage: actual == expected - True + sage: J = RealSymmetricEJA(2,field=QQ,orthonormalize=False) + sage: I = J.one().to_matrix() + sage: J.trace_inner_product(I, -I) + -2 :: - sage: set_random_seed() - sage: J = QuaternionHermitianEJA.random_instance() - sage: x,y = J.random_elements(2) - sage: Xe = x.to_matrix() - sage: Ye = y.to_matrix() - sage: X = J.real_unembed(Xe) - sage: Y = J.real_unembed(Ye) - sage: expected = (X*Y).trace().coefficient_tuple()[0] - sage: actual = J.trace_inner_product(Xe,Ye) - sage: actual == expected - True + 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 """ - # This does in fact compute the real part of the trace. - # If we compute the trace of e.g. a complex matrix M, - # then we do so by adding up its diagonal entries -- - # call them z_1 through z_n. The real embedding of z_1 - # will be a 2-by-2 REAL matrix [a, b; -b, a] whose trace - # as a REAL matrix will be 2*a = 2*Re(z_1). And so forth. - return (X*Y).trace()/cls.dimension_over_reals() + 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() + + + 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(ConcreteEJA, RationalBasisEJA, MatrixEJA): +class RealSymmetricEJA(HermitianMatrixEJA, RationalBasisEJA, ConcreteEJA): """ The rank-n simple EJA consisting of real symmetric n-by-n matrices, the usual symmetric Jordan product, and the trace inner @@ -1865,16 +2101,14 @@ class RealSymmetricEJA(ConcreteEJA, RationalBasisEJA, 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 The Jordan multiplication is what we think it is:: - sage: set_random_seed() sage: J = RealSymmetricEJA.random_instance() sage: x,y = J.random_elements(2) sage: actual = (x*y).to_matrix() @@ -1897,268 +2131,84 @@ class RealSymmetricEJA(ConcreteEJA, RationalBasisEJA, 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): - # 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 + A = MatrixSpace(field, n) + super().__init__(A, **kwargs) - associative = False - if n <= 1: - associative = True + 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) - 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)) +class ComplexHermitianEJA(HermitianMatrixEJA, RationalBasisEJA, ConcreteEJA): + """ + The rank-n simple EJA consisting of complex Hermitian n-by-n + matrices over the real numbers, the usual symmetric Jordan product, + and the real-part-of-trace inner product. It has dimension `n^2` over + the reals. + SETUP:: -class ComplexMatrixEJA(RealEmbeddedMatrixEJA): - # A manual dictionary-cache for the complex_extension() method, - # since apparently @classmethods can't also be @cached_methods. - _complex_extension = {} + sage: from mjo.eja.eja_algebra import ComplexHermitianEJA - @classmethod - def complex_extension(cls,field): - r""" - The complex field that we embed/unembed, as an extension - of the given ``field``. - """ - if field in cls._complex_extension: - return cls._complex_extension[field] - - # Sage doesn't know how to adjoin the complex "i" (the root of - # x^2 + 1) to a field in a general way. Here, we just enumerate - # all of the cases that I have cared to support so far. - if field is AA: - # Sage doesn't know how to embed AA into QQbar, i.e. how - # to adjoin sqrt(-1) to AA. - F = QQbar - elif not field.is_exact(): - # RDF or RR - F = field.complex_field() - else: - # Works for QQ and... maybe some other fields. - R = PolynomialRing(field, 'z') - z = R.gen() - F = field.extension(z**2 + 1, 'I', embedding=CLF(-1).sqrt()) + EXAMPLES: - cls._complex_extension[field] = F - return F + 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:: - @staticmethod - def dimension_over_reals(): - return 2 + 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 - @classmethod - def real_embed(cls,M): - """ - Embed the n-by-n complex matrix ``M`` into the space of real - matrices of size 2n-by-2n via the map the sends each entry `z = a + - bi` to the block matrix ``[[a,b],[-b,a]]``. + TESTS: - SETUP:: + The dimension of this algebra is `n^2`:: - sage: from mjo.eja.eja_algebra import ComplexMatrixEJA + 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 - EXAMPLES:: + The Jordan multiplication is what we think it is:: - sage: F = QuadraticField(-1, 'I') - sage: x1 = F(4 - 2*i) - sage: x2 = F(1 + 2*i) - sage: x3 = F(-i) - sage: x4 = F(6) - sage: M = matrix(F,2,[[x1,x2],[x3,x4]]) - sage: ComplexMatrixEJA.real_embed(M) - [ 4 -2| 1 2] - [ 2 4|-2 1] - [-----+-----] - [ 0 -1| 6 0] - [ 1 0| 0 6] - - TESTS: - - Embedding is a homomorphism (isomorphism, in fact):: - - sage: set_random_seed() - sage: n = ZZ.random_element(3) - sage: F = QuadraticField(-1, 'I') - sage: X = random_matrix(F, n) - sage: Y = random_matrix(F, n) - sage: Xe = ComplexMatrixEJA.real_embed(X) - sage: Ye = ComplexMatrixEJA.real_embed(Y) - sage: XYe = ComplexMatrixEJA.real_embed(X*Y) - sage: Xe*Ye == XYe - True - - """ - super().real_embed(M) - n = M.nrows() - - # We don't need any adjoined elements... - field = M.base_ring().base_ring() - - blocks = [] - for z in M.list(): - a = z.real() - b = z.imag() - blocks.append(matrix(field, 2, [ [ a, b], - [-b, a] ])) - - return matrix.block(field, n, blocks) - - - @classmethod - def real_unembed(cls,M): - """ - The inverse of _embed_complex_matrix(). - - SETUP:: - - sage: from mjo.eja.eja_algebra import ComplexMatrixEJA - - EXAMPLES:: - - sage: A = matrix(QQ,[ [ 1, 2, 3, 4], - ....: [-2, 1, -4, 3], - ....: [ 9, 10, 11, 12], - ....: [-10, 9, -12, 11] ]) - sage: ComplexMatrixEJA.real_unembed(A) - [ 2*I + 1 4*I + 3] - [ 10*I + 9 12*I + 11] - - TESTS: - - Unembedding is the inverse of embedding:: - - sage: set_random_seed() - sage: F = QuadraticField(-1, 'I') - sage: M = random_matrix(F, 3) - sage: Me = ComplexMatrixEJA.real_embed(M) - sage: ComplexMatrixEJA.real_unembed(Me) == M - True - - """ - super().real_unembed(M) - n = ZZ(M.nrows()) - d = cls.dimension_over_reals() - F = cls.complex_extension(M.base_ring()) - i = F.gen() - - # Go top-left to bottom-right (reading order), converting every - # 2-by-2 block we see to a single complex element. - elements = [] - for k in range(n/d): - for j in range(n/d): - submat = M[d*k:d*k+d,d*j:d*j+d] - if submat[0,0] != submat[1,1]: - raise ValueError('bad on-diagonal submatrix') - if submat[0,1] != -submat[1,0]: - raise ValueError('bad off-diagonal submatrix') - z = submat[0,0] + submat[0,1]*i - elements.append(z) - - return matrix(F, n/d, elements) - - -class ComplexHermitianEJA(ConcreteEJA, RationalBasisEJA, ComplexMatrixEJA): - """ - The rank-n simple EJA consisting of complex Hermitian n-by-n - matrices over the real numbers, the usual symmetric Jordan product, - and the real-part-of-trace inner product. It has dimension `n^2` over - the reals. - - SETUP:: - - sage: from mjo.eja.eja_algebra import ComplexHermitianEJA - - EXAMPLES: - - In theory, our "field" can be any subfield of the 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 - - 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: J = ComplexHermitianEJA(n) - sage: J.dimension() == n^2 - True - - The Jordan multiplication is what we think it is:: - - sage: set_random_seed() - sage: J = ComplexHermitianEJA.random_instance() - sage: x,y = J.random_elements(2) - sage: actual = (x*y).to_matrix() - sage: X = x.to_matrix() - sage: Y = y.to_matrix() - sage: expected = (X*Y + Y*X)/2 - sage: actual == expected - True - sage: J(expected) == x*y - True + sage: J = ComplexHermitianEJA.random_instance() + sage: x,y = J.random_elements(2) + sage: actual = (x*y).to_matrix() + sage: X = x.to_matrix() + sage: Y = y.to_matrix() + sage: expected = (X*Y + Y*X)/2 + sage: actual == expected + True + sage: J(expected) == x*y + True We can change the generator prefix:: @@ -2171,254 +2221,36 @@ class ComplexHermitianEJA(ConcreteEJA, RationalBasisEJA, ComplexMatrixEJA): 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,ZZ) - sage: all( M.is_symmetric() for M in B) - True - - """ - R = PolynomialRing(ZZ, 'z') - z = R.gen() - F = ZZ.extension(z**2 + 1, 'I') - I = F.gen(1) - - # This is like the symmetric case, but we need to be careful: - # - # * We want conjugate-symmetry, not just symmetry. - # * The diagonal will (as a result) be real. - # - S = [] - Eij = matrix.zero(F,n) - for i in range(n): - for j in range(i+1): - # "build" E_ij - Eij[i,j] = 1 - if i == j: - Sij = cls.real_embed(Eij) - S.append(Sij) - else: - # The second one has a minus because it's conjugated. - Eij[j,i] = 1 # Eij = Eij + Eij.transpose() - Sij_real = cls.real_embed(Eij) - S.append(Sij_real) - # Eij = I*Eij - I*Eij.transpose() - Eij[i,j] = I - Eij[j,i] = -I - Sij_imag = cls.real_embed(Eij) - S.append(Sij_imag) - Eij[j,i] = 0 - # "erase" E_ij - Eij[i,j] = 0 - - # Since we embedded the entries, we can drop back to the - # desired real "field" instead of the extension "F". - return tuple( s.change_ring(field) for s in S ) - - 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 = matrix.identity(ZZ, self.dimension_over_reals()*n) - 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 QuaternionMatrixEJA(RealEmbeddedMatrixEJA): - - # A manual dictionary-cache for the quaternion_extension() method, - # since apparently @classmethods can't also be @cached_methods. - _quaternion_extension = {} - - @classmethod - def quaternion_extension(cls,field): - r""" - The quaternion field that we embed/unembed, as an extension - of the given ``field``. - """ - if field in cls._quaternion_extension: - return cls._quaternion_extension[field] - - Q = QuaternionAlgebra(field,-1,-1) - - cls._quaternion_extension[field] = Q - return Q - @staticmethod - def dimension_over_reals(): - return 4 - - @classmethod - def real_embed(cls,M): - """ - Embed the n-by-n quaternion matrix ``M`` into the space of real - matrices of size 4n-by-4n by first sending each quaternion entry `z - = a + bi + cj + dk` to the block-complex matrix ``[[a + bi, - c+di],[-c + di, a-bi]]`, and then embedding those into a real - matrix. - - SETUP:: - - sage: from mjo.eja.eja_algebra import QuaternionMatrixEJA - - EXAMPLES:: - - sage: Q = QuaternionAlgebra(QQ,-1,-1) - sage: i,j,k = Q.gens() - sage: x = 1 + 2*i + 3*j + 4*k - sage: M = matrix(Q, 1, [[x]]) - sage: QuaternionMatrixEJA.real_embed(M) - [ 1 2 3 4] - [-2 1 -4 3] - [-3 4 1 -2] - [-4 -3 2 1] - - Embedding is a homomorphism (isomorphism, in fact):: - - sage: set_random_seed() - sage: n = ZZ.random_element(2) - sage: Q = QuaternionAlgebra(QQ,-1,-1) - sage: X = random_matrix(Q, n) - sage: Y = random_matrix(Q, n) - sage: Xe = QuaternionMatrixEJA.real_embed(X) - sage: Ye = QuaternionMatrixEJA.real_embed(Y) - sage: XYe = QuaternionMatrixEJA.real_embed(X*Y) - sage: Xe*Ye == XYe - True - - """ - super().real_embed(M) - quaternions = M.base_ring() - n = M.nrows() - - F = QuadraticField(-1, 'I') - i = F.gen() - - blocks = [] - for z in M.list(): - t = z.coefficient_tuple() - a = t[0] - b = t[1] - c = t[2] - d = t[3] - cplxM = matrix(F, 2, [[ a + b*i, c + d*i], - [-c + d*i, a - b*i]]) - realM = ComplexMatrixEJA.real_embed(cplxM) - blocks.append(realM) - - # We should have real entries by now, so use the realest field - # we've got for the return value. - return matrix.block(quaternions.base_ring(), n, blocks) - - - - @classmethod - def real_unembed(cls,M): - """ - The inverse of _embed_quaternion_matrix(). - - SETUP:: - - sage: from mjo.eja.eja_algebra import QuaternionMatrixEJA - - EXAMPLES:: - - sage: M = matrix(QQ, [[ 1, 2, 3, 4], - ....: [-2, 1, -4, 3], - ....: [-3, 4, 1, -2], - ....: [-4, -3, 2, 1]]) - sage: QuaternionMatrixEJA.real_unembed(M) - [1 + 2*i + 3*j + 4*k] - - TESTS: - - Unembedding is the inverse of embedding:: - - sage: set_random_seed() - sage: Q = QuaternionAlgebra(QQ, -1, -1) - sage: M = random_matrix(Q, 3) - sage: Me = QuaternionMatrixEJA.real_embed(M) - sage: QuaternionMatrixEJA.real_unembed(Me) == M - True - - """ - super().real_unembed(M) - n = ZZ(M.nrows()) - d = cls.dimension_over_reals() - - # Use the base ring of the matrix to ensure that its entries can be - # multiplied by elements of the quaternion algebra. - Q = cls.quaternion_extension(M.base_ring()) - i,j,k = Q.gens() - - # Go top-left to bottom-right (reading order), converting every - # 4-by-4 block we see to a 2-by-2 complex block, to a 1-by-1 - # quaternion block. - elements = [] - for l in range(n/d): - for m in range(n/d): - submat = ComplexMatrixEJA.real_unembed( - M[d*l:d*l+d,d*m:d*m+d] ) - if submat[0,0] != submat[1,1].conjugate(): - raise ValueError('bad on-diagonal submatrix') - if submat[0,1] != -submat[1,0].conjugate(): - raise ValueError('bad off-diagonal submatrix') - z = submat[0,0].real() - z += submat[0,0].imag()*i - z += submat[0,1].real()*j - z += submat[0,1].imag()*k - elements.append(z) - - return matrix(Q, n/d, elements) - - -class QuaternionHermitianEJA(ConcreteEJA, - RationalBasisEJA, - QuaternionMatrixEJA): +class QuaternionHermitianEJA(HermitianMatrixEJA, RationalBasisEJA, ConcreteEJA): r""" The rank-n simple EJA consisting of self-adjoint n-by-n quaternion matrices, the usual symmetric Jordan product, and the @@ -2443,16 +2275,14 @@ class QuaternionHermitianEJA(ConcreteEJA, 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 The Jordan multiplication is what we think it is:: - sage: set_random_seed() sage: J = QuaternionHermitianEJA.random_instance() sage: x,y = J.random_elements(2) sage: actual = (x*y).to_matrix() @@ -2475,123 +2305,46 @@ class QuaternionHermitianEJA(ConcreteEJA, 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,ZZ) - sage: all( M.is_symmetric() for M in B ) - True - - """ - Q = QuaternionAlgebra(QQ,-1,-1) - I,J,K = Q.gens() - - # This is like the symmetric case, but we need to be careful: - # - # * We want conjugate-symmetry, not just symmetry. - # * The diagonal will (as a result) be real. - # - S = [] - Eij = matrix.zero(Q,n) - for i in range(n): - for j in range(i+1): - # "build" E_ij - Eij[i,j] = 1 - if i == j: - Sij = cls.real_embed(Eij) - S.append(Sij) - else: - # The second, third, and fourth ones have a minus - # because they're conjugated. - # Eij = Eij + Eij.transpose() - Eij[j,i] = 1 - Sij_real = cls.real_embed(Eij) - S.append(Sij_real) - # Eij = I*(Eij - Eij.transpose()) - Eij[i,j] = I - Eij[j,i] = -I - Sij_I = cls.real_embed(Eij) - S.append(Sij_I) - # Eij = J*(Eij - Eij.transpose()) - Eij[i,j] = J - Eij[j,i] = -J - Sij_J = cls.real_embed(Eij) - S.append(Sij_J) - # Eij = K*(Eij - Eij.transpose()) - Eij[i,j] = K - Eij[j,i] = -K - Sij_K = cls.real_embed(Eij) - S.append(Sij_K) - Eij[j,i] = 0 - # "erase" E_ij - Eij[i,j] = 0 - - # Since we embedded the entries, we can drop back to the - # desired real "field" instead of the quaternion algebra "Q". - return tuple( s.change_ring(field) for s in S ) - - 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 + from mjo.hurwitz import QuaternionMatrixAlgebra + A = QuaternionMatrixAlgebra(n, scalars=field) + super().__init__(A, **kwargs) - associative = False - if n <= 1: - associative = True - - 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 = matrix.identity(ZZ, self.dimension_over_reals()*n) - 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(ConcreteEJA, MatrixEJA): +class OctonionHermitianEJA(HermitianMatrixEJA, RationalBasisEJA, ConcreteEJA): r""" SETUP:: - sage: from mjo.eja.eja_algebra import (FiniteDimensionalEJA, + sage: from mjo.eja.eja_algebra import (EJA, ....: OctonionHermitianEJA) + sage: from mjo.hurwitz import Octonions, OctonionMatrixAlgebra EXAMPLES: @@ -2606,11 +2359,12 @@ class OctonionHermitianEJA(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 - sage: J = FiniteDimensionalEJA(basis, + sage: J = EJA(basis, ....: jp, ....: ip, ....: field=QQ, @@ -2672,18 +2426,31 @@ class OctonionHermitianEJA(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 + The maximum rank of a random OctonionHermitianEJA. + """ + # 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): @@ -2695,82 +2462,14 @@ class OctonionHermitianEJA(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 + from mjo.hurwitz import OctonionMatrixAlgebra + A = OctonionMatrixAlgebra(n, scalars=field) + super().__init__(A, **kwargs) - EXAMPLES:: - - sage: B = OctonionHermitianEJA._denormalized_basis(3,QQ) - sage: all( M.is_hermitian() for M in B ) - True - sage: len(B) - 27 - - """ - from mjo.octonions import OctonionMatrixAlgebra - MS = OctonionMatrixAlgebra(n, scalars=field) - es = MS.entry_algebra().gens() - - basis = [] - for i in range(n): - for j in range(i+1): - if i == j: - E_ii = MS.monomial( (i,j,es[0]) ) - basis.append(E_ii) - else: - for e in es: - E_ij = MS.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 MS.indices(): - E_ij += MS.monomial( (j,i,ec) ) - else: - E_ij -= MS.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().real().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): @@ -2780,7 +2479,7 @@ class AlbertEJA(OctonionHermitianEJA): SETUP:: - from mjo.eja.eja_algebra import AlbertEJA + sage: from mjo.eja.eja_algebra import AlbertEJA EXAMPLES:: @@ -2794,14 +2493,15 @@ class AlbertEJA(OctonionHermitianEJA): super().__init__(3, *args, **kwargs) -class HadamardEJA(ConcreteEJA, RationalBasisEJA): +class HadamardEJA(RationalBasisEJA, ConcreteEJA): """ - Return the Euclidean Jordan Algebra corresponding to the set - `R^n` under the Hadamard product. + Return the Euclidean Jordan algebra on `R^n` with the Hadamard + (pointwise real-number multiplication) Jordan product and the + usual inner-product. - Note: this is nothing more than the Cartesian product of ``n`` - copies of the spin algebra. Once Cartesian product algebras - are implemented, this can go. + This is nothing more than the Cartesian product of ``n`` copies of + the one-dimensional Jordan spin algebra, and is the most common + example of a non-simple Euclidean Jordan algebra. SETUP:: @@ -2832,16 +2532,16 @@ class HadamardEJA(ConcreteEJA, RationalBasisEJA): sage: HadamardEJA(3, prefix='r').gens() (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] @@ -2855,38 +2555,47 @@ class HadamardEJA(ConcreteEJA, RationalBasisEJA): 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) -class BilinearFormEJA(ConcreteEJA, RationalBasisEJA): +class BilinearFormEJA(RationalBasisEJA, ConcreteEJA): r""" The rank-2 simple EJA consisting of real vectors ``x=(x0, x_bar)`` with the half-trace inner product and jordan product ``x*y = @@ -2946,7 +2655,6 @@ class BilinearFormEJA(ConcreteEJA, RationalBasisEJA): matrix. We opt not to orthonormalize the basis, because if we did, we would have to normalize the `s_{i}` in a similar manner:: - sage: set_random_seed() sage: n = ZZ.random_element(5) sage: M = matrix.random(QQ, max(0,n-1), algorithm='unimodular') sage: B11 = matrix.identity(QQ,1) @@ -2982,22 +2690,22 @@ class BilinearFormEJA(ConcreteEJA, RationalBasisEJA): # 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 @@ -3008,6 +2716,7 @@ class BilinearFormEJA(ConcreteEJA, RationalBasisEJA): jordan_product, inner_product, field=field, + matrix_space=MS, associative=associative, **kwargs) @@ -3015,25 +2724,37 @@ class BilinearFormEJA(ConcreteEJA, RationalBasisEJA): # 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) @@ -3044,6 +2765,7 @@ class BilinearFormEJA(ConcreteEJA, RationalBasisEJA): 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 @@ -3094,7 +2816,6 @@ class JordanSpinEJA(BilinearFormEJA): Ensure that we have the usual inner product on `R^n`:: - sage: set_random_seed() sage: J = JordanSpinEJA.random_instance() sage: x,y = J.random_elements(2) sage: actual = x.inner_product(y) @@ -3116,25 +2837,22 @@ 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) -class TrivialEJA(ConcreteEJA, RationalBasisEJA): +class TrivialEJA(RationalBasisEJA, ConcreteEJA): """ The trivial Euclidean Jordan algebra consisting of only a zero element. @@ -3163,10 +2881,11 @@ class TrivialEJA(ConcreteEJA, RationalBasisEJA): 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 @@ -3176,6 +2895,8 @@ class TrivialEJA(ConcreteEJA, RationalBasisEJA): jordan_product, inner_product, associative=True, + field=field, + matrix_space=MS, **kwargs) # The rank is zero using my definition, namely the dimension of the @@ -3184,13 +2905,16 @@ class TrivialEJA(ConcreteEJA, RationalBasisEJA): 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) -class CartesianProductEJA(FiniteDimensionalEJA): +class CartesianProductEJA(EJA): r""" The external (orthogonal) direct sum of two or more Euclidean Jordan algebras. Every Euclidean Jordan algebra decomposes into an @@ -3202,6 +2926,7 @@ class CartesianProductEJA(FiniteDimensionalEJA): sage: from mjo.eja.eja_algebra import (random_eja, ....: CartesianProductEJA, + ....: ComplexHermitianEJA, ....: HadamardEJA, ....: JordanSpinEJA, ....: RealSymmetricEJA) @@ -3211,7 +2936,6 @@ class CartesianProductEJA(FiniteDimensionalEJA): The Jordan product is inherited from our factors and implemented by our CombinatorialFreeModule Cartesian product superclass:: - sage: set_random_seed() sage: J1 = HadamardEJA(2) sage: J2 = RealSymmetricEJA(2) sage: J = cartesian_product([J1,J2]) @@ -3313,6 +3037,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:: @@ -3326,7 +3072,6 @@ class CartesianProductEJA(FiniteDimensionalEJA): The cached unit element is the same one that would be computed:: - sage: set_random_seed() # long time sage: J1 = random_eja() # long time sage: J2 = random_eja() # long time sage: J = cartesian_product([J1,J2]) # long time @@ -3335,11 +3080,8 @@ class CartesianProductEJA(FiniteDimensionalEJA): sage: expected = J.one() # long time sage: actual == expected # long time True - """ - Element = FiniteDimensionalEJAElement - - + Element = CartesianProductEJAElement def __init__(self, factors, **kwargs): m = len(factors) if m == 0: @@ -3351,134 +3093,141 @@ 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] + ) - 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)) + 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() - def cartesian_factors(self): - # Copy/pasted from CombinatorialFreeModule_CartesianProduct. - return self._sets + # 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) ] - def cartesian_factor(self, i): - r""" - Return the ``i``th factor of this algebra. - """ - return self._sets[i] + # 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 - def _repr_(self): - # Copy/pasted from CombinatorialFreeModule_CartesianProduct. - from sage.categories.cartesian_product import cartesian_product - return cartesian_product.symbol.join("%s" % factor - for factor in self._sets) + 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)) - def matrix_space(self): + def _sets_keys(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) + TESTS: - :: + The superclass uses ``_sets_keys()`` to implement its + ``cartesian_factors()`` method:: - sage: J1 = ComplexHermitianEJA(1) - sage: J2 = ComplexHermitianEJA(1) + sage: J1 = RealSymmetricEJA(2, + ....: field=QQ, + ....: orthonormalize=False, + ....: prefix="a") + sage: J2 = ComplexHermitianEJA(2,field=QQ,orthonormalize=False) 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: x = sum(i*J.gens()[i] for i in range(len(J.gens()))) + sage: x.cartesian_factors() + (a1 + 2*a2, 3*b0 + 4*b1 + 5*b2 + 6*b3) - :: + """ + # Copy/pasted from CombinatorialFreeModule_CartesianProduct, + # but returning a tuple instead of a list. + return tuple(range(len(self.cartesian_factors()))) - 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 | - +----+ + def cartesian_factors(self): + # Copy/pasted from CombinatorialFreeModule_CartesianProduct. + return self._sets + def cartesian_factor(self, i): + r""" + Return the ``i``th factor of this algebra. """ - 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() ) + return self._sets[i] - from sage.sets.cartesian_product import CartesianProduct - return CartesianProduct(factors, cat) + def _repr_(self): + # Copy/pasted from CombinatorialFreeModule_CartesianProduct. + from sage.categories.cartesian_product import cartesian_product + return cartesian_product.symbol.join("%s" % factor + for factor in self._sets) @cached_method @@ -3541,7 +3290,6 @@ class CartesianProductEJA(FiniteDimensionalEJA): The answer never changes:: - sage: set_random_seed() sage: J1 = random_eja() sage: J2 = random_eja() sage: J = cartesian_product([J1,J2]) @@ -3557,7 +3305,7 @@ class CartesianProductEJA(FiniteDimensionalEJA): Pi = self._module_morphism(lambda j: Ji.monomial(j - offset), codomain=Ji) - return FiniteDimensionalEJAOperator(self,Ji,Pi.matrix()) + return EJAOperator(self,Ji,Pi.matrix()) @cached_method def cartesian_embedding(self, i): @@ -3631,7 +3379,6 @@ class CartesianProductEJA(FiniteDimensionalEJA): The answer never changes:: - sage: set_random_seed() sage: J1 = random_eja() sage: J2 = random_eja() sage: J = cartesian_product([J1,J2]) @@ -3644,7 +3391,6 @@ class CartesianProductEJA(FiniteDimensionalEJA): produce the identity map, and mismatching them should produce the zero map:: - sage: set_random_seed() sage: J1 = random_eja() sage: J2 = random_eja() sage: J = cartesian_product([J1,J2]) @@ -3667,11 +3413,39 @@ class CartesianProductEJA(FiniteDimensionalEJA): Ji = self.cartesian_factor(i) Ei = Ji._module_morphism(lambda j: self.monomial(j + offset), codomain=self) - return FiniteDimensionalEJAOperator(Ji,self,Ei.matrix()) + return EJAOperator(Ji,self,Ei.matrix()) + + + def subalgebra(self, basis, **kwargs): + r""" + Create a subalgebra of this algebra from the given basis. + Only overridden to allow us to use a special Cartesian product + subalgebra class. + SETUP:: -FiniteDimensionalEJA.CartesianProduct = CartesianProductEJA + sage: from mjo.eja.eja_algebra import (HadamardEJA, + ....: QuaternionHermitianEJA) + + EXAMPLES: + + Subalgebras of Cartesian product EJAs have a different class + than those of non-Cartesian-product EJAs:: + + sage: J1 = HadamardEJA(2,field=QQ,orthonormalize=False) + sage: J2 = QuaternionHermitianEJA(0,field=QQ,orthonormalize=False) + sage: J = cartesian_product([J1,J2]) + sage: K1 = J1.subalgebra((J1.one(),), orthonormalize=False) + sage: K = J.subalgebra((J.one(),), orthonormalize=False) + sage: K1.__class__ is K.__class__ + False + + """ + from mjo.eja.eja_subalgebra import CartesianProductEJASubalgebra + return CartesianProductEJASubalgebra(self, basis, **kwargs) + +EJA.CartesianProduct = CartesianProductEJA class RationalBasisCartesianProductEJA(CartesianProductEJA, RationalBasisEJA): @@ -3681,9 +3455,9 @@ class RationalBasisCartesianProductEJA(CartesianProductEJA, SETUP:: - sage: from mjo.eja.eja_algebra import (HadamardEJA, + sage: from mjo.eja.eja_algebra import (EJA, + ....: HadamardEJA, ....: JordanSpinEJA, - ....: OctonionHermitianEJA, ....: RealSymmetricEJA) EXAMPLES: @@ -3704,41 +3478,304 @@ 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 = EJA((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: 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]) + + +class ComplexSkewSymmetricEJA(RationalBasisEJA, ConcreteEJA): + r""" + The skew-symmetric EJA of order `2n` described in Faraut and + Koranyi's Exercise III.1.b. It has dimension `2n^2 - n`. + + It is (not obviously) isomorphic to the QuaternionHermitianEJA of + order `n`, as can be inferred by comparing rank/dimension or + explicitly from their "characteristic polynomial of" functions, + which just so happen to align nicely. + + SETUP:: + + sage: from mjo.eja.eja_algebra import (ComplexSkewSymmetricEJA, + ....: QuaternionHermitianEJA) + sage: from mjo.eja.eja_operator import EJAOperator + + EXAMPLES: + + This EJA is isomorphic to the quaternions:: + + sage: J = ComplexSkewSymmetricEJA(2, field=QQ, orthonormalize=False) + sage: K = QuaternionHermitianEJA(2, field=QQ, orthonormalize=False) + sage: jordan_isom_matrix = matrix.diagonal(QQ,[-1,1,1,1,1,-1]) + sage: phi = EJAOperator(J,K,jordan_isom_matrix) + sage: all( phi(x*y) == phi(x)*phi(y) + ....: for x in J.gens() + ....: for y in J.gens() ) + True + sage: x,y = J.random_elements(2) + sage: phi(x*y) == phi(x)*phi(y) + True + + TESTS: + + Random elements should satisfy the same conditions that the basis + elements do:: + + sage: K = ComplexSkewSymmetricEJA.random_instance(field=QQ, + ....: orthonormalize=False) + sage: x,y = K.random_elements(2) + sage: z = x*y + sage: x = x.to_matrix() + sage: y = y.to_matrix() + sage: z = z.to_matrix() + sage: all( e.is_skew_symmetric() for e in (x,y,z) ) + True + sage: J = -K.one().to_matrix() + sage: all( e*J == J*e.conjugate() for e in (x,y,z) ) + True + + The power law in Faraut & Koranyi's II.7.a is satisfied. + We're in a subalgebra of theirs, but powers are still + defined the same:: + + sage: K = ComplexSkewSymmetricEJA.random_instance(field=QQ, + ....: orthonormalize=False) + sage: x = K.random_element() + sage: k = ZZ.random_element(5) + sage: actual = x^k + sage: J = -K.one().to_matrix() + sage: expected = K(-J*(J*x.to_matrix())^k) + sage: actual == expected + True + + """ + @staticmethod + def _max_random_instance_size(max_dimension): + # Obtained by solving d = 2n^2 - n, which comes from noticing + # that, in 2x2 block form, any element of this algebra has a + # free skew-symmetric top-left block, a Hermitian top-right + # block, and two bottom blocks that are determined by the top. + # 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, max_dimension=None, *args, **kwargs): + """ + Return a random instance of this type of algebra. + """ + 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) + + @staticmethod + def _denormalized_basis(A): + """ + SETUP:: + + sage: from mjo.hurwitz import ComplexMatrixAlgebra + sage: from mjo.eja.eja_algebra import ComplexSkewSymmetricEJA + + TESTS: + + The basis elements are all skew-Hermitian:: + + sage: d_max = ComplexSkewSymmetricEJA._max_random_instance_dimension() + sage: n_max = ComplexSkewSymmetricEJA._max_random_instance_size(d_max) + sage: n = ZZ.random_element(n_max + 1) + sage: A = ComplexMatrixAlgebra(2*n, scalars=QQ) + sage: B = ComplexSkewSymmetricEJA._denormalized_basis(A) + sage: all( M.is_skew_symmetric() for M in B) + True + + The basis elements ``b`` all satisfy ``b*J == J*b.conjugate()``, + as in the definition of the algebra:: + + sage: d_max = ComplexSkewSymmetricEJA._max_random_instance_dimension() + sage: n_max = ComplexSkewSymmetricEJA._max_random_instance_size(d_max) + sage: n = ZZ.random_element(n_max + 1) + sage: A = ComplexMatrixAlgebra(2*n, scalars=QQ) + sage: I_n = matrix.identity(ZZ, n) + sage: J = matrix.block(ZZ, 2, 2, (0, I_n, -I_n, 0), subdivide=False) + sage: J = A.from_list(J.rows()) + sage: B = ComplexSkewSymmetricEJA._denormalized_basis(A) + sage: all( b*J == J*b.conjugate() for b in B ) + True + + """ + es = A.entry_algebra_gens() + gen = lambda A,m: A.monomial(m) + + basis = [] + + # The size of the blocks. We're going to treat these thing as + # 2x2 block matrices, + # + # [ x1 x2 ] + # [ -x2-conj x1-conj ] + # + # where x1 is skew-symmetric and x2 is Hermitian. + # + m = A.nrows()/2 + + # We only loop through the top half of the matrix, because the + # bottom can be constructed from the top. + for i in range(m): + # First do the top-left block, which is skew-symmetric. + # We can compute the bottom-right block in the process. + for j in range(i+1): + if i != j: + # Skew-symmetry implies zeros for (i == j). + for e in es: + # Top-left block's entry. + E_ij = gen(A, (i,j,e)) + E_ij -= gen(A, (j,i,e)) + + # Bottom-right block's entry. + F_ij = gen(A, (i+m,j+m,e)).conjugate() + F_ij -= gen(A, (j+m,i+m,e)).conjugate() + + basis.append(E_ij + F_ij) + + # Now do the top-right block, which is Hermitian, and compute + # the bottom-left block along the way. + for j in range(m,i+m+1): + if (i+m) == j: + # Hermitian matrices have real diagonal entries. + # Top-right block's entry. + E_ii = gen(A, (i,j,es[0])) + + # Bottom-left block's entry. Don't conjugate + # 'cause it's real. + E_ii -= gen(A, (i+m,j-m,es[0])) + basis.append(E_ii) + else: + for e in es: + # Top-right block's entry. BEWARE! We're not + # reflecting across the main diagonal as in + # (i,j)~(j,i). We're only reflecting across + # the diagonal for the top-right block. + E_ij = gen(A, (i,j,e)) + + # Shift it back to non-offset coords, transpose, + # conjugate, and put it back: + # + # (i,j) -> (i,j-m) -> (j-m, i) -> (j-m, i+m) + E_ij += gen(A, (j-m,i+m,e)).conjugate() + + # Bottom-left's block's below-diagonal entry. + # Just shift the top-right coords down m and + # left m. + F_ij = -gen(A, (i+m,j-m,e)).conjugate() + F_ij += -gen(A, (j,i,e)) # double-conjugate cancels + + basis.append(E_ij + F_ij) + + return tuple( basis ) + + @staticmethod + @cached_method + def _J_matrix(matrix_space): + n = matrix_space.nrows() // 2 + F = matrix_space.base_ring() + I_n = matrix.identity(F, n) + J = matrix.block(F, 2, 2, (0, I_n, -I_n, 0), subdivide=False) + return matrix_space.from_list(J.rows()) + + def J_matrix(self): + return ComplexSkewSymmetricEJA._J_matrix(self.matrix_space()) + + def __init__(self, n, field=AA, **kwargs): + # New code; always check the axioms. + #if "check_axioms" not in kwargs: kwargs["check_axioms"] = False + + from mjo.hurwitz import ComplexMatrixAlgebra + A = ComplexMatrixAlgebra(2*n, scalars=field) + J = ComplexSkewSymmetricEJA._J_matrix(A) + + def jordan_product(X,Y): + return (X*J*Y + Y*J*X)/2 + + def inner_product(X,Y): + return (X.conjugate_transpose()*Y).trace().real() + + super().__init__(self._denormalized_basis(A), + jordan_product, + inner_product, + field=field, + matrix_space=A, + **kwargs) + + # This algebra is conjectured (by me) to be isomorphic to + # the quaternion Hermitian EJA of size n, and the rank + # would follow from that. + #self.rank.set_cache(n) + self.one.set_cache( self(-J) )