From f6102336f37a0fd2f44a5b511e2f160c47f0bd6a Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Mon, 28 Jul 2025 22:09:15 +0200 Subject: [PATCH 1/7] submodules and quotients over PID --- src/sage/modules/ore_module.py | 185 ++++++++++++------------ src/sage/modules/ore_module_morphism.py | 46 +++--- src/sage/modules/subspace_helper.py | 126 ++++++++++++++++ 3 files changed, 243 insertions(+), 114 deletions(-) create mode 100644 src/sage/modules/subspace_helper.py diff --git a/src/sage/modules/ore_module.py b/src/sage/modules/ore_module.py index 130aec42f1c..9f65cb50554 100644 --- a/src/sage/modules/ore_module.py +++ b/src/sage/modules/ore_module.py @@ -187,7 +187,6 @@ from sage.structure.unique_representation import UniqueRepresentation from sage.categories.action import Action -from sage.categories.fields import Fields from sage.categories.ore_modules import OreModules from sage.matrix.matrix0 import Matrix @@ -197,6 +196,7 @@ from sage.rings.polynomial.ore_polynomial_element import OrePolynomial from sage.modules.free_module import FreeModule_ambient from sage.modules.free_module_element import FreeModuleElement_generic_dense +from sage.modules.subspace_helper import SubspaceHelper from sage.modules.ore_module_element import OreModuleElement # Action by left multiplication on Ore modules @@ -1226,25 +1226,35 @@ def _span(self, gens): zero = rank * [base.zero()] rows += (2*rank - len(rows)) * [zero] M = matrix(base, rows) - M.echelonize() - oldr = 0 - r = M.rank() - iter = 1 - while r > oldr: - for i in range(r): + if hasattr(M, 'popov_form'): + def normalize(M): + N = M.popov_form() + for i in range(N.nrows()): + for j in range(N.ncols()): + M[i,j] = N[i,j] + else: + normalize = M.__class__.echelonize + g = f + normalize(M) + sM = None + while True: + r = 0 + for i in range(rank): v = M.row(i) - for _ in range(iter): - v = f(v) - v = v.list() + if v == 0: + break + v = g(v).list() for j in range(rank): - M[i+r, j] = v[j] - M.echelonize() - oldr = r - r = M.rank() - iter *= 2 + M[i+rank, j] = v[j] + r += 1 + normalize(M) + if M.list() == sM: + break + sM = M.list() + g = g * g return M.matrix_from_rows(range(r)) - def span(self, gens, names=None): + def span(self, gens, saturate=False, names=None, check=True): r""" Return the submodule of this Ore module generated (over the underlying Ore ring) by ``gens``. @@ -1256,6 +1266,8 @@ def span(self, gens, names=None): - ``names`` (default: ``None``) -- the name of the vectors in a basis of this submodule + - ``check`` (default: ``True``) -- a boolean, ignored + EXAMPLES:: sage: K. = Frac(GF(5)['t']) @@ -1327,9 +1339,9 @@ def span(self, gens, names=None): :meth:`quotient` """ gens = self._span(gens) - return self._submodule_class(self, gens, names=names) + return self._submodule_class(self, gens, saturate, names) - def quotient(self, sub, names=None, check=True): + def quotient(self, sub, remove_torsion=False, names=None, check=True): r""" Return the quotient of this Ore module by the submodule generated (over the underlying Ore ring) by ``gens``. @@ -1402,7 +1414,8 @@ def quotient(self, sub, names=None, check=True): :meth:`quo`, :meth:`span` """ gens = self._span(sub) - return self._quotientModule_class(self, gens, names=names) + check_torsion = not remove_torsion + return self._quotientModule_class(self, gens, remove_torsion, names) quo = quotient @@ -1452,7 +1465,7 @@ class OreSubmodule(OreModule): r""" Class for submodules of Ore modules. """ - def __classcall_private__(cls, ambient, gens, names): + def __classcall_private__(cls, ambient, gens, saturate, names): r""" Normalize the input before passing it to the init function (useful to ensure the uniqueness assupmtion). @@ -1480,30 +1493,27 @@ def __classcall_private__(cls, ambient, gens, names): :: - sage: R. = QQ[] + sage: R. = QQ[] sage: S. = OrePolynomialRing(R, R.derivation()) - sage: M. = S.quotient_module((X + t)^2) - sage: M.span((X + t)*v) + sage: M. = S.quotient_module((X + x + y)^2) + sage: M.span((X + x + y)*v) Traceback (most recent call last): ... - NotImplementedError: Ore submodules are currently only implemented over fields + NotImplementedError: subspaces and quotients are only implemented over PID """ base = ambient.base_ring() - if base not in Fields(): - raise NotImplementedError("Ore submodules are currently only implemented over fields") - if isinstance(gens, Matrix): - basis = gens + if isinstance(gens, SubspaceHelper): + if not saturate or gens.is_saturated: + subspace = gens + else: + subspace = SubspaceHelper(gens.basis, saturate) else: basis = matrix(base, gens) - basis = basis.echelon_form() - basis.set_immutable() - rank = basis.rank() - if basis.nrows() != rank: - basis = basis.matrix_from_rows(range(rank)) - names = normalize_names(names, rank) - return cls.__classcall__(cls, ambient, basis, names) + subspace = SubspaceHelper(basis, saturate) + names = normalize_names(names, subspace.rank) + return cls.__classcall__(cls, ambient, subspace, names) - def __init__(self, ambient, basis, names) -> None: + def __init__(self, ambient, subspace, names) -> None: r""" Initialize this Ore submodule. @@ -1531,12 +1541,13 @@ def __init__(self, ambient, basis, names) -> None: from sage.modules.ore_module_morphism import OreModuleRetraction base = ambient.base_ring() self._ambient = ambient - self._basis = basis - rows = [basis.solve_left(ambient(x).image()) for x in basis.rows()] + self._subspace = subspace + C = subspace.coordinates.matrix_from_columns(range(subspace.rank)) + rows = [ambient(x).image() * C for x in subspace.basis.rows()] OreModule.__init__(self, matrix(base, rows), ambient.ore_ring(action=False), names, ambient._ore_category) - coerce = self.hom(basis, codomain=ambient) + coerce = self.hom(subspace.basis, codomain=ambient) ambient.register_coercion(coerce) self._inject = coerce.__copy__() self.register_conversion(OreModuleRetraction(ambient, self)) @@ -1594,6 +1605,12 @@ def ambient(self): """ return self._ambient + def saturate(self, names=None, coerce=False): + M = self._submodule_class(self._ambient, self._subspace, True, names) + if coerce: + raise NotImplementedError + return M + def rename_basis(self, names, coerce=False): r""" Return the same Ore module with the given naming @@ -1677,7 +1694,7 @@ def rename_basis(self, names, coerce=False): rank = self.rank() names = normalize_names(names, rank) cls = self.__class__ - M = cls.__classcall__(cls, self._ambient, self._basis, names) + M = cls.__classcall__(cls, self._ambient, self._subspace, names) if coerce: mat = identity_matrix(self.base_ring(), rank) id = self.hom(mat, codomain=M) @@ -1781,12 +1798,12 @@ def morphism_corestriction(self, f): if f.codomain() is not self._ambient: raise ValueError("the codomain of the morphism must be the ambient space") rows = [] - basis = self._basis + C = self._subspace.coordinates try: - rows = [basis.solve_left(y) for y in f._matrix.rows()] + im_gens = [self(f(x)) for x in f.domain().basis()] except ValueError: raise ValueError("the image of the morphism is not contained in this submodule") - return f.domain().hom(rows, codomain=self) + return f.domain().hom(im_gens, codomain=self) _hom_change_domain = morphism_restriction _hom_change_codomain = morphism_corestriction @@ -1799,7 +1816,7 @@ class OreQuotientModule(OreModule): r""" Class for quotients of Ore modules. """ - def __classcall_private__(cls, cover, gens, names): + def __classcall_private__(cls, cover, gens, remove_torsion, names): r""" Normalize the input before passing it to the init function (useful to ensure the uniqueness assumption). @@ -1827,30 +1844,29 @@ def __classcall_private__(cls, cover, gens, names): :: - sage: R. = QQ[] - sage: S. = OrePolynomialRing(R, R.derivation()) - sage: M. = S.quotient_module((X + t)^2) - sage: M.quo((X + t)*v) + sage: R. = QQ[] + sage: S. = OrePolynomialRing(R, R.derivation(x)) + sage: M. = S.quotient_module((X + x + y)^2) + sage: M.quo((X + x + y)*v) Traceback (most recent call last): ... - NotImplementedError: quotient of Ore modules are currently only implemented over fields + NotImplementedError: subspaces and quotients are only implemented over PID """ base = cover.base_ring() - if base not in Fields(): - raise NotImplementedError("quotient of Ore modules are currently only implemented over fields") - if isinstance(gens, Matrix): - basis = gens + if isinstance(gens, SubspaceHelper): + if not remove_torsion or gens.is_saturated: + subspace = gens + else: + subspace = SubspaceHelper(gens.basis, remove_torsion) else: basis = matrix(base, gens) - basis = basis.echelon_form() - basis.set_immutable() - rank = basis.rank() - if basis.nrows() != rank: - basis = basis.matrix_from_rows(range(rank)) - names = normalize_names(names, cover.rank() - rank) - return cls.__classcall__(cls, cover, basis, names) - - def __init__(self, cover, basis, names) -> None: + subspace = SubspaceHelper(basis, remove_torsion) + if not subspace.is_saturated: + raise NotImplementedError("torsion Ore modules are not implemented") + names = normalize_names(names, cover.rank() - subspace.rank) + return cls.__classcall__(cls, cover, subspace, names) + + def __init__(self, cover, subspace, names) -> None: r""" Initialize this Ore quotient. @@ -1880,26 +1896,13 @@ def __init__(self, cover, basis, names) -> None: self._cover = cover d = cover.rank() base = cover.base_ring() - self._relations = basis - pivots = basis.pivots() - r = basis.rank() - coerce = matrix(base, d, d-r) - indices = [] - i = 0 - for j in range(d): - if i < r and pivots[i] == j: - i += 1 - else: - indices.append(j) - coerce[j, j-i] = base.one() - for i in range(r): - for j in range(d-r): - coerce[pivots[i], j] = -basis[i, indices[j]] - rows = [cover.gen(i).image() * coerce for i in indices] - OreModule.__init__(self, matrix(base, rows), + self._subspace = subspace + rank = subspace.rank + coerce = subspace.coordinates.matrix_from_columns(range(rank, d)) + images = [cover(x).image() for x in subspace.complement.rows()] + OreModule.__init__(self, matrix(base, d-rank, d, images) * coerce, cover.ore_ring(action=False), names, cover._ore_category) - self._indices = indices self._project = coerce = cover.hom(coerce, codomain=self) self.register_coercion(coerce) cover.register_conversion(OreModuleSection(self, cover)) @@ -1921,12 +1924,7 @@ def _repr_element(self, x) -> str: w """ M = self._cover - indices = self._indices - base = self.base_ring() - coords = M.rank() * [base.zero()] - for i in range(self.rank()): - coords[indices[i]] = x[i] - return M(coords)._repr_() + return M(x)._repr_() def _latex_element(self, x) -> str: r""" @@ -1945,12 +1943,7 @@ def _latex_element(self, x) -> str: \overline{w} """ M = self._cover - indices = self._indices - base = self.base_ring() - coords = M.rank() * [base.zero()] - for i in range(self.rank()): - coords[indices[i]] = x[i] - return "\\overline{%s}" % M(coords)._latex_() + return "\\overline{%s}" % M(x)._latex_() def cover(self): r""" @@ -2011,7 +2004,7 @@ def relations(self, names=None): :meth:`relations` """ - return self._submodule_class(self._cover, self._relations, names=names) + return self._submodule_class(self._cover, self._subspace, False, names) def rename_basis(self, names, coerce=False): r""" @@ -2097,7 +2090,7 @@ def rename_basis(self, names, coerce=False): rank = self.rank() names = normalize_names(names, rank) cls = self.__class__ - M = cls.__classcall__(cls, self._cover, self._relations, names) + M = cls.__classcall__(cls, self._cover, self._subspace, names) if coerce: mat = identity_matrix(self.base_ring(), rank) id = self.hom(mat, codomain=M) @@ -2163,10 +2156,10 @@ def morphism_quotient(self, f): """ if f.domain() is not self._cover: raise ValueError("the domain of the morphism must be the cover ring") - Z = self._relations * f._matrix + Z = self._subspace.basis * f._matrix if not Z.is_zero(): raise ValueError("the morphism does not factor through this quotient") - mat = f._matrix.matrix_from_rows(self._indices) + mat = self._subspace.complement * f._matrix return self.hom(mat, codomain=f.codomain()) def morphism_modulo(self, f): diff --git a/src/sage/modules/ore_module_morphism.py b/src/sage/modules/ore_module_morphism.py index 524b68b2a0d..a7c904fd178 100644 --- a/src/sage/modules/ore_module_morphism.py +++ b/src/sage/modules/ore_module_morphism.py @@ -612,7 +612,7 @@ def is_injective(self) -> bool: sage: g.is_injective() False """ - return self._matrix.rank() == self.domain().rank() + return self.kernel().rank() == 0 def is_surjective(self) -> bool: r""" @@ -638,7 +638,7 @@ def is_surjective(self) -> bool: sage: g.is_surjective() True """ - return self._matrix.rank() == self.codomain().rank() + return self.image()._subspace.basis.is_one() def is_bijective(self) -> bool: r""" @@ -756,10 +756,15 @@ def kernel(self, names=None): m1 + (z+3)*m3 + (z^2+z+4)*m4, m2 + (2*z^2+4*z+2)*m4 + (2*z^2+z+1)*m5] """ - ker = self._matrix.left_kernel_matrix() - return OreSubmodule(self.domain(), ker, names) + mat = self._matrix + if hasattr(mat, 'minimal_kernel_matrix'): + # Optimisation + ker = mat.minimal_kernel_matrix() + else: + ker = mat.left_kernel_matrix() + return OreSubmodule(self.domain(), ker, False, names) - def image(self, names=None): + def image(self, saturate=False, names=None): r""" Return ``True`` if this morphism is injective. @@ -784,9 +789,9 @@ def image(self, names=None): m1 + (z+3)*m3 + (z^2+z+4)*m4, m2 + (2*z^2+4*z+2)*m4 + (2*z^2+z+1)*m5] """ - return OreSubmodule(self.codomain(), self._matrix, names) + return OreSubmodule(self.codomain(), self._matrix, saturate, names) - def cokernel(self, names=None): + def cokernel(self, remove_torsion=False, names=None): r""" Return ``True`` if this morphism is injective. @@ -809,7 +814,7 @@ def cokernel(self, names=None): sage: coker.basis() [m3, m4, m5] """ - return OreQuotientModule(self.codomain(), self._matrix, names) + return OreQuotientModule(self.codomain(), self._matrix, remove_torsion, names) def coimage(self, names=None): r""" @@ -834,8 +839,13 @@ def coimage(self, names=None): sage: coim.basis() [m3, m4, m5] """ - ker = self._matrix.left_kernel_matrix() - return OreQuotientModule(self.domain(), ker, names) + mat = self._matrix + if hasattr(mat, 'minimal_kernel_matrix'): + # Optimisation + ker = mat.minimal_kernel_matrix() + else: + ker = mat.left_kernel_matrix() + return OreQuotientModule(self.domain(), ker, False, names) def determinant(self): r""" @@ -928,9 +938,14 @@ def _call_(self, y): ValueError: not in the submodule """ X = self.codomain() + subspace = X._subspace + rank = subspace.rank + xs = y * subspace.coordinates + if xs[rank:]: + raise ValueError("not in the submodule") try: - xs = X._basis.solve_left(y) - except ValueError: + xs = xs[:rank].change_ring(X.base_ring()) + except (ValueError, TypeError): raise ValueError("not in the submodule") return X(xs) @@ -958,9 +973,4 @@ def _call_(self, y): """ X = self.codomain() Y = self.domain() - indices = Y._indices - zero = X.base_ring().zero() - xs = X.rank() * [zero] - for i in range(Y.rank()): - xs[indices[i]] = y[i] - return X(xs) + return X(y * Y._subspace.complement) diff --git a/src/sage/modules/subspace_helper.py b/src/sage/modules/subspace_helper.py new file mode 100644 index 00000000000..4cc5bd83bf1 --- /dev/null +++ b/src/sage/modules/subspace_helper.py @@ -0,0 +1,126 @@ +from sage.misc.classcall_metaclass import ClasscallMetaclass +from sage.misc.lazy_attribute import lazy_attribute +from sage.categories.fields import Fields +from sage.categories.principal_ideal_domains import PrincipalIdealDomains +from sage.matrix.matrix_polynomial_dense import Matrix_polynomial_dense +from sage.matrix.constructor import matrix + + +class SubspaceHelper(metaclass=ClasscallMetaclass): + def __classcall_private__(self, mat, saturate=False): + base = mat.base_ring() + if base in Fields(): + cls = SubspaceHelper_field + elif (isinstance(mat, Matrix_polynomial_dense) + and base.base_ring() in Fields()): + cls = SubspaceHelper_polynomial_ring + elif base in PrincipalIdealDomains(): + cls = SubspaceHelper_PID + else: + raise NotImplementedError("subspaces and quotients are only implemented over PID") + return cls.__call__(mat, saturate) + + def __hash__(self): + return hash(self.basis) + + def __eq__(self, other): + return self.basis == other.basis + + def __repr__(self): + return self.basis.__repr__() + + +class SubspaceHelper_field(SubspaceHelper): + def __init__(self, mat, saturate): + base = mat.base_ring() + n = mat.ncols() + basis = mat.echelon_form() + self.rank = r = basis.rank() + pivots = basis.pivots() + self.basis = basis.matrix_from_rows(range(r)) + self.basis.set_immutable() + self.complement = matrix(base, n-r, n) + self.coordinates = matrix(base, n, n) + indices = [] + i = 0 + for j in range(n): + if i < r and pivots[i] == j: + self.coordinates[j, i] = base.one() + i += 1 + else: + indices.append(j) + self.complement[j-i, j] = base.one() + self.coordinates[j, j-i+r] = base.one() + for i in range(r): + for j in range(n-r): + self.coordinates[pivots[i], j+r] = -basis[i, indices[j]] + self.is_saturated = True + + +class SubspaceHelper_PID(SubspaceHelper): + def __init__(self, mat, saturate): + base = mat.base_ring() + n = mat.ncols() + S, U, V = mat.smith_form() + r = 0 + for i in range(min(S.nrows(), S.ncols())): + if S[i,i] == 0: + break + r += 1 + self.rank = r + W = V.inverse().change_ring(base) + if saturate: + self.basis = W.matrix_from_rows(range(r)) + self.complement = W.matrix_from_rows(range(r, n)) + self.coordinates = V + self.is_saturated = True + else: + S = S.matrix_from_rows(range(r)) + self.basis = matrix(base, [[S[i,i]*W[i,j] for j in range(n)] + for i in range(r)]) + self.complement = W.matrix_from_rows(range(r, n)) + K = base.fraction_field() + scalars = [~S[i,i] for i in range(r)] + (n-r)*[K.one()] + self.coordinates = matrix(K, [[scalars[j]*V[i,j] for j in range(n)] + for i in range(n)]) + self.is_saturated = all(S[i,i].is_unit() for i in range(r)) + self.basis.set_immutable() + + +class SubspaceHelper_polynomial_ring(SubspaceHelper): + def __init__(self, mat, saturate): + base = mat.base_ring() + if saturate: + S, _, V = mat.smith_form() + W = V.inverse().change_ring(base) + r = 0 + for i in range(min(S.nrows(), S.ncols())): + if S[i,i] == 0: + break + r += 1 + mat = W.matrix_from_rows(range(r)) + self.is_saturated = True + self.basis = mat.popov_form(include_zero_vectors=False) + self.basis.set_immutable() + self.rank = self.basis.nrows() + + @lazy_attribute + def _popov(self): + return self.basis.stack(self.complement).popov_form(transformation=True) + + @lazy_attribute + def complement(self): + return self.basis.basis_completion().popov_form() + + @lazy_attribute + def coordinates(self): + P, T = self._popov + if P.is_one(): + return T + else: + return self.basis.stack(self.complement).inverse() + + @lazy_attribute + def is_saturated(self): + P, _ = self._popov + return P.is_one() From ee5c61ecd039912046b93ccd6441c6241756c0a2 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Mon, 28 Jul 2025 22:27:11 +0200 Subject: [PATCH 2/7] pickling --- src/sage/modules/ore_module.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/sage/modules/ore_module.py b/src/sage/modules/ore_module.py index 9f65cb50554..bbc121d3751 100644 --- a/src/sage/modules/ore_module.py +++ b/src/sage/modules/ore_module.py @@ -1552,6 +1552,9 @@ def __init__(self, ambient, subspace, names) -> None: self._inject = coerce.__copy__() self.register_conversion(OreModuleRetraction(ambient, self)) + def __reduce__(self): + return self._submodule_class, (self._ambient, self._subspace, False, self._names) + def _repr_element(self, x) -> str: r""" Return a string representation of ``x``. @@ -1907,6 +1910,9 @@ def __init__(self, cover, subspace, names) -> None: self.register_coercion(coerce) cover.register_conversion(OreModuleSection(self, cover)) + def __reduce__(self): + return self._quotient_class, (self._cover, self._subspace, False, self._names) + def _repr_element(self, x) -> str: r""" Return a string representation of `x`. From 56053cd223f419f7ba895b5725d047d5bb2f6e75 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Wed, 30 Jul 2025 15:35:19 +0200 Subject: [PATCH 3/7] submodules of submodules and Fitting indexes --- src/sage/modules/ore_module.py | 124 ++++++++++++++++++++++++++++++--- 1 file changed, 113 insertions(+), 11 deletions(-) diff --git a/src/sage/modules/ore_module.py b/src/sage/modules/ore_module.py index bbc121d3751..801e081ccdb 100644 --- a/src/sage/modules/ore_module.py +++ b/src/sage/modules/ore_module.py @@ -193,6 +193,7 @@ from sage.matrix.constructor import matrix from sage.matrix.special import identity_matrix +from sage.rings.infinity import Infinity from sage.rings.polynomial.ore_polynomial_element import OrePolynomial from sage.modules.free_module import FreeModule_ambient from sage.modules.free_module_element import FreeModuleElement_generic_dense @@ -400,6 +401,13 @@ def __init__(self, mat, ore, names, category) -> None: self._quotientModule_class = OreQuotientModule self._pseudohom = FreeModule_ambient.pseudohom(self, mat, ore, codomain=self) + def _element_constructor_(self, x): + if isinstance(x, OreModuleElement): + M = x.parent()._pushout_(self) + if M is not None: + return self(M(x)) + return super()._element_constructor_(x) + def _repr_(self) -> str: r""" Return a string representation of this Ore module. @@ -1216,10 +1224,14 @@ def _span(self, gens): rows = [] for gen in gens: if isinstance(gen, OreModule): - incl = self.coerce_map_from(gen) - if incl is None: + if not gen.is_submodule(self): raise ValueError("not canonically a submodule") - rows += incl._matrix.rows() + incl = self.coerce_map_from(gen) + if incl is not None: + rows += incl._matrix.rows() + else: + for x in gen.basis(): + rows.append(self(x).list()) elif isinstance(gen, OreModuleElement): rows.append(self(gen).list()) if len(rows) < 2*rank: @@ -1341,6 +1353,8 @@ def span(self, gens, saturate=False, names=None, check=True): gens = self._span(gens) return self._submodule_class(self, gens, saturate, names) + submodule = span + def quotient(self, sub, remove_torsion=False, names=None, check=True): r""" Return the quotient of this Ore module by the submodule @@ -1419,6 +1433,52 @@ def quotient(self, sub, remove_torsion=False, names=None, check=True): quo = quotient + def ambient_modules(self): + return [self] + + def _pushout_(self, other): + if isinstance(other, OreModule): + ambients = self.ambient_modules() + for M in other.ambient_modules(): + if M in ambients: + return M + + def is_submodule(self, other): + M = self._pushout_(other) + if M is None: + return False + return all(M(x) in other for x in self.basis()) + + def _fitting_index(self): + if self is self.ambient_module(): + return self.base_ring().one() + raise NotImplementedError("Fitting indexes are not implemented for this Ore module") + + def fitting_index(self, other=None): + if other is None: + return self._fitting_index() + ambients = self.ambient_modules() + if other in ambients: + index = self.base_ring().one() + for amb in ambients: + if other is amb: + return index + index *= amb._fitting_index() + M = self._pushout_(other) + if M is None: + raise ValueError("the two submodules do not live in a common ambient space") + N = M.span(self, other) + Ns = N.span(self) + No = N.span(other) + denom = No._fitting_index() + if denom: + return Ns._fitting_index() / denom + else: + return Infinity + + def covers(self): + return [self] + def __eq__(self, other) -> bool: r""" Return ``True`` if this Ore module is the same than ``other``. @@ -1550,7 +1610,12 @@ def __init__(self, ambient, subspace, names) -> None: coerce = self.hom(subspace.basis, codomain=ambient) ambient.register_coercion(coerce) self._inject = coerce.__copy__() - self.register_conversion(OreModuleRetraction(ambient, self)) + retract = self._retract = OreModuleRetraction(ambient, self) + self.register_conversion(retract) + while isinstance(ambient, OreSubmodule): + retract = retract * ambient._retract + self.register_conversion(retract) + ambient = ambient.ambient_module() def __reduce__(self): return self._submodule_class, (self._ambient, self._subspace, False, self._names) @@ -1591,7 +1656,7 @@ def _latex_element(self, x) -> str: """ return self._ambient(x)._latex_() - def ambient(self): + def ambient_module(self): r""" Return the ambient Ore module in which this submodule lives. @@ -1601,18 +1666,35 @@ def ambient(self): sage: S. = OrePolynomialRing(K, K.frobenius_endomorphism()) sage: M. = S.quotient_module((X + z)^2) sage: N = M.span((X + z)*v) - sage: N.ambient() + sage: N.ambient_module() Ore module over Finite Field in z of size 5^3 twisted by z |--> z^5 - sage: N.ambient() is M + sage: N.ambient_module() is M True """ return self._ambient + def ambient_modules(self): + ambients = [self] + ambient = self + while isinstance(ambient, OreSubmodule): + ambient = ambient._ambient + ambients.append(ambient) + return ambients + def saturate(self, names=None, coerce=False): - M = self._submodule_class(self._ambient, self._subspace, True, names) + subspace = self._subspace + if subspace.is_saturated: + return self.rename_basis(names, coerce) + S = self._submodule_class(self._ambient, subspace, True, names) if coerce: - raise NotImplementedError - return M + M = self._ambient + base = self.base_ring() + rank = self.rank() + mat = matrix(base, rank, [S(M(x)) for x in self.basis()]) + f = self.hom(mat, codomain=S) + S._unset_coercions_used() + S.register_coercion(f) + return S def rename_basis(self, names, coerce=False): r""" @@ -1705,6 +1787,13 @@ def rename_basis(self, names, coerce=False): M.register_coercion(id) return M + def _fitting_index(self): + subspace = self._subspace + if subspace.rank != self._ambient.rank(): + return self.base_ring().zero() + else: + return subspace.basis.determinant() + def injection_morphism(self): r""" Return the inclusion of this submodule in the ambient space. @@ -1908,7 +1997,12 @@ def __init__(self, cover, subspace, names) -> None: names, cover._ore_category) self._project = coerce = cover.hom(coerce, codomain=self) self.register_coercion(coerce) - cover.register_conversion(OreModuleSection(self, cover)) + section = self._section = OreModuleSection(self, cover) + cover.register_conversion(section) + while isinstance(cover, OreQuotientModule): + section = cover._section * section + cover = cover.cover() + cover.register_conversion(section) def __reduce__(self): return self._quotient_class, (self._cover, self._subspace, False, self._names) @@ -1973,6 +2067,14 @@ def cover(self): """ return self._cover + def covers(self): + covers = [self] + cover = self + while isinstance(cover, OreQuotientModule): + cover = cover._cover + covers.append(cover) + return covers + def relations(self, names=None): r""" If this quotient in `M/N`, return `N`. From eaa437b9f6d9d4337de6e2950e2c8045eb1f087f Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sat, 2 Aug 2025 06:09:34 +0200 Subject: [PATCH 4/7] doctests in ore_module.py --- src/sage/modules/ore_module.py | 502 +++++++++++++++++++++++++++++++-- 1 file changed, 471 insertions(+), 31 deletions(-) diff --git a/src/sage/modules/ore_module.py b/src/sage/modules/ore_module.py index 801e081ccdb..d61091edd50 100644 --- a/src/sage/modules/ore_module.py +++ b/src/sage/modules/ore_module.py @@ -262,7 +262,6 @@ def _act_(self, P, x): # Generic class for Ore modules ############################### - def normalize_names(names, rank): r""" Return a normalized form of ``names``. @@ -402,6 +401,32 @@ def __init__(self, mat, ore, names, category) -> None: self._pseudohom = FreeModule_ambient.pseudohom(self, mat, ore, codomain=self) def _element_constructor_(self, x): + r""" + Return the element of this parent constructed from ``x``. + + INPUT: + + - ``x`` -- an element in another Ore module, or a list + of coordinates + + EXAMPLES:: + + sage: A. = GF(5)[] + sage: f = A.hom([t+1]) + sage: S. = OrePolynomialRing(A, f) + sage: M = S.quotient_module(X^2 + t) + sage: M((1, t)) # indirect doctest + (1, t) + + We construct an element from a submodule:: + + sage: N = M.span((t, 0)) + sage: v = N.gen(0) + sage: v + (t, 0) + sage: M(v) + (t, 0) + """ if isinstance(x, OreModuleElement): M = x.parent()._pushout_(self) if M is not None: @@ -1219,7 +1244,7 @@ def _span(self, gens): base = self.base_ring() rank = self.rank() f = self._pseudohom - if not isinstance(gens, (list, tuple)): + if not isinstance(gens, list): gens = [gens] rows = [] for gen in gens: @@ -1232,7 +1257,7 @@ def _span(self, gens): else: for x in gen.basis(): rows.append(self(x).list()) - elif isinstance(gen, OreModuleElement): + else: rows.append(self(gen).list()) if len(rows) < 2*rank: zero = rank * [base.zero()] @@ -1268,13 +1293,22 @@ def normalize(M): def span(self, gens, saturate=False, names=None, check=True): r""" - Return the submodule of this Ore module generated (over the - underlying Ore ring) by ``gens``. + Return the submodule or saturated submodule of this Ore module + generated (over the underlying Ore ring) by ``gens``. + + We recall that a submodule `N` of `M` is called saturated if the + quotient `M/N` has no torsion. + The saturation of `N` in `M` is the submodule `N' \subset M` + consisting of vectors `x \in M` such that `a x \in N` for some + nonzero `a` in the base ring. INPUT: - ``gens`` -- a list of vectors or submodules of this Ore module + - ``saturate`` (default: ``False``) -- a boolean; if ``True``, + return the saturation of the submodule generated by ``gens`` + - ``names`` (default: ``None``) -- the name of the vectors in a basis of this submodule @@ -1282,9 +1316,9 @@ def span(self, gens, saturate=False, names=None, check=True): EXAMPLES:: - sage: K. = Frac(GF(5)['t']) - sage: S. = OrePolynomialRing(K, K.derivation()) - sage: P = X^2 + t*X + 1 + sage: A. = GF(5)['t'] + sage: S. = OrePolynomialRing(A, A.derivation()) + sage: P = X^2 + t*X + t sage: M = S.quotient_module(P^3, names='e') sage: M.inject_variables() Defining e0, e1, e2, e3, e4, e5 @@ -1293,18 +1327,44 @@ def span(self, gens, saturate=False, names=None, check=True): sage: MP = M.span([P*e0]) sage: MP - Ore module of rank 4 over Fraction Field of Univariate Polynomial Ring in t over Finite Field of size 5 twisted by d/dt + Ore module of rank 4 over Univariate Polynomial Ring in t over Finite Field of size 5 twisted by d/dt sage: MP.basis() - [e0 + (t^4+t^2+3)*e4 + t^3*e5, - e1 + (4*t^3+2*t)*e4 + (4*t^2+3)*e5, - e2 + (2*t^2+2)*e4 + 2*t*e5, - e3 + 4*t*e4 + 4*e5] + [t*e0 + t*e1 + e2, + (4*t+1)*e0 + e1 + (t+4)*e2 + e3, + (t+4)*e0 + e1 + 3*e2 + (t+4)*e3 + e4, + (4*t+1)*e0 + 4*e1 + 4*e3 + (t+4)*e4 + e5] When there is only one generator, encapsulating it in a list is not necessary; one can equally write:: sage: MP = M.span(P*e0) + In this case, the module `M P` is already saturated, so computing its + saturation yields the same result:: + + sage: MPsat = M.span(P*e0, saturate=True) + sage: MPsat.basis() + [t*e0 + t*e1 + e2, + (4*t+1)*e0 + e1 + (t+4)*e2 + e3, + (t+4)*e0 + e1 + 3*e2 + (t+4)*e3 + e4, + (4*t+1)*e0 + 4*e1 + 4*e3 + (t+4)*e4 + e5] + sage: MPsat == MP + True + + Of course, it is not always the case:: + + sage: N = M.span(X^5*e0) + sage: N.basis() + [(t^3+4*t^2+4*t)*e0 + (t^2+3*t+3)*e1 + (2*t^2+t+4)*e2 + 3*t^2*e3 + (2*t^2+2*t+2)*e4, + (3*t^2+3*t+4)*e0 + (t^3+4*t^2+t+3)*e1 + (t^2+2*t+4)*e2 + (2*t^2+2*t+4)*e3 + (3*t^2+4*t+2)*e4, + (t+3)*e0 + (t^2+t)*e1 + (t^3+4*t^2+3*t)*e2 + (t^2+t+1)*e3 + (2*t^2+3*t+3)*e4, + e0 + (3*t+4)*e1 + (4*t^2+4*t+3)*e2 + (t^3+4*t^2+1)*e3 + (t^2+4)*e4, + 4*e1 + (t+3)*e2 + (2*t^2+2*t+3)*e3 + (t^3+4*t^2+2*t+1)*e4, + e5] + sage: Nsat = M.span(X^5*e0, saturate=True) + sage: Nsat.basis() + [e0, e1, e2, e3, e4, e5] + If one wants, one can give names to the basis of the submodule using the attribute ``names``:: @@ -1315,7 +1375,7 @@ def span(self, gens, saturate=False, names=None, check=True): [u0, u1] sage: M(u0) - e0 + (t^2+4)*e2 + 3*t^3*e3 + (t^2+1)*e4 + 3*t*e5 + (t^2+t)*e0 + (2*t^2+t+2)*e1 + (t^2+2*t+2)*e2 + 2*t*e3 + e4 Note that a coercion map from the submodule to the ambient module is automatically set:: @@ -1327,7 +1387,7 @@ def span(self, gens, saturate=False, names=None, check=True): expression perfectly works:: sage: t*u0 + e1 - t*e0 + e1 + (t^3+4*t)*e2 + 3*t^4*e3 + (t^3+t)*e4 + 3*t^2*e5 + (t^3+t^2)*e0 + (2*t^3+t^2+2*t+1)*e1 + (t^3+2*t^2+2*t)*e2 + 2*t^2*e3 + t*e4 Here is an example with multiple generators:: @@ -1341,10 +1401,10 @@ def span(self, gens, saturate=False, names=None, check=True): sage: N = MP.span(P^2*e0) sage: N - Ore module of rank 2 over Fraction Field of Univariate Polynomial Ring in t over Finite Field of size 5 twisted by d/dt + Ore module of rank 2 over Univariate Polynomial Ring in t over Finite Field of size 5 twisted by d/dt sage: N.basis() - [e0 + (t^2+4)*e2 + 3*t^3*e3 + (t^2+1)*e4 + 3*t*e5, - e1 + (4*t^2+4)*e3 + 3*t*e4 + 4*e5] + [(t^2+t)*e0 + (2*t^2+t+2)*e1 + (t^2+2*t+2)*e2 + 2*t*e3 + e4, + (3*t^2+1)*e0 + (2*t^2+3*t+2)*e1 + 4*t*e2 + (t^2+3*t+4)*e3 + (2*t+3)*e4 + e5] .. SEEALSO:: @@ -1362,7 +1422,9 @@ def quotient(self, sub, remove_torsion=False, names=None, check=True): INPUT: - - ``gens`` -- a list of vectors or submodules of this Ore module + - ``sub`` -- a list of vectors or submodules of this Ore module + + - ``remove_torsion`` (default: ``False``) -- a boolean - ``names`` (default: ``None``) -- the name of the vectors in a basis of the quotient @@ -1371,9 +1433,9 @@ def quotient(self, sub, remove_torsion=False, names=None, check=True): EXAMPLES:: - sage: K. = Frac(GF(5)['t']) - sage: S. = OrePolynomialRing(K, K.derivation()) - sage: P = X^2 + t*X + 1 + sage: A. = GF(5)['t'] + sage: S. = OrePolynomialRing(A, A.derivation()) + sage: P = X^2 + t*X + t sage: M = S.quotient_module(P^3, names='e') sage: M.inject_variables() Defining e0, e1, e2, e3, e4, e5 @@ -1382,20 +1444,36 @@ def quotient(self, sub, remove_torsion=False, names=None, check=True): sage: modP = M.quotient(P*e0) sage: modP - Ore module of rank 2 over Fraction Field of Univariate Polynomial Ring in t over Finite Field of size 5 twisted by d/dt + Ore module of rank 2 over Univariate Polynomial Ring in t over Finite Field of size 5 twisted by d/dt As a shortcut, we can write ``quo`` instead of ``quotient`` or even use the ``/`` operator:: sage: modP = M / (P*e0) sage: modP - Ore module of rank 2 over Fraction Field of Univariate Polynomial Ring in t over Finite Field of size 5 twisted by d/dt + Ore module of rank 2 over Univariate Polynomial Ring in t over Finite Field of size 5 twisted by d/dt + + In the above example, the quotient is still a free module. + It might happen however that torsion shows up in the quotient. + Currently, torsion Ore modules are not implemented, so attempting to + create a quotient with torsion raises an error:: + + sage: M.quotient(X^5*e0) + Traceback (most recent call last): + ... + NotImplementedError: torsion Ore modules are not implemented + + It is nevertheless always possible to build the free part of the + quotient by passing in the argument ``remove_torsion=True``:: + + sage: M.quotient(X^5*e0, remove_torsion=True) + Ore module of rank 0 over Univariate Polynomial Ring in t over Finite Field of size 5 twisted by d/dt By default, the vectors in the quotient have the same names as their representatives in `M`:: sage: modP.basis() - [e4, e5] + [(t+4)*e0 + (t+3)*e1, (4*t+3)*e0 + t*e2] One can override this behavior by setting the attributes ``names``:: @@ -1410,7 +1488,7 @@ def quotient(self, sub, remove_torsion=False, names=None, check=True): and ``modP`` in the same formula works:: sage: t*u0 + e1 - (t^3+4*t)*u0 + (t^2+2)*u1 + (t^2+2*t+2)*u0 + (t+4)*u1 One can combine the construction of quotients and submodules without trouble. For instance, here we build the space `M P / M P^2`:: @@ -1418,25 +1496,94 @@ def quotient(self, sub, remove_torsion=False, names=None, check=True): sage: modP2 = M / (P^2*e0) sage: N = modP2.span(P*e0) sage: N - Ore module of rank 2 over Fraction Field of Univariate Polynomial Ring in t over Finite Field of size 5 twisted by d/dt + Ore module of rank 2 over Univariate Polynomial Ring in t over Finite Field of size 5 twisted by d/dt sage: N.basis() - [e2 + (2*t^2+2)*e4 + 2*t*e5, - e3 + 4*t*e4 + 4*e5] + [t*e0 + t*e1 + e2, (4*t+1)*e0 + e1 + (t+4)*e2 + e3] .. SEEALSO:: :meth:`quo`, :meth:`span` """ gens = self._span(sub) - check_torsion = not remove_torsion return self._quotientModule_class(self, gens, remove_torsion, names) quo = quotient def ambient_modules(self): + r""" + Return the list of modules in which this module naturally lives. + + EXAMPLES:: + + sage: K. = GF(7^5) + sage: S. = OrePolynomialRing(K, K.frobenius_endomorphism()) + sage: P = X^2 + a + sage: M = S.quotient_module(P^3, names='e') + sage: M + Ore module over Finite Field in a of size 7^5 twisted by a |--> a^7 + sage: M.inject_variables() + Defining e0, e1, e2, e3, e4, e5 + + For an ambient module, the list is reduced to one element (namely + the module itself):: + + sage: M.ambient_modules() + [Ore module over Finite Field in a of size 7^5 twisted by a |--> a^7] + + On the contrary, for a submodule of `M`, the list also contains + the ambient space:: + + sage: MP = M.span(P*e0) + sage: MP.ambient_modules() + [Ore module of rank 4 over Finite Field in a of size 7^5 twisted by a |--> a^7, + Ore module over Finite Field in a of size 7^5 twisted by a |--> a^7] + + If we now create a submodule of `M P`, the list gets even longer:: + + sage: MP2 = MP.span(P^2*e0) + sage: MP2.ambient_modules() + [Ore module of rank 2 over Finite Field in a of size 7^5 twisted by a |--> a^7, + Ore module of rank 4 over Finite Field in a of size 7^5 twisted by a |--> a^7, + Ore module over Finite Field in a of size 7^5 twisted by a |--> a^7] + + We underline nevertheless that if we define `M P^2` has a submodule + of `M`, the intermediate `M P` does not show up in the list:: + + sage: MP2 = M.span(P^2*e0) + sage: MP2.ambient_modules() + [Ore module of rank 2 over Finite Field in a of size 7^5 twisted by a |--> a^7, + Ore module over Finite Field in a of size 7^5 twisted by a |--> a^7] + """ return [self] def _pushout_(self, other): + r""" + Return the smallest module in which ``self`` and ``other`` + are both included (or ``None`` if such a module does not + exist). + + TESTS:: + + sage: A. = GF(3)[] + sage: f = A.hom([t+1]) + sage: S. = OrePolynomialRing(A, f) + sage: P = X^2 + t + sage: M = S.quotient_module(P^3) + sage: e0 = M.gen(0) + + sage: MP = M.span(P*e0) + sage: MP2 = MP.span(P^2*e0) + sage: MPX = MP.span(P*X^3*e0) + sage: MP2._pushout_(MPX) is MP + True + + :: + + sage: MP2 = M.span(P^2*e0) + sage: MPX = M.span(P*X^3*e0) + sage: MP2._pushout_(MPX) is M + True + """ if isinstance(other, OreModule): ambients = self.ambient_modules() for M in other.ambient_modules(): @@ -1444,17 +1591,98 @@ def _pushout_(self, other): return M def is_submodule(self, other): + r""" + Return ``True`` if ``other`` is included in this module; + ``False`` otherwise. + + EXAMPLES:: + + sage: A. = GF(3)[] + sage: f = A.hom([t+1]) + sage: S. = OrePolynomialRing(A, f) + sage: P = X^2 + t + sage: M = S.quotient_module(P^3, names='e') + sage: M.inject_variables() + Defining e0, e1, e2, e3, e4, e5 + sage: MP = M.span(P*e0) + sage: MP2 = MP.span(P^2*e0) + + sage: MP2.is_submodule(MP) + True + sage: MP.is_submodule(MP2) + False + """ M = self._pushout_(other) if M is None: return False return all(M(x) in other for x in self.basis()) def _fitting_index(self): + r""" + Return the generator of the Fitting ideal of the + quotient of the ambient space by this module. + + TESTS:: + + sage: K. = GF(7^5) + sage: S. = OrePolynomialRing(K, K.frobenius_endomorphism()) + sage: M = S.quotient_module(X^2 + a) + sage: M.fitting_index() # indirect doctest + 1 + """ if self is self.ambient_module(): return self.base_ring().one() raise NotImplementedError("Fitting indexes are not implemented for this Ore module") def fitting_index(self, other=None): + r""" + Return the generator of the Fitting ideal of the quotient + of ``other`` by this module. + + INPUT: + + - ``other`` (default: ``None``) -- an Ore module; if ``None``, + the ambient space of this module + + EXAMPLES:: + + sage: A. = GF(3)[] + sage: f = A.hom([t+1]) + sage: S. = OrePolynomialRing(A, f) + sage: P = X^2 + t + sage: M = S.quotient_module(P^2, names='e') + sage: M.inject_variables() + Defining e0, e1, e2, e3 + + We create a submodule and compute its Fitting index:: + + sage: N = M.span(X^3*e0) + sage: N.fitting_index() + t^6 + t^4 + t^2 + + Here is another example where the submodule has smaller rank; + in this case, the Fitting index is `0`:: + + sage: MP = M.span(P*e0) + sage: MP + Ore module of rank 2 over Univariate Polynomial Ring in t over Finite Field of size 3 twisted by t |--> t + 1 + sage: MP.fitting_index() + 0 + + Another example with two submodules of `M`:: + + sage: NP = M.span(X^3*P*e0) + sage: NP.fitting_index() # index in M + 0 + sage: NP.fitting_index(MP) + t^3 + 2*t + + We note that it is actually not necessary that ``other`` contains + ``self``; if it is not the case, a fraction is returned:: + + sage: MP.fitting_index(NP) + 1/(t^3 + 2*t) + """ if other is None: return self._fitting_index() ambients = self.ambient_modules() @@ -1477,6 +1705,49 @@ def fitting_index(self, other=None): return Infinity def covers(self): + r""" + Return the list of modules of which this module is a quotient. + + EXAMPLES:: + + sage: K. = GF(7^5) + sage: S. = OrePolynomialRing(K, K.frobenius_endomorphism()) + sage: P = X^2 + a + sage: M = S.quotient_module(P^3, names='e') + sage: M + Ore module over Finite Field in a of size 7^5 twisted by a |--> a^7 + sage: M.inject_variables() + Defining e0, e1, e2, e3, e4, e5 + + For an ambient module, the list is reduced to one element (namely + the module itself):: + + sage: M.covers() + [Ore module over Finite Field in a of size 7^5 twisted by a |--> a^7] + + We now create a quotient of `M` and observe what happens:: + + sage: MP2 = M.quo(P^2*e0) + sage: MP2.covers() + [Ore module of rank 4 over Finite Field in a of size 7^5 twisted by a |--> a^7, + Ore module over Finite Field in a of size 7^5 twisted by a |--> a^7] + + If we now create a quotient of `M/MP`, another item is added to the list:: + + sage: MP = MP2.quo(P*e0) + sage: MP.covers() + [Ore module of rank 2 over Finite Field in a of size 7^5 twisted by a |--> a^7, + Ore module of rank 4 over Finite Field in a of size 7^5 twisted by a |--> a^7, + Ore module over Finite Field in a of size 7^5 twisted by a |--> a^7] + + We underline nevertheless that if we directly define `M/M P` has a + quotient of `M`, the intermediate `M/M P^2` does not show up in the list:: + + sage: MP = M.quo(P^2*e0) + sage: MP.covers() + [Ore module of rank 4 over Finite Field in a of size 7^5 twisted by a |--> a^7, + Ore module over Finite Field in a of size 7^5 twisted by a |--> a^7] + """ return [self] def __eq__(self, other) -> bool: @@ -1618,6 +1889,23 @@ def __init__(self, ambient, subspace, names) -> None: ambient = ambient.ambient_module() def __reduce__(self): + r""" + Return the necessary arguments to construct this object, + as per the pickle protocol. + + EXAMPLES:: + + sage: K. = GF(5^3) + sage: S. = OrePolynomialRing(K, K.frobenius_endomorphism()) + sage: P = X + z + sage: M = S.quotient_module(P^2, names='e') + sage: M.inject_variables() + Defining e0, e1 + + sage: N = M.span(P*e0) + sage: loads(dumps(N)) is N + True + """ return self._submodule_class, (self._ambient, self._subspace, False, self._names) def _repr_element(self, x) -> str: @@ -1674,6 +1962,50 @@ def ambient_module(self): return self._ambient def ambient_modules(self): + r""" + Return the list of modules in which this module naturally lives. + + EXAMPLES:: + + sage: K. = GF(7^5) + sage: S. = OrePolynomialRing(K, K.frobenius_endomorphism()) + sage: P = X^2 + a + sage: M = S.quotient_module(P^3, names='e') + sage: M + Ore module over Finite Field in a of size 7^5 twisted by a |--> a^7 + sage: M.inject_variables() + Defining e0, e1, e2, e3, e4, e5 + + For an ambient module, the list is reduced to one element (namely + the module itself):: + + sage: M.ambient_modules() + [Ore module over Finite Field in a of size 7^5 twisted by a |--> a^7] + + On the contrary, for a submodule of `M`, the list also contains + the ambient space:: + + sage: MP = M.span(P*e0) + sage: MP.ambient_modules() + [Ore module of rank 4 over Finite Field in a of size 7^5 twisted by a |--> a^7, + Ore module over Finite Field in a of size 7^5 twisted by a |--> a^7] + + If we now create a submodule of `M P`, the list gets even longer:: + + sage: MP2 = MP.span(P^2*e0) + sage: MP2.ambient_modules() + [Ore module of rank 2 over Finite Field in a of size 7^5 twisted by a |--> a^7, + Ore module of rank 4 over Finite Field in a of size 7^5 twisted by a |--> a^7, + Ore module over Finite Field in a of size 7^5 twisted by a |--> a^7] + + We underline nevertheless that if we define `M P^2` has a submodule + of `M`, the intermediate `M P` does not show up in the list:: + + sage: MP2 = M.span(P^2*e0) + sage: MP2.ambient_modules() + [Ore module of rank 2 over Finite Field in a of size 7^5 twisted by a |--> a^7, + Ore module over Finite Field in a of size 7^5 twisted by a |--> a^7] + """ ambients = [self] ambient = self while isinstance(ambient, OreSubmodule): @@ -1682,6 +2014,40 @@ def ambient_modules(self): return ambients def saturate(self, names=None, coerce=False): + r""" + Return the saturation of this module in the ambient module. + + By definition, the saturation of `N` in `M` is the submodule + of `M` consisting of vectors `x` such that `a x \in N` for a + nonzero scalar `a` in the base ring. + + EXAMPLES:: + + sage: A. = GF(3)[] + sage: f = A.hom([t+1]) + sage: S. = OrePolynomialRing(A, f) + sage: P = X^2 + t + sage: M = S.quotient_module(P^2, names='e') + sage: M.inject_variables() + Defining e0, e1, e2, e3 + + We create a submodule, which is not saturated:: + + sage: N = M.span(X^3*P*e0) + sage: N.basis() + [(t^3+2*t^2)*e0 + (t^2+2*t)*e2, (t^2+2*t+1)*e1 + (t+1)*e3] + + and compute its saturation:: + + sage: Nsat = N.saturate() + sage: Nsat.basis() + [t*e0 + e2, (t+1)*e1 + e3] + + One can check that ``Nsat`` is the submodule generated by `M P`:: + + sage: Nsat == M.span(P*e0) + True + """ subspace = self._subspace if subspace.is_saturated: return self.rename_basis(names, coerce) @@ -1788,6 +2154,20 @@ def rename_basis(self, names, coerce=False): return M def _fitting_index(self): + r""" + Return the generator of the Fitting ideal of the + quotient of the ambient space by this module. + + TESTS:: + + sage: A. = GF(3)[] + sage: f = A.hom([t+1]) + sage: S. = OrePolynomialRing(A, f) + sage: M = S.quotient_module(X^2 + t) + sage: N = M.multiplication_map(X^3).image() + sage: N.fitting_index() # indirect doctest + t^3 + 2*t + """ subspace = self._subspace if subspace.rank != self._ambient.rank(): return self.base_ring().zero() @@ -2005,7 +2385,24 @@ def __init__(self, cover, subspace, names) -> None: cover.register_conversion(section) def __reduce__(self): - return self._quotient_class, (self._cover, self._subspace, False, self._names) + r""" + Return the necessary arguments to construct this object, + as per the pickle protocol. + + EXAMPLES:: + + sage: K. = GF(5^3) + sage: S. = OrePolynomialRing(K, K.frobenius_endomorphism()) + sage: P = X + z + sage: M = S.quotient_module(P^2, names='e') + sage: M.inject_variables() + Defining e0, e1 + + sage: N = M.quo(P*e0) + sage: loads(dumps(N)) is N + True + """ + return self._quotientModule_class, (self._cover, self._subspace, False, self._names) def _repr_element(self, x) -> str: r""" @@ -2068,6 +2465,49 @@ def cover(self): return self._cover def covers(self): + r""" + Return the list of modules of which this module is a quotient. + + EXAMPLES:: + + sage: K. = GF(7^5) + sage: S. = OrePolynomialRing(K, K.frobenius_endomorphism()) + sage: P = X^2 + a + sage: M = S.quotient_module(P^3, names='e') + sage: M + Ore module over Finite Field in a of size 7^5 twisted by a |--> a^7 + sage: M.inject_variables() + Defining e0, e1, e2, e3, e4, e5 + + For an ambient module, the list is reduced to one element (namely + the module itself):: + + sage: M.covers() + [Ore module over Finite Field in a of size 7^5 twisted by a |--> a^7] + + We now create a quotient of `M` and observe what happens:: + + sage: MP2 = M.quo(P^2*e0) + sage: MP2.covers() + [Ore module of rank 4 over Finite Field in a of size 7^5 twisted by a |--> a^7, + Ore module over Finite Field in a of size 7^5 twisted by a |--> a^7] + + If we now create a quotient of `M/MP`, another item is added to the list:: + + sage: MP = MP2.quo(P*e0) + sage: MP.covers() + [Ore module of rank 2 over Finite Field in a of size 7^5 twisted by a |--> a^7, + Ore module of rank 4 over Finite Field in a of size 7^5 twisted by a |--> a^7, + Ore module over Finite Field in a of size 7^5 twisted by a |--> a^7] + + We underline nevertheless that if we directly define `M/M P` has a + quotient of `M`, the intermediate `M/M P^2` does not show up in the list:: + + sage: MP = M.quo(P^2*e0) + sage: MP.covers() + [Ore module of rank 4 over Finite Field in a of size 7^5 twisted by a |--> a^7, + Ore module over Finite Field in a of size 7^5 twisted by a |--> a^7] + """ covers = [self] cover = self while isinstance(cover, OreQuotientModule): From e21b12f1cbce6419f287a2ebb2a650f1845080ef Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sat, 2 Aug 2025 06:14:04 +0200 Subject: [PATCH 5/7] subspace -> submodule --- src/sage/modules/meson.build | 1 + src/sage/modules/ore_module.py | 82 +++++++-------- src/sage/modules/ore_module_morphism.py | 10 +- src/sage/modules/subspace_helper.py | 126 ------------------------ 4 files changed, 47 insertions(+), 172 deletions(-) delete mode 100644 src/sage/modules/subspace_helper.py diff --git a/src/sage/modules/meson.build b/src/sage/modules/meson.build index c9591ad82da..4396b462f9a 100644 --- a/src/sage/modules/meson.build +++ b/src/sage/modules/meson.build @@ -47,6 +47,7 @@ py.install_sources( 'vector_space_morphism.py', 'vector_symbolic_dense.py', 'vector_symbolic_sparse.py', + 'submodule_helper.py', subdir: 'sage/modules', ) diff --git a/src/sage/modules/ore_module.py b/src/sage/modules/ore_module.py index d61091edd50..22ff8f9a761 100644 --- a/src/sage/modules/ore_module.py +++ b/src/sage/modules/ore_module.py @@ -197,7 +197,7 @@ from sage.rings.polynomial.ore_polynomial_element import OrePolynomial from sage.modules.free_module import FreeModule_ambient from sage.modules.free_module_element import FreeModuleElement_generic_dense -from sage.modules.subspace_helper import SubspaceHelper +from sage.modules.submodule_helper import SubmoduleHelper from sage.modules.ore_module_element import OreModuleElement # Action by left multiplication on Ore modules @@ -1058,7 +1058,7 @@ def hom(self, im_gens, codomain=None): Finally ``im_gens`` can also be itself a Ore morphism, in which case SageMath tries to cast it into a morphism with the requested domains and codomains. - As an example below, we restrict `g` to a subspace:: + As an example below, we restrict `g` to a submodule:: sage: C. = U.span((X + t)*u0) sage: gC = C.hom(g) @@ -1830,21 +1830,21 @@ def __classcall_private__(cls, ambient, gens, saturate, names): sage: M.span((X + x + y)*v) Traceback (most recent call last): ... - NotImplementedError: subspaces and quotients are only implemented over PID + NotImplementedError: submodules and quotients are only implemented over PID """ base = ambient.base_ring() - if isinstance(gens, SubspaceHelper): + if isinstance(gens, SubmoduleHelper): if not saturate or gens.is_saturated: - subspace = gens + submodule = gens else: - subspace = SubspaceHelper(gens.basis, saturate) + submodule = SubmoduleHelper(gens.basis, saturate) else: basis = matrix(base, gens) - subspace = SubspaceHelper(basis, saturate) - names = normalize_names(names, subspace.rank) - return cls.__classcall__(cls, ambient, subspace, names) + submodule = SubmoduleHelper(basis, saturate) + names = normalize_names(names, submodule.rank) + return cls.__classcall__(cls, ambient, submodule, names) - def __init__(self, ambient, subspace, names) -> None: + def __init__(self, ambient, submodule, names) -> None: r""" Initialize this Ore submodule. @@ -1872,13 +1872,13 @@ def __init__(self, ambient, subspace, names) -> None: from sage.modules.ore_module_morphism import OreModuleRetraction base = ambient.base_ring() self._ambient = ambient - self._subspace = subspace - C = subspace.coordinates.matrix_from_columns(range(subspace.rank)) - rows = [ambient(x).image() * C for x in subspace.basis.rows()] + self._submodule = submodule + C = submodule.coordinates.matrix_from_columns(range(submodule.rank)) + rows = [ambient(x).image() * C for x in submodule.basis.rows()] OreModule.__init__(self, matrix(base, rows), ambient.ore_ring(action=False), names, ambient._ore_category) - coerce = self.hom(subspace.basis, codomain=ambient) + coerce = self.hom(submodule.basis, codomain=ambient) ambient.register_coercion(coerce) self._inject = coerce.__copy__() retract = self._retract = OreModuleRetraction(ambient, self) @@ -1906,7 +1906,7 @@ def __reduce__(self): sage: loads(dumps(N)) is N True """ - return self._submodule_class, (self._ambient, self._subspace, False, self._names) + return self._submodule_class, (self._ambient, self._submodule, False, self._names) def _repr_element(self, x) -> str: r""" @@ -2048,10 +2048,10 @@ def saturate(self, names=None, coerce=False): sage: Nsat == M.span(P*e0) True """ - subspace = self._subspace - if subspace.is_saturated: + submodule = self._submodule + if submodule.is_saturated: return self.rename_basis(names, coerce) - S = self._submodule_class(self._ambient, subspace, True, names) + S = self._submodule_class(self._ambient, submodule, True, names) if coerce: M = self._ambient base = self.base_ring() @@ -2145,7 +2145,7 @@ def rename_basis(self, names, coerce=False): rank = self.rank() names = normalize_names(names, rank) cls = self.__class__ - M = cls.__classcall__(cls, self._ambient, self._subspace, names) + M = cls.__classcall__(cls, self._ambient, self._submodule, names) if coerce: mat = identity_matrix(self.base_ring(), rank) id = self.hom(mat, codomain=M) @@ -2168,11 +2168,11 @@ def _fitting_index(self): sage: N.fitting_index() # indirect doctest t^3 + 2*t """ - subspace = self._subspace - if subspace.rank != self._ambient.rank(): + submodule = self._submodule + if submodule.rank != self._ambient.rank(): return self.base_ring().zero() else: - return subspace.basis.determinant() + return submodule.basis.determinant() def injection_morphism(self): r""" @@ -2270,7 +2270,7 @@ def morphism_corestriction(self, f): if f.codomain() is not self._ambient: raise ValueError("the codomain of the morphism must be the ambient space") rows = [] - C = self._subspace.coordinates + C = self._submodule.coordinates try: im_gens = [self(f(x)) for x in f.domain().basis()] except ValueError: @@ -2322,23 +2322,23 @@ def __classcall_private__(cls, cover, gens, remove_torsion, names): sage: M.quo((X + x + y)*v) Traceback (most recent call last): ... - NotImplementedError: subspaces and quotients are only implemented over PID + NotImplementedError: submodules and quotients are only implemented over PID """ base = cover.base_ring() - if isinstance(gens, SubspaceHelper): + if isinstance(gens, SubmoduleHelper): if not remove_torsion or gens.is_saturated: - subspace = gens + submodule = gens else: - subspace = SubspaceHelper(gens.basis, remove_torsion) + submodule = SubmoduleHelper(gens.basis, remove_torsion) else: basis = matrix(base, gens) - subspace = SubspaceHelper(basis, remove_torsion) - if not subspace.is_saturated: + submodule = SubmoduleHelper(basis, remove_torsion) + if not submodule.is_saturated: raise NotImplementedError("torsion Ore modules are not implemented") - names = normalize_names(names, cover.rank() - subspace.rank) - return cls.__classcall__(cls, cover, subspace, names) + names = normalize_names(names, cover.rank() - submodule.rank) + return cls.__classcall__(cls, cover, submodule, names) - def __init__(self, cover, subspace, names) -> None: + def __init__(self, cover, submodule, names) -> None: r""" Initialize this Ore quotient. @@ -2368,10 +2368,10 @@ def __init__(self, cover, subspace, names) -> None: self._cover = cover d = cover.rank() base = cover.base_ring() - self._subspace = subspace - rank = subspace.rank - coerce = subspace.coordinates.matrix_from_columns(range(rank, d)) - images = [cover(x).image() for x in subspace.complement.rows()] + self._submodule = submodule + rank = submodule.rank + coerce = submodule.coordinates.matrix_from_columns(range(rank, d)) + images = [cover(x).image() for x in submodule.complement.rows()] OreModule.__init__(self, matrix(base, d-rank, d, images) * coerce, cover.ore_ring(action=False), names, cover._ore_category) @@ -2402,7 +2402,7 @@ def __reduce__(self): sage: loads(dumps(N)) is N True """ - return self._quotientModule_class, (self._cover, self._subspace, False, self._names) + return self._quotientModule_class, (self._cover, self._submodule, False, self._names) def _repr_element(self, x) -> str: r""" @@ -2552,7 +2552,7 @@ def relations(self, names=None): :meth:`relations` """ - return self._submodule_class(self._cover, self._subspace, False, names) + return self._submodule_class(self._cover, self._submodule, False, names) def rename_basis(self, names, coerce=False): r""" @@ -2638,7 +2638,7 @@ def rename_basis(self, names, coerce=False): rank = self.rank() names = normalize_names(names, rank) cls = self.__class__ - M = cls.__classcall__(cls, self._cover, self._subspace, names) + M = cls.__classcall__(cls, self._cover, self._submodule, names) if coerce: mat = identity_matrix(self.base_ring(), rank) id = self.hom(mat, codomain=M) @@ -2704,10 +2704,10 @@ def morphism_quotient(self, f): """ if f.domain() is not self._cover: raise ValueError("the domain of the morphism must be the cover ring") - Z = self._subspace.basis * f._matrix + Z = self._submodule.basis * f._matrix if not Z.is_zero(): raise ValueError("the morphism does not factor through this quotient") - mat = self._subspace.complement * f._matrix + mat = self._submodule.complement * f._matrix return self.hom(mat, codomain=f.codomain()) def morphism_modulo(self, f): diff --git a/src/sage/modules/ore_module_morphism.py b/src/sage/modules/ore_module_morphism.py index a7c904fd178..c032917d238 100644 --- a/src/sage/modules/ore_module_morphism.py +++ b/src/sage/modules/ore_module_morphism.py @@ -638,7 +638,7 @@ def is_surjective(self) -> bool: sage: g.is_surjective() True """ - return self.image()._subspace.basis.is_one() + return self.image()._submodule.basis.is_one() def is_bijective(self) -> bool: r""" @@ -938,9 +938,9 @@ def _call_(self, y): ValueError: not in the submodule """ X = self.codomain() - subspace = X._subspace - rank = subspace.rank - xs = y * subspace.coordinates + submodule = X._submodule + rank = submodule.rank + xs = y * submodule.coordinates if xs[rank:]: raise ValueError("not in the submodule") try: @@ -973,4 +973,4 @@ def _call_(self, y): """ X = self.codomain() Y = self.domain() - return X(y * Y._subspace.complement) + return X(y * Y._submodule.complement) diff --git a/src/sage/modules/subspace_helper.py b/src/sage/modules/subspace_helper.py deleted file mode 100644 index 4cc5bd83bf1..00000000000 --- a/src/sage/modules/subspace_helper.py +++ /dev/null @@ -1,126 +0,0 @@ -from sage.misc.classcall_metaclass import ClasscallMetaclass -from sage.misc.lazy_attribute import lazy_attribute -from sage.categories.fields import Fields -from sage.categories.principal_ideal_domains import PrincipalIdealDomains -from sage.matrix.matrix_polynomial_dense import Matrix_polynomial_dense -from sage.matrix.constructor import matrix - - -class SubspaceHelper(metaclass=ClasscallMetaclass): - def __classcall_private__(self, mat, saturate=False): - base = mat.base_ring() - if base in Fields(): - cls = SubspaceHelper_field - elif (isinstance(mat, Matrix_polynomial_dense) - and base.base_ring() in Fields()): - cls = SubspaceHelper_polynomial_ring - elif base in PrincipalIdealDomains(): - cls = SubspaceHelper_PID - else: - raise NotImplementedError("subspaces and quotients are only implemented over PID") - return cls.__call__(mat, saturate) - - def __hash__(self): - return hash(self.basis) - - def __eq__(self, other): - return self.basis == other.basis - - def __repr__(self): - return self.basis.__repr__() - - -class SubspaceHelper_field(SubspaceHelper): - def __init__(self, mat, saturate): - base = mat.base_ring() - n = mat.ncols() - basis = mat.echelon_form() - self.rank = r = basis.rank() - pivots = basis.pivots() - self.basis = basis.matrix_from_rows(range(r)) - self.basis.set_immutable() - self.complement = matrix(base, n-r, n) - self.coordinates = matrix(base, n, n) - indices = [] - i = 0 - for j in range(n): - if i < r and pivots[i] == j: - self.coordinates[j, i] = base.one() - i += 1 - else: - indices.append(j) - self.complement[j-i, j] = base.one() - self.coordinates[j, j-i+r] = base.one() - for i in range(r): - for j in range(n-r): - self.coordinates[pivots[i], j+r] = -basis[i, indices[j]] - self.is_saturated = True - - -class SubspaceHelper_PID(SubspaceHelper): - def __init__(self, mat, saturate): - base = mat.base_ring() - n = mat.ncols() - S, U, V = mat.smith_form() - r = 0 - for i in range(min(S.nrows(), S.ncols())): - if S[i,i] == 0: - break - r += 1 - self.rank = r - W = V.inverse().change_ring(base) - if saturate: - self.basis = W.matrix_from_rows(range(r)) - self.complement = W.matrix_from_rows(range(r, n)) - self.coordinates = V - self.is_saturated = True - else: - S = S.matrix_from_rows(range(r)) - self.basis = matrix(base, [[S[i,i]*W[i,j] for j in range(n)] - for i in range(r)]) - self.complement = W.matrix_from_rows(range(r, n)) - K = base.fraction_field() - scalars = [~S[i,i] for i in range(r)] + (n-r)*[K.one()] - self.coordinates = matrix(K, [[scalars[j]*V[i,j] for j in range(n)] - for i in range(n)]) - self.is_saturated = all(S[i,i].is_unit() for i in range(r)) - self.basis.set_immutable() - - -class SubspaceHelper_polynomial_ring(SubspaceHelper): - def __init__(self, mat, saturate): - base = mat.base_ring() - if saturate: - S, _, V = mat.smith_form() - W = V.inverse().change_ring(base) - r = 0 - for i in range(min(S.nrows(), S.ncols())): - if S[i,i] == 0: - break - r += 1 - mat = W.matrix_from_rows(range(r)) - self.is_saturated = True - self.basis = mat.popov_form(include_zero_vectors=False) - self.basis.set_immutable() - self.rank = self.basis.nrows() - - @lazy_attribute - def _popov(self): - return self.basis.stack(self.complement).popov_form(transformation=True) - - @lazy_attribute - def complement(self): - return self.basis.basis_completion().popov_form() - - @lazy_attribute - def coordinates(self): - P, T = self._popov - if P.is_one(): - return T - else: - return self.basis.stack(self.complement).inverse() - - @lazy_attribute - def is_saturated(self): - P, _ = self._popov - return P.is_one() From 0d7ec94b9473785dba6fb3925546eb19edd19694 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sat, 2 Aug 2025 07:21:02 +0200 Subject: [PATCH 6/7] add file submodule_helper.py --- src/sage/modules/ore_module.py | 2 + src/sage/modules/submodule_helper.py | 397 +++++++++++++++++++++++++++ 2 files changed, 399 insertions(+) create mode 100644 src/sage/modules/submodule_helper.py diff --git a/src/sage/modules/ore_module.py b/src/sage/modules/ore_module.py index 22ff8f9a761..daa06950111 100644 --- a/src/sage/modules/ore_module.py +++ b/src/sage/modules/ore_module.py @@ -167,6 +167,8 @@ AUTHOR: - Xavier Caruso (2024-10) + +- Xavier Caruso (2025-08); add support for Ore modules over PID """ # *************************************************************************** diff --git a/src/sage/modules/submodule_helper.py b/src/sage/modules/submodule_helper.py new file mode 100644 index 00000000000..0bca7cf4151 --- /dev/null +++ b/src/sage/modules/submodule_helper.py @@ -0,0 +1,397 @@ +r""" +Helper classes for handling submodules over various base rings. + +Currently, it is only used for Ore modules. + +AUTHOR: + +- Xavier Caruso (2025-08) +""" + +# *************************************************************************** +# Copyright (C) 2025 Xavier Caruso +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# https://www.gnu.org/licenses/ +# *************************************************************************** + +from sage.misc.classcall_metaclass import ClasscallMetaclass +from sage.misc.lazy_attribute import lazy_attribute +from sage.categories.fields import Fields +from sage.categories.principal_ideal_domains import PrincipalIdealDomains +from sage.matrix.matrix_polynomial_dense import Matrix_polynomial_dense +from sage.matrix.constructor import matrix + + +class SubmoduleHelper(metaclass=ClasscallMetaclass): + r""" + A class for manipulating submodules at the level of matrices. + The class provides the arguments: + + - ``basis``: a matrix in normal form whose rows form a + basis of the submodule + + - ``complement``: a matrix in normal form whose rows form + a basis of a complement of the submodule + + - ``coordinates``: a change-of-basis matrix from the canonical + basis to the basis obtained by concatening ``basis`` and + ``complement`` + + - ``is_saturated``: a boolean; whether this submodule is + saturated in the ambient space + """ + def __classcall_private__(self, mat, saturate=False): + r""" + Dispatch to the appropriate class. + + TESTS:: + + sage: from sage.modules.submodule_helper import SubmoduleHelper + sage: Fq = GF(5) + sage: SH = SubmoduleHelper(matrix(Fq, [[1, 3]])) + sage: type(SH) + + + :: + + sage: A. = Fq[] + sage: SH = SubmoduleHelper(matrix(A, [[t, 3 + t]])) + sage: type(SH) + + + :: + + sage: SH = SubmoduleHelper(matrix(ZZ, [[1, 3]])) + sage: type(SH) + + + :: + + sage: R. = Fq[] + sage: SH = SubmoduleHelper(matrix(R, [[x, 3 + y]])) + Traceback (most recent call last): + ... + NotImplementedError: submodules and quotients are only implemented over PID + """ + base = mat.base_ring() + if base in Fields(): + cls = SubmoduleHelper_field + elif (isinstance(mat, Matrix_polynomial_dense) + and base.base_ring() in Fields()): + cls = SubmoduleHelper_polynomial_ring + elif base in PrincipalIdealDomains(): + cls = SubmoduleHelper_PID + else: + raise NotImplementedError("submodules and quotients are only implemented over PID") + return cls.__call__(mat, saturate) + + def __hash__(self): + r""" + Return a hash of this submodule. + + Two differents instances corresponding to the same submodule + have identical hashes. + + EXAMPLES:: + + sage: from sage.modules.submodule_helper import SubmoduleHelper + sage: Fq = GF(5) + sage: SH1 = SubmoduleHelper(matrix(Fq, [[1, 3]])) + sage: h1 = hash(SH1) + sage: h1 # random + -8066549207035083627 + + :: + + sage: Fq = GF(5) + sage: SH2 = SubmoduleHelper(matrix(Fq, [[2, 6]])) + sage: h2 = hash(SH2) + sage: h2 # random + -8066549207035083627 + sage: h1 == h2 + True + """ + return hash(self.basis) + + def __eq__(self, other): + r""" + Return ``True`` is ``self`` and ``other`` define the + same submodule. + + EXAMPLES:: + + sage: from sage.modules.submodule_helper import SubmoduleHelper + sage: Fq = GF(5) + sage: SH1 = SubmoduleHelper(matrix(Fq, [[1, 3]])) + sage: SH2 = SubmoduleHelper(matrix(Fq, [[2, 6]])) + sage: SH1 == SH2 + True + """ + return self.basis == other.basis + + +class SubmoduleHelper_field(SubmoduleHelper): + r""" + Submodules over fields. + """ + def __init__(self, mat, saturate): + r""" + Initialize this submodule. + + INPUT: + + - ``mat`` -- a matrix whose rows span the submodule + + - ``saturate`` -- a boolean, ignored + + EXAMPLES:: + + sage: from sage.modules.submodule_helper import SubmoduleHelper + sage: Fq = GF(5) + sage: SH1 = SubmoduleHelper(matrix(Fq, [[1, 2, 3], [1, 3, 4]])) + sage: SH1.basis + [1 0 1] + [0 1 1] + sage: SH1.complement + [0 0 1] + sage: SH1.coordinates + [1 0 4] + [0 1 4] + [0 0 1] + + We check that the previous outputs are coherent:: + + sage: SH1.basis.stack(SH1.complement) * SH1.coordinates == 1 + True + """ + base = mat.base_ring() + n = mat.ncols() + basis = mat.echelon_form() + self.rank = r = basis.rank() + pivots = basis.pivots() + self.basis = basis.matrix_from_rows(range(r)) + self.basis.set_immutable() + self.complement = matrix(base, n-r, n) + self.coordinates = matrix(base, n, n) + indices = [] + i = 0 + for j in range(n): + if i < r and pivots[i] == j: + self.coordinates[j, i] = base.one() + i += 1 + else: + indices.append(j) + self.complement[j-i, j] = base.one() + self.coordinates[j, j-i+r] = base.one() + for i in range(r): + for j in range(n-r): + self.coordinates[pivots[i], j+r] = -basis[i, indices[j]] + self.is_saturated = True + + +class SubmoduleHelper_PID(SubmoduleHelper): + r""" + Submodules over principal ideal domains (except + polynomial rings to which a special class is dedicated). + """ + def __init__(self, mat, saturate): + r""" + Initialize this submodule. + + INPUT: + + - ``mat`` -- a matrix whose rows span the submodule + + - ``saturate`` -- a boolean + + EXAMPLES:: + + sage: from sage.modules.submodule_helper import SubmoduleHelper + sage: SH = SubmoduleHelper(matrix(ZZ, [[1, 2, 3], [1, 3, 4]])) + sage: SH.basis + [1 0 1] + [0 1 1] + sage: SH.complement + [1 0 0] + sage: SH.coordinates + [ 0 0 1] + [-1 1 1] + [ 1 0 -1] + + We check that the previous outputs are coherent:: + + sage: SH.basis.stack(SH.complement) * SH.coordinates == 1 + True + + When ``saturate=True``, the saturation of the span of the given + matrix is created:: + + sage: SH1 = SubmoduleHelper(matrix(ZZ, [[1, 2, 3], [2, 6, 8]])) + sage: SH1.basis + [1 0 1] + [0 2 2] + sage: SH2 = SubmoduleHelper(matrix(ZZ, [[1, 2, 3], [2, 6, 8]]), True) + sage: SH2.basis + [1 0 1] + [0 1 1] + """ + base = mat.base_ring() + n = mat.ncols() + S, U, V = mat.smith_form() + r = 0 + for i in range(min(S.nrows(), S.ncols())): + if S[i,i] == 0: + break + r += 1 + self.rank = r + W = V.inverse().change_ring(base) + if saturate: + basis = W.matrix_from_rows(range(r)) + complement = W.matrix_from_rows(range(r, n)) + self.is_saturated = True + else: + S = S.matrix_from_rows(range(r)) + basis = matrix(base, [[S[i,i]*W[i,j] for j in range(n)] + for i in range(r)]) + complement = W.matrix_from_rows(range(r, n)) + self.is_saturated = all(S[i,i].is_unit() for i in range(r)) + self.basis = basis.echelon_form() + self.basis.set_immutable() + self.complement = complement.echelon_form() + self.coordinates = self.basis.stack(self.complement).inverse() + +class SubmoduleHelper_polynomial_ring(SubmoduleHelper): + r""" + Submodules over polynomial rings. + """ + def __init__(self, mat, saturate): + r""" + Initialize this submodule. + + INPUT: + + - ``mat`` -- a matrix whose rows span the submodule + + - ``saturate`` -- a boolean + + EXAMPLES:: + + sage: from sage.modules.submodule_helper import SubmoduleHelper + sage: A. = GF(5)[] + sage: SH = SubmoduleHelper(matrix(A, [[t, t^2, t^3], [t, t+1, t+2]])) + sage: SH.basis + [ t^3 + 3*t^2 + 3*t t^3 + 3*t^2 + 2*t + 4 3] + [ t t + 1 t + 2] + + When ``saturate=True``, the saturation of the span of the given + matrix is created:: + + sage: SHsat = SubmoduleHelper(matrix(A, [[t, t^2, t^3], [t, t+1, t+2]]), True) + sage: SHsat.basis + [t^2 + 3*t + 4 t^2 + 3*t + 3 1] + [ t t + 1 t + 2] + """ + base = mat.base_ring() + if saturate: + S, _, V = mat.smith_form() + W = V.inverse().change_ring(base) + r = 0 + for i in range(min(S.nrows(), S.ncols())): + if S[i,i] == 0: + break + r += 1 + mat = W.matrix_from_rows(range(r)) + self.is_saturated = True + self.basis = mat.popov_form(include_zero_vectors=False) + self.basis.set_immutable() + self.rank = self.basis.nrows() + + @lazy_attribute + def _popov(self): + r""" + Return a Popov form and the corresponding transformation matrix + of the matrix whose rows are the concatenation of the rows of + ``self.basis`` and ``self.complement``. + + EXAMPLES:: + + sage: from sage.modules.submodule_helper import SubmoduleHelper + sage: A. = GF(5)[] + sage: SH = SubmoduleHelper(matrix(A, [[1, t, t^2], [t, t+1, t+2]])) + sage: SH._popov + ( + [1 0 0] [2*t^3 + 4*t^2 + 4*t + 4 2*t^3 + 4*t^2 + 4*t + 3 3*t^3 + 4*t] + [0 1 0] [ 3*t^3 + t^2 + 4*t + 1 3*t^3 + t^2 + 4*t + 2 2*t^3 + 3*t + 1] + [0 0 1], [ 2*t^2 + 2*t + 2 2*t^2 + 2*t + 2 3*t^2 + 2*t + 2] + ) + """ + return self.basis.stack(self.complement).popov_form(transformation=True) + + @lazy_attribute + def complement(self): + r""" + Return a basis of a complement submodule of this submodule. + + EXAMPLES:: + + sage: from sage.modules.submodule_helper import SubmoduleHelper + sage: A. = GF(5)[] + sage: SH = SubmoduleHelper(matrix(A, [[1, t, t^2], [t, t+1, t+2]])) + sage: SH.complement + [t^2 + t + 1 t^2 + t + 1 t] + """ + return self.basis.basis_completion().popov_form() + + @lazy_attribute + def coordinates(self): + r""" + Return a change-of-basis matrix from the canonical basis + to the basis obtained by concatening ``self.basis`` and + ``self.complement``. + + EXAMPLES:: + + sage: from sage.modules.submodule_helper import SubmoduleHelper + sage: A. = GF(5)[] + sage: SH = SubmoduleHelper(matrix(A, [[1, t, t^2], [t, t+1, t+2]])) + sage: SH.coordinates + [2*t^3 + 4*t^2 + 4*t + 4 2*t^3 + 4*t^2 + 4*t + 3 3*t^3 + 4*t] + [ 3*t^3 + t^2 + 4*t + 1 3*t^3 + t^2 + 4*t + 2 2*t^3 + 3*t + 1] + [ 2*t^2 + 2*t + 2 2*t^2 + 2*t + 2 3*t^2 + 2*t + 2] + + :: + + sage: SH.basis.stack(SH.complement) * SH.coordinates == 1 + True + """ + P, T = self._popov + if P.is_one(): + return T + else: + return self.basis.stack(self.complement).inverse() + + @lazy_attribute + def is_saturated(self): + r""" + Return ``True`` if this submodule is saturated; ``False`` otherwise. + + EXAMPLES:: + + sage: from sage.modules.submodule_helper import SubmoduleHelper + sage: A. = GF(5)[] + sage: SH = SubmoduleHelper(matrix(A, [[1, t, t^2], [t, t+1, t+2]])) + sage: SH.is_saturated + True + + :: + + sage: SH = SubmoduleHelper(matrix(A, [[t, t^2, t^3], [t, t+1, t+2]])) + sage: SH.is_saturated + False + """ + P, _ = self._popov + return P.is_one() From 9861ef9eb19287ba10cc5fb037b3a109f507ccf3 Mon Sep 17 00:00:00 2001 From: Xavier Caruso Date: Sat, 2 Aug 2025 08:13:06 +0200 Subject: [PATCH 7/7] fix lint --- src/sage/modules/ore_module.py | 1 + src/sage/modules/submodule_helper.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/sage/modules/ore_module.py b/src/sage/modules/ore_module.py index daa06950111..cd1beb06979 100644 --- a/src/sage/modules/ore_module.py +++ b/src/sage/modules/ore_module.py @@ -261,6 +261,7 @@ def _act_(self, P, x): ans += y._rmul_(P[i]) return ans + # Generic class for Ore modules ############################### diff --git a/src/sage/modules/submodule_helper.py b/src/sage/modules/submodule_helper.py index 0bca7cf4151..ecfe337adad 100644 --- a/src/sage/modules/submodule_helper.py +++ b/src/sage/modules/submodule_helper.py @@ -264,6 +264,7 @@ def __init__(self, mat, saturate): self.complement = complement.echelon_form() self.coordinates = self.basis.stack(self.complement).inverse() + class SubmoduleHelper_polynomial_ring(SubmoduleHelper): r""" Submodules over polynomial rings.