From ff8c9b19da5ed821366a491a95b4f6c946f315ae Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Tue, 2 Mar 2021 15:35:08 -0500 Subject: [PATCH] eja: don't mess with the user's basis. This is a pre-pre-prerequisite for implementing the octonion algebras. In a generic matrix algebra, the scalar ring may not be the same ring that the entries of the matrix comes from. --- mjo/eja/DESIGN | 8 ++++++ mjo/eja/eja_algebra.py | 64 +++++++++++++++++++++--------------------- mjo/eja/eja_utils.py | 51 --------------------------------- 3 files changed, 40 insertions(+), 83 deletions(-) diff --git a/mjo/eja/DESIGN b/mjo/eja/DESIGN index 7607b1c..75302ca 100644 --- a/mjo/eja/DESIGN +++ b/mjo/eja/DESIGN @@ -81,6 +81,14 @@ the same way as the octonion algebra, but for the sake of the user interface, we must also support at least the usual SageMath vectors and matrices. +Note: this has one less-than-obvious consequence: we have to assume +that the user has supplied an entirely-correct basis (with entries in +the correct structure). We generally cannot mess witht the entries of +his basis, or use them to figure out what (for example) the ambient +scalar ring is. None of these are insurmountable obstacles; we just +have to be a little careful distinguishing between what's inside the +algebra elements and what's outside them. + Basis normalization ------------------- For performance reasons, we prefer the algebra constructors to diff --git a/mjo/eja/eja_algebra.py b/mjo/eja/eja_algebra.py index c55061e..4d0c802 100644 --- a/mjo/eja/eja_algebra.py +++ b/mjo/eja/eja_algebra.py @@ -152,11 +152,6 @@ class FiniteDimensionalEJA(CombinatorialFreeModule): # we've specified a real embedding. raise ValueError("scalar field is not real") - from mjo.eja.eja_utils import _change_ring - # If the basis given to us wasn't over the field that it's - # supposed to be over, fix that. Or, you know, crash. - basis = tuple( _change_ring(b, field) for b in basis ) - if check_axioms: # Check commutativity of the Jordan and inner-products. # This has to be done before we build the multiplication @@ -1917,7 +1912,7 @@ class RealSymmetricEJA(ConcreteEJA, RealMatrixEJA): """ @classmethod - def _denormalized_basis(cls, n): + def _denormalized_basis(cls, n, field): """ Return a basis for the space of real symmetric n-by-n matrices. @@ -1929,7 +1924,7 @@ class RealSymmetricEJA(ConcreteEJA, RealMatrixEJA): sage: set_random_seed() sage: n = ZZ.random_element(1,5) - sage: B = RealSymmetricEJA._denormalized_basis(n) + sage: B = RealSymmetricEJA._denormalized_basis(n,ZZ) sage: all( M.is_symmetric() for M in B) True @@ -1939,7 +1934,7 @@ class RealSymmetricEJA(ConcreteEJA, RealMatrixEJA): S = [] for i in range(n): for j in range(i+1): - Eij = matrix(ZZ, n, lambda k,l: k==i and l==j) + Eij = matrix(field, n, lambda k,l: k==i and l==j) if i == j: Sij = Eij else: @@ -1960,7 +1955,7 @@ class RealSymmetricEJA(ConcreteEJA, RealMatrixEJA): n = ZZ.random_element(cls._max_random_instance_size() + 1) return cls(n, **kwargs) - def __init__(self, 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 @@ -1969,9 +1964,10 @@ class RealSymmetricEJA(ConcreteEJA, RealMatrixEJA): if n <= 1: associative = True - super().__init__(self._denormalized_basis(n), + super().__init__(self._denormalized_basis(n,field), self.jordan_product, self.trace_inner_product, + field=field, associative=associative, **kwargs) @@ -2191,7 +2187,7 @@ class ComplexHermitianEJA(ConcreteEJA, ComplexMatrixEJA): """ @classmethod - def _denormalized_basis(cls, n): + def _denormalized_basis(cls, n, field): """ Returns a basis for the space of complex Hermitian n-by-n matrices. @@ -2209,15 +2205,14 @@ class ComplexHermitianEJA(ConcreteEJA, ComplexMatrixEJA): sage: set_random_seed() sage: n = ZZ.random_element(1,5) - sage: B = ComplexHermitianEJA._denormalized_basis(n) + sage: B = ComplexHermitianEJA._denormalized_basis(n,ZZ) sage: all( M.is_symmetric() for M in B) True """ - field = ZZ - R = PolynomialRing(field, 'z') + R = PolynomialRing(ZZ, 'z') z = R.gen() - F = field.extension(z**2 + 1, 'I') + F = ZZ.extension(z**2 + 1, 'I') I = F.gen(1) # This is like the symmetric case, but we need to be careful: @@ -2248,12 +2243,12 @@ class ComplexHermitianEJA(ConcreteEJA, ComplexMatrixEJA): # "erase" E_ij Eij[i,j] = 0 - # Since we embedded these, we can drop back to the "field" that we - # started with instead of the complex extension "F". + # 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, **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 @@ -2262,9 +2257,10 @@ class ComplexHermitianEJA(ConcreteEJA, ComplexMatrixEJA): if n <= 1: associative = True - super().__init__(self._denormalized_basis(n), + 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 @@ -2492,7 +2488,7 @@ class QuaternionHermitianEJA(ConcreteEJA, QuaternionMatrixEJA): """ @classmethod - def _denormalized_basis(cls, n): + def _denormalized_basis(cls, n, field): """ Returns a basis for the space of quaternion Hermitian n-by-n matrices. @@ -2510,12 +2506,11 @@ class QuaternionHermitianEJA(ConcreteEJA, QuaternionMatrixEJA): sage: set_random_seed() sage: n = ZZ.random_element(1,5) - sage: B = QuaternionHermitianEJA._denormalized_basis(n) + sage: B = QuaternionHermitianEJA._denormalized_basis(n,ZZ) sage: all( M.is_symmetric() for M in B ) True """ - field = ZZ Q = QuaternionAlgebra(QQ,-1,-1) I,J,K = Q.gens() @@ -2559,12 +2554,12 @@ class QuaternionHermitianEJA(ConcreteEJA, QuaternionMatrixEJA): # "erase" E_ij Eij[i,j] = 0 - # Since we embedded these, we can drop back to the "field" that we - # started with instead of the quaternion algebra "Q". + # 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, **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 @@ -2573,9 +2568,10 @@ class QuaternionHermitianEJA(ConcreteEJA, QuaternionMatrixEJA): if n <= 1: associative = True - super().__init__(self._denormalized_basis(n), + super().__init__(self._denormalized_basis(n,field), self.jordan_product, self.trace_inner_product, + field=field, associative=associative, **kwargs) @@ -2643,7 +2639,7 @@ class HadamardEJA(ConcreteEJA): (r0, r1, r2) """ - def __init__(self, n, **kwargs): + def __init__(self, n, field=AA, **kwargs): if n == 0: jordan_product = lambda x,y: x inner_product = lambda x,y: x @@ -2664,10 +2660,12 @@ class HadamardEJA(ConcreteEJA): if "orthonormalize" not in kwargs: kwargs["orthonormalize"] = False if "check_axioms" not in kwargs: kwargs["check_axioms"] = False - column_basis = tuple( b.column() for b in FreeModule(ZZ, n).basis() ) + column_basis = tuple( b.column() + for b in FreeModule(field, n).basis() ) super().__init__(column_basis, jordan_product, inner_product, + field=field, associative=True, **kwargs) self.rank.set_cache(n) @@ -2775,7 +2773,7 @@ class BilinearFormEJA(ConcreteEJA): True """ - def __init__(self, B, **kwargs): + def __init__(self, B, field=AA, **kwargs): # The matrix "B" is supplied by the user in most cases, # so it makes sense to check whether or not its positive- # definite unless we are specifically asked not to... @@ -2803,7 +2801,8 @@ class BilinearFormEJA(ConcreteEJA): return P([z0] + zbar.list()) n = B.nrows() - column_basis = tuple( b.column() for b in FreeModule(ZZ, n).basis() ) + column_basis = tuple( b.column() + for b in FreeModule(field, n).basis() ) # TODO: I haven't actually checked this, but it seems legit. associative = False @@ -2813,6 +2812,7 @@ class BilinearFormEJA(ConcreteEJA): super().__init__(column_basis, jordan_product, inner_product, + field=field, associative=associative, **kwargs) @@ -2908,7 +2908,7 @@ class JordanSpinEJA(BilinearFormEJA): True """ - def __init__(self, n, **kwargs): + def __init__(self, n, *args, **kwargs): # This is a special case of the BilinearFormEJA with the # identity matrix as its bilinear form. B = matrix.identity(ZZ, n) @@ -2919,7 +2919,7 @@ class JordanSpinEJA(BilinearFormEJA): # But also don't pass check_field=False here, because the user # can pass in a field! - super().__init__(B, **kwargs) + super().__init__(B, *args, **kwargs) @staticmethod def _max_random_instance_size(): diff --git a/mjo/eja/eja_utils.py b/mjo/eja/eja_utils.py index 81b5634..832dcef 100644 --- a/mjo/eja/eja_utils.py +++ b/mjo/eja/eja_utils.py @@ -2,57 +2,6 @@ from sage.functions.other import sqrt from sage.matrix.constructor import matrix from sage.modules.free_module_element import vector -def _change_ring(x, R): - r""" - Change the ring of a vector, matrix, or a cartesian product of - those things. - - SETUP:: - - sage: from mjo.eja.eja_utils import _change_ring - - EXAMPLES:: - - sage: v = vector(QQ, (1,2,3)) - sage: m = matrix(QQ, [[1,2],[3,4]]) - sage: _change_ring(v, RDF) - (1.0, 2.0, 3.0) - sage: _change_ring(m, RDF) - [1.0 2.0] - [3.0 4.0] - sage: _change_ring((v,m), RDF) - ( - [1.0 2.0] - (1.0, 2.0, 3.0), [3.0 4.0] - ) - sage: V1 = cartesian_product([v.parent(), v.parent()]) - sage: V = cartesian_product([v.parent(), V1]) - sage: V((v, (v, v))) - ((1, 2, 3), ((1, 2, 3), (1, 2, 3))) - sage: _change_ring(V((v, (v, v))), RDF) - ((1.0, 2.0, 3.0), ((1.0, 2.0, 3.0), (1.0, 2.0, 3.0))) - - """ - try: - return x.change_ring(R) - except AttributeError: - try: - from sage.categories.sets_cat import cartesian_product - if hasattr(x, 'element_class'): - # x is a parent and we're in a recursive call. - return cartesian_product( [_change_ring(x_i, R) - for x_i in x.cartesian_factors()] ) - else: - # x is an element, and we want to change the ring - # of its parent. - P = x.parent() - Q = cartesian_product( [_change_ring(P_i, R) - for P_i in P.cartesian_factors()] ) - return Q(x) - except AttributeError: - # No parent for x - return x.__class__( _change_ring(x_i, R) for x_i in x ) - def _scale(x, alpha): r""" Scale the vector, matrix, or cartesian-product-of-those-things -- 2.44.2