diff --git a/src/doc/en/reference/modules/index.rst b/src/doc/en/reference/modules/index.rst index d5ff3a70986..fc5c39dba8f 100644 --- a/src/doc/en/reference/modules/index.rst +++ b/src/doc/en/reference/modules/index.rst @@ -57,4 +57,19 @@ Modules sage/modules/multi_filtered_vector_space sage/modules/tensor_operations +Finitely presented graded modules +--------------------------------- + +.. toctree:: + :maxdepth: 2 + + sage/modules/fp_graded/free_module + sage/modules/fp_graded/free_element + sage/modules/fp_graded/free_morphism + sage/modules/fp_graded/free_homspace + sage/modules/fp_graded/module + sage/modules/fp_graded/element + sage/modules/fp_graded/morphism + sage/modules/fp_graded/homspace + .. include:: ../footer.txt diff --git a/src/sage/modules/fp_graded/__init__.py b/src/sage/modules/fp_graded/__init__.py new file mode 100755 index 00000000000..2e5420c89a1 --- /dev/null +++ b/src/sage/modules/fp_graded/__init__.py @@ -0,0 +1,12 @@ +""" +Finitely presented graded left modules over graded connected algebras +""" + + +# TODO: +# +# 1. Why do we need to define __bool__ and __eq__ in element.py? They should be taken care of automatically, once we define __nonzero__. +# 2. inhheritance: free modules, elements, etc., should perhaps inherit from fp versions, or maybe both should inherit from generic classes, to consolidate some methods (like degree, _repr_, others?) +# 3. Test with graded modules over other graded rings. (Should be okay, but add some doctests.) +# +# In __classcall__/__init__ for FP_Modules, allow as input a free module or a morphism of free modules? diff --git a/src/sage/modules/fp_graded/element.py b/src/sage/modules/fp_graded/element.py new file mode 100755 index 00000000000..c7f3b24efe5 --- /dev/null +++ b/src/sage/modules/fp_graded/element.py @@ -0,0 +1,322 @@ +r""" +Elements of finitely presented graded modules + +This class implements construction and basic manipulation of elements of the +Sage parent :class:`sage.modules.fp_graded.module.FP_Module`, which models +finitely presented modules over connected graded algebras. + +AUTHORS: + +- Robert R. Bruner, Michael J. Catanzaro (2012): Initial version. +- Sverre Lunoee--Nielsen and Koen van Woerden (2019-11-29): Updated the + original software to Sage version 8.9. +- Sverre Lunoee--Nielsen (2020-07-01): Refactored the code and added + new documentation and tests. +""" + +#***************************************************************************** +# Copyright (C) 2011 Robert R. Bruner and +# Michael J. Catanzaro +# +# 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.cachefunc import cached_method +from sage.modules.with_basis.indexed_element import IndexedFreeModuleElement + +from .free_element import FreeGradedModuleElement + + +class FP_Element(IndexedFreeModuleElement): + r""" + A module element of a finitely presented graded module over + a connected graded algebra. + + TESTS: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: FP_Module(SteenrodAlgebra(2), [0])([Sq(2)]) + + """ + def free_element(self): + r""" + A lift of this element to the free module F, + if this element is in a quotient of F. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: M = FP_Module(SteenrodAlgebra(2), [0,1], [[Sq(4), Sq(3)]]) + sage: x = M([Sq(1), 1]) + sage: x + + sage: x.parent() + Finitely presented left module on 2 generators and 1 relation over mod 2 Steenrod algebra, milnor basis + sage: x.free_element() + + sage: x.free_element().parent() + Finitely presented free left module on 2 generators over mod 2 Steenrod algebra, milnor basis + """ + C = self.parent().j.codomain() + return C(self.dense_coefficient_list()) + + + @cached_method + def degree(self): + r""" + The degree of this element. + + OUTPUT: The integer degree of this element, or ``None`` if this is the + zero element. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: M = FP_Module(SteenrodAlgebra(2), [0,1], [[Sq(4), Sq(3)]]) + sage: x = M.an_element(7) + + sage: x + + sage: x.degree() + 7 + + The zero element has no degree:: + + sage: (x-x).degree() + Traceback (most recent call last): + ... + ValueError: the zero element does not have a well-defined degree + + TESTS: + + sage: N = FP_Module(SteenrodAlgebra(2), [0], [[Sq(2)]]) + sage: y = Sq(2)*N.generator(0) + sage: y == 0 + True + sage: y.degree() + Traceback (most recent call last): + ... + ValueError: the zero element does not have a well-defined degree + """ + if self.is_zero(): + raise ValueError("the zero element does not have a well-defined degree") + return self.free_element().degree() + + + def _repr_(self): + r""" + Return a string representation of this element. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: M = FP_Module(SteenrodAlgebra(2), [0,1], [[Sq(4), Sq(3)]]) + sage: [M.an_element(n) for n in range(1,10)] + [, + , + , + , + , + , + , + , + ] + """ + return self.free_element()._repr_() + + + def _lmul_(self, a): + r""" + Act by left multiplication on this element by ``a``. + + INPUT: + + - ``a`` -- an element of the algebra this module is defined over. + + OUTPUT: the module element `a\cdot x` where `x` is this module element. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A2 = SteenrodAlgebra(2, profile=(3,2,1)) + sage: M = FP_Module(A2, [0,3], [[Sq(2)*Sq(4), Sq(3)]]) + sage: A2.Sq(2)*M.generator(1) + <0, Sq(2)> + sage: A2.Sq(2)*(A2.Sq(1)*A2.Sq(2)*M.generator(0) + M.generator(1)) + + + TESTS: + + sage: elements = [M.an_element(n) for n in range(1,10)] + sage: a = A2.Sq(3) + sage: [a*x for x in elements] + [, + <0, 0>, + , + <0, Sq(1,1)>, + <0, 0>, + , + , + , + <0, Sq(3,2)>] + """ + return self.parent()(a*self.free_element()) + + + def vector_presentation(self): + r""" + A coordinate vector representing this module element when it is non-zero. + + These are coordinates with respect to the basis chosen by + :meth:`sage.modules.fp_graded.module.FP_Module.basis_elements`. + When the element is zero, it has no well defined degree, and this + function returns ``None``. + + OUTPUT: A vector of elements in the ground field of the algebra for + this module when this element is non-zero. Otherwise, the value + ``None``. + + .. SEEALSO:: + + :meth:`sage.modules.fp_graded.module.FP_Module.vector_presentation` + :meth:`sage.modules.fp_graded.module.FP_Module.basis_elements` + :meth:`sage.modules.fp_graded.module.FP_Module.element_from_coordinates` + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A2 = SteenrodAlgebra(2, profile=(3,2,1)) + sage: M = FP_Module(A2, (0,1)) + + sage: x = M.an_element(7) + sage: v = x.vector_presentation(); v + (1, 0, 0, 0, 0, 1, 0) + sage: type(v) + + + sage: V = M.vector_presentation(7) + sage: v in V + True + + sage: M.element_from_coordinates(v, 7) == x + True + + We can use the basis for the module elements in the degree of `x`, + together with the coefficients `v` to recreate the element `x`:: + + sage: basis = M.basis_elements(7) + sage: x_ = sum( [c*b for (c,b) in zip(v, basis)] ); x_ + + sage: x == x_ + True + + TESTS: + + sage: M.zero().vector_presentation() is None + True + """ + # We cannot represent the zero element since it does not have a degree, + # and we therefore do not know which vector space it belongs to. + # + # In this case, we could return the integer value 0 since coercion would + # place it inside any vector space. However, this will not work for + # homomorphisms, so we return None to be consistent. + try: + degree = self.free_element().degree() + except ValueError: + return None + + F_n = self.parent().vector_presentation(degree) + return F_n.quotient_map()(self.free_element().vector_presentation()) + + + def __nonzero__(self): + r""" + Determine if this element is non-zero. + + OUTPUT: The boolean value ``True`` if this element is non-zero, and ``False`` + otherwise. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: M = FP_Module(SteenrodAlgebra(2), [0,2,4], [[Sq(4),Sq(2),0]]) + sage: M(0).__nonzero__() + False + sage: M((Sq(6), 0, Sq(2))).__nonzero__() + True + sage: a = M((Sq(1)*Sq(2)*Sq(1)*Sq(4), 0, 0)) + sage: b = M((0, Sq(2)*Sq(2)*Sq(2), 0)) + sage: a.__nonzero__() + True + sage: b.__nonzero__() + True + sage: (a + b).__nonzero__() + False + """ + pres = self.vector_presentation() + if pres is None: + return False + return bool(pres) + + __bool__ = __nonzero__ + + def __eq__(self, other): + r""" + True iff ``self`` and ``other`` are equal. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: M = FP_Module(SteenrodAlgebra(2), [0,1], [[Sq(4), Sq(3)]]) + sage: x = M([Sq(1), 1]) + sage: x + + sage: x == x + True + sage: x == M.zero() + False + sage: x-x == M.zero() + True + """ + try: + return (self - other).is_zero() + except TypeError: + return False + + + def normalize(self): + r""" + A normalized form of ``self``. + + OUTPUT: An instance of this element class representing the same + module element as this element. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: M = FP_Module(SteenrodAlgebra(2), [0,2,4], [[Sq(4),Sq(2),0]]) + + sage: m = M((Sq(6), 0, Sq(2))); m + + sage: m.normalize() + + sage: m == m.normalize() + True + + sage: n = M((Sq(4), Sq(2), 0)); n + + sage: n.normalize() + <0, 0, 0> + sage: n == n.normalize() + True + """ + if self.is_zero(): + return self.parent().zero() + + v = self.vector_presentation() + return self.parent().element_from_coordinates(v, self.degree()) diff --git a/src/sage/modules/fp_graded/free_element.py b/src/sage/modules/fp_graded/free_element.py new file mode 100755 index 00000000000..d7b2bfbc8ed --- /dev/null +++ b/src/sage/modules/fp_graded/free_element.py @@ -0,0 +1,239 @@ +r""" +Elements of finitely generated free graded left modules + +This class implements construction and basic manipulation of +elements of the Sage parent +:class:`sage.modules.fp_graded.free_module.FreeModule`, which models +free graded left modules over connected algebras. + +For an overview of the free module API, see :doc:`free_module`. + +AUTHORS: + +- Robert R. Bruner, Michael J. Catanzaro (2012): Initial version. +- Sverre Lunoee--Nielsen and Koen van Woerden (2019-11-29): Updated the + original software to Sage version 8.9. +- Sverre Lunoee--Nielsen (2020-07-01): Refactored the code and added + new documentation and tests. +""" + +#***************************************************************************** +# Copyright (C) 2019 Robert R. Bruner +# and Michael J. Catanzaro +# +# 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.cachefunc import cached_method +from sage.modules.with_basis.indexed_element import IndexedFreeModuleElement + +class FreeGradedModuleElement(IndexedFreeModuleElement): + r""" + Create a module element of a finitely generated free graded left module + over a connected graded algebra. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: M = FreeGradedModule(SteenrodAlgebra(2), (0, 1)) + + sage: M([0, 0]) + <0, 0> + + sage: M([1, 0]) + <1, 0> + + sage: M([0, 1]) + <0, 1> + + sage: M([Sq(1), 1]) + + """ + def degree(self): + r""" + The degree of this element. + + OUTPUT: the integer degree of this element, or None if this is the zero + element. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import * + sage: A = SteenrodAlgebra(2) + sage: M = FreeGradedModule(A, (0,1)) + sage: x = M.an_element(7); x + + sage: x.degree() + 7 + + The zero element has no degree:: + + sage: (x-x).degree() + Traceback (most recent call last): + ... + ValueError: the zero element does not have a well-defined degree + + Neither do non-homogeneous slements + + sage: y = M.an_element(4) + sage: (x+y).degree() + Traceback (most recent call last): + ... + ValueError: this is a nonhomogeneous element, no well-defined degree + """ + if self.is_zero(): + raise ValueError("the zero element does not have a well-defined degree") + degrees = [] + try: + for g, c in zip(self.parent().generator_degrees(), + self.dense_coefficient_list()): + if c: + degrees.append(g + c.degree()) + except ValueError: + raise ValueError("this is a nonhomogeneous element, no well-defined degree") + m = min(degrees) + M = max(degrees) + if m == M: + return m + raise ValueError("this is a nonhomogeneous element, no well-defined degree") + + + def _repr_(self): + r""" + Return a string representation of this element. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import * + sage: A = SteenrodAlgebra(2) + sage: M = FreeGradedModule(A, (0,1)) + sage: [M.an_element(n) for n in range(1,10)] + [, + , + , + , + , + , + , + , + ] + """ + return '<%s>' % ', '.join(['%s' % c for c in self.dense_coefficient_list()]) + + + def _lmul_(self, a): + r""" + Act by left multiplication on this element by ``a``. + + INPUT: + + - ``a`` -- an element of the algebra this module is defined over. + + OUTPUT: the module element `a\cdot x` where `x` is this module element. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import * + sage: A2 = SteenrodAlgebra(2, profile=(3,2,1)) + sage: M = FreeGradedModule(A2, (0,0,3)) + sage: A2.Sq(2)*M.generator(1) + <0, Sq(2), 0> + sage: A2.Sq(2)*(A2.Sq(1)*A2.Sq(2)*M.generator(1) + M.generator(2)) + <0, Sq(2,1), Sq(2)> + + TESTS: + + sage: elements = [M.an_element(n) for n in range(1,10)] + sage: a = A2.Sq(3) + sage: [a*x for x in elements] + [, + <0, 0, 0>, + , + <0, 0, Sq(1,1)>, + <0, 0, 0>, + , + , + , + <0, 0, Sq(3,2)>] + """ + return self.parent()((a*c for c in self.dense_coefficient_list())) + + @cached_method + def vector_presentation(self): + r""" + A coordinate vector representing this module element when it is non-zero. + + These are coordinates with respect to the basis chosen by + :meth:`sage.modules.fp_graded.free_module.FreeGradedModule.basis_elements`. + When the element is zero, it has no well defined degree, and this + function returns ``None``. + + OUTPUT: A vector of elements in the ground field of the algebra for + this module when this element is non-zero. Otherwise, the value + ``None``. + + .. SEEALSO:: + + :meth:`sage.modules.fp_graded.free_module.FreeGradedModule.vector_presentation` + :meth:`sage.modules.fp_graded.free_module.FreeGradedModule.basis_elements` + :meth:`sage.modules.fp_graded.free_module.FreeGradedModule.element_from_coordinates` + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import * + sage: A2 = SteenrodAlgebra(2, profile=(3,2,1)) + sage: M = FreeGradedModule(A2, (0,1)) + sage: x = M.an_element(7) + sage: v = x.vector_presentation(); v + (1, 0, 0, 0, 0, 1, 0) + sage: type(v) + + + sage: V = M.vector_presentation(7) + sage: v in V + True + + sage: M.element_from_coordinates(v, 7) == x + True + + We can use the basis for the module elements in the degree of `x`, + together with the coefficients `v` to recreate the element `x`:: + + sage: basis = M.basis_elements(7) + sage: x_ = sum( [c*b for (c,b) in zip(v, basis)] ); x_ + + sage: x == x_ + True + + TESTS: + + sage: M.zero().vector_presentation() is None + True + """ + # We cannot represent the zero element since it does not have a degree, + # and we therefore do not know which vector space it belongs to. + # + # In this case, we could return the integer value 0 since coercion would + # place it inside any vector space. However, this will not work for + # homomorphisms, so we we return None to be consistent. + if self.is_zero(): + return None + + bas_gen = self.parent().basis_elements(self.degree()) + base_vec = self.parent().vector_presentation(self.degree()) + + base_dict = dict(zip(bas_gen, base_vec.basis())) + + # Create a sparse representation of the element. + sparse_coeffs = [x for x in enumerate(self.dense_coefficient_list()) if not x[1].is_zero()] + + vector = base_vec.zero() + for summand_index, algebra_element in sparse_coeffs: + for scalar_coefficient, monomial in zip(algebra_element.coefficients(), algebra_element.monomials()): + vector += scalar_coefficient*base_dict[monomial*self.parent().generator(summand_index)] + + return vector diff --git a/src/sage/modules/fp_graded/free_homspace.py b/src/sage/modules/fp_graded/free_homspace.py new file mode 100755 index 00000000000..ccdfc37ed66 --- /dev/null +++ b/src/sage/modules/fp_graded/free_homspace.py @@ -0,0 +1,196 @@ +r""" +The set of homomorphisms of finitely generated free graded left modules + +This class implements methods for construction and basic manipulation +of homsets of finitely generated free graded left modules over a +connected graded `k`-algebra, where `k` is a field. + +For an overview of the free module API, see :doc:`free_module`. + +TESTS: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: from sage.misc.sage_unittest import TestSuite + sage: A = SteenrodAlgebra(2) + sage: F1 = FreeGradedModule(A, (1,3)) + sage: F2 = FreeGradedModule(A, (2,3)) + sage: homset = Hom(F1, F2); homset + Set of Morphisms from Finitely presented free left module on 2 generators ... + sage: homset([F2((Sq(1), 1)), F2((0, Sq(2)))]) + Module homomorphism of degree 2 defined by sending the generators + [<1, 0>, <0, 1>] + to + [, <0, Sq(2)>] + sage: TestSuite(homset).run() + +AUTHORS: + +- Robert R. Bruner, Michael J. Catanzaro (2012): Initial version. +- Sverre Lunoee--Nielsen and Koen van Woerden (2019-11-29): Updated the + original software to Sage version 8.9. +- Sverre Lunoee--Nielsen (2020-07-01): Refactored the code and added + new documentation and tests. +""" + +#***************************************************************************** +# Copyright (C) 2011 Robert R. Bruner and +# Michael J. Catanzaro +# +# 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.categories.homset import Homset +from sage.misc.cachefunc import cached_method + + +def is_FreeGradedModuleHomspace(x): + r""" + Check if the given object is of type FreeGradedModuleHomspace. + + OUTPUT: The boolean ``True`` if and only if ``x`` is of type + FreeGradedModuleHomspace, and ``False`` otherwise. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: from sage.modules.fp_graded.free_homspace import is_FreeGradedModuleHomspace + sage: A2 = SteenrodAlgebra(2, profile=(3,2,1)) + sage: F = FreeGradedModule(A2, (1,3)) + sage: L = FreeGradedModule(A2, (2,3)) + sage: is_FreeGradedModuleHomspace(Hom(F, L)) + True + + TESTS: + + sage: is_FreeGradedModuleHomspace(0) + False + """ + return isinstance(x, FreeGradedModuleHomspace) + +class FreeGradedModuleHomspace(Homset): + # In the category framework, Elements of the class FP_Module are of the + # class FP_Element, see + # http://doc.sagemath.org/html/en/thematic_tutorials/coercion_and_categories.html#implementing-the-category-framework-for-the-elements + from .free_morphism import FreeGradedModuleMorphism + + Element = FreeGradedModuleMorphism + + def _element_constructor_(self, values): + r""" + Construct any element of this homset. + + This function is used internally by the ()-method when creating + homomorphisms. + + INPUT: + + - ``values`` -- A tuple of values (i.e. elements of the + codomain for this homset) corresponding bijectively to the generators + of the domain of this homset, or the zero integer constant. + + OUTPUT: An instance of the morphism class. The returned morphism is + defined by mapping the module generators in the domain to the given + values. + + OUTPUT: A module homomorphism. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: A2 = SteenrodAlgebra(2, profile=(3,2,1)) + sage: F = FreeGradedModule(A2, (1,3)) + sage: L = FreeGradedModule(A2, (2,5)) + sage: H = Hom(F, L) + + sage: values = (A2.Sq(4)*L.generator(0), A2.Sq(3)*L.generator(1)) + sage: f = H(values); f + Module homomorphism of degree 5 defined by sending the generators + [<1, 0>, <0, 1>] + to + [, <0, Sq(3)>] + + sage: H(0) + The trivial homomorphism. + """ + from .free_morphism import FreeGradedModuleMorphism + if isinstance(values, FreeGradedModuleMorphism): + return values + elif values == 0 or all(v.is_zero() for v in values): + return self.zero() + else: + return self.element_class(self, values) + + + def _an_element_(self): + r""" + Return a morphism belonging to this homspace. + + OUTPUT: A morphism in this homspace. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: A2 = SteenrodAlgebra(2, profile=(3,2,1)) + sage: F = FreeGradedModule(A2, (1,3)) + sage: L = FreeGradedModule(A2, (2,3)) + sage: H = Hom(F, L) + sage: H._an_element_() + The trivial homomorphism. + """ + return self.zero() + + + @cached_method + def zero(self): + r""" + Return the trivial morphism of this homspace. + + OUTPUT: The morphism evaluating to the zero element for any element in + the domain. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: A2 = SteenrodAlgebra(2, profile=(3,2,1)) + sage: F = FreeGradedModule(A2, (1,3)) + sage: L = FreeGradedModule(A2, (2,3)) + sage: H = Hom(F, L) + sage: H.zero() + The trivial homomorphism. + """ + return self.element_class(self, self.codomain().zero()) + + + def identity(self): + r""" + Return the identity morphism, if this is an endomorphism set. + + OUTPUT: The identity endomorphism. + + TESTS: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: A2 = SteenrodAlgebra(2, profile=(3,2,1)) + sage: L = FreeGradedModule(A2, (2,3)) + sage: H = Hom(L, L) + sage: H.identity() + The identity homomorphism. + + TESTS: + + sage: F = FreeGradedModule(A2, (1,3)) + sage: H = Hom(F, L) + sage: H.identity() + Traceback (most recent call last): + ... + TypeError: this homspace does not consist of endomorphisms + """ + if self.is_endomorphism_set(): + return self.element_class(self, self.codomain().generators()) + else: + raise TypeError('this homspace does not consist of endomorphisms') + diff --git a/src/sage/modules/fp_graded/free_module.py b/src/sage/modules/fp_graded/free_module.py new file mode 100755 index 00000000000..63ab4187cb0 --- /dev/null +++ b/src/sage/modules/fp_graded/free_module.py @@ -0,0 +1,767 @@ +r""" +Finitely generated free graded left modules over connected graded algebras. + +This class implements methods for construction and basic manipulation of +finitely generated free graded modules over connected graded algebras. + +========== +User guide +========== + +Let `p` be a prime number. The mod `p` Steenrod algebra `A_p` +is a connected algebra over the finite field of `p` elements. All modules +presented here will be defined over `A_p`, or one of its sub-Hopf algebras. +E.g.:: + + sage: A = SteenrodAlgebra(p=2) + +The constructor of the module class takes as arguments the algebra +over which the module is defined and an ordered tuple of degrees for +the generators:: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: M = FreeGradedModule(algebra=A, generator_degrees=(0,1)); M + Finitely presented free left module on 2 generators over mod 2 Steenrod algebra, milnor basis + +The resulting free module will have generators in the degrees given to its +constructor:: + + sage: M.generator_degrees() + (0, 1) + +The connectivity of a module over a connected graded algebra is the minimum +degree of all its module generators. Thus, if the module is non-trivial, the +connectivity is an integer:: + + sage: M.connectivity() + 0 + +--------------- +Module elements +--------------- + +For an `A`-module with generators `\{g_i\}_{i=1}^N`, any homogeneous element +of degree `n` has the form + +.. MATH:: + + x = \sum_{i=1}^N a_i\cdot g_i\,, + +where `a_i\in A_{n-\deg(g_i)}` for all `i`. The ordered set `\{a_i\}` +is referred to as the coefficients of `x`. + +Module elements are displayed by their algebra coefficients:: + + sage: M.an_element(n=5) + + + sage: M.an_element(n=15) + + +The generators are themselves elements of the module:: + + sage: M.generators() + [<1, 0>, <0, 1>] + +Producing elements from a given set of coefficients is possible using the +module class ()-method:: + + sage: coeffs=[Sq(5), Sq(1,1)] + sage: x = M(coeffs); x + + +The module action produces new elements:: + + sage: Sq(2)*x + + +Each non-zero element has a well-defined degree:: + + sage: x.degree() + 5 + +But the zero element has not:: + + sage: zero = M.zero(); zero + <0, 0> + sage: zero.degree() + Traceback (most recent call last): + ... + ValueError: the zero element does not have a well-defined degree + +Any two elements can be added as long as they are in the same degree:: + + sage: y = M.an_element(5); y + + sage: x + y + + +or when at least one of them is zero:: + + sage: x + zero == x + True + +Finally, additive inverses exist:: + + sage: x - x + <0, 0> + +For every integer `n`, the set of module elements of degree `n` form a +vector space over the ground field `k`. A basis for this vector space can be +computed:: + + sage: M.basis_elements(5) + [, , <0, Sq(1,1)>, <0, Sq(4)>] + +together with a corresponding vector space presentation:: + + sage: M.vector_presentation(5) + Vector space of dimension 4 over Finite Field of size 2 + +Given any element, its coordinates with resepct to this basis can be computed:: + + sage: v = x.vector_presentation(); v + (0, 1, 1, 0) + +Going the other way, any element can be constructed by specifying its +coordinates:: + + sage: x_ = M.element_from_coordinates((0,1,1,0), 5) + sage: x_ + + sage: x_ == x + True + +-------------------- +Module homomorphisms +-------------------- + +Homomorphisms of free graded `A`-modules `M\to N` are linear maps of their +underlying `k`-vector spaces which commute with the `A`-module structure. + +To create a homomorphism, first create the object modelling the set of all +such homomorphisms using the free function ``Hom``:: + + sage: M = FreeGradedModule(A, (0,1)) + sage: N = FreeGradedModule(A, (2,)) + sage: homspace = Hom(M, N); homspace + Set of Morphisms from Finitely presented free left module on 2 generators over mod 2 Steenrod algebra, milnor basis to Finitely presented free left module on 1 generator over mod 2 Steenrod algebra, milnor basis in Category of finite dimensional graded modules with basis over mod 2 Steenrod algebra, milnor basis + +Just as module elements, homomorphisms are created using the ()-method +of the homspace object. The only argument is a list of module elements in the +codomain, corresponding to the module generators of the domain:: + + sage: g = N([1]) # the generator of the codomain module. + sage: values = [Sq(2)*g, Sq(2)*Sq(1)*g] + sage: f = homspace(values) + +The resulting homomorphism is the one sending the `i`-th generator of the +domain to the `i`-th codomain value given:: + + sage: f + Module homomorphism of degree 4 defined by sending the generators + [<1, 0>, <0, 1>] + to + [, ] + +Convenience methods exist for creating the trivial morphism:: + + sage: homspace.zero() + The trivial homomorphism. + +as well as the identity endomorphism:: + + sage: Hom(M, M).identity() + The identity homomorphism. + +Homomorphisms can be evaluated on elements of the domain module:: + + sage: v1 = f(Sq(7)*M.generator(0)); v1 + + + sage: v2 = f(Sq(17)*M.generator(1)); v2 + + +and they respect the module action:: + + sage: v1 == Sq(7)*f(M.generator(0)) + True + + sage: v2 == Sq(17)*f(M.generator(1)) + True + +Any non-trivial homomorphism has a well-defined degree:: + + sage: f.degree() + 4 + +but just as module elements, the trivial homomorphism does not:: + + sage: zero_map = homspace.zero() + sage: zero_map.degree() + Traceback (most recent call last): + ... + ValueError: the zero morphism does not have a well-defined degree + +Any two homomorphisms can be added as long as they are of the same degree:: + + sage: f2 = homspace([Sq(2)*g, Sq(3)*g]) + sage: f + f2 + Module homomorphism of degree 4 defined by sending the generators + [<1, 0>, <0, 1>] + to + [<0>, ] + +or when at least one of them is zero:: + + sage: f + zero_map == f + True + +Finally, additive inverses exist:: + + sage: f - f + The trivial homomorphism. + +The restriction of a homomorphism to the vector space of `n`-dimensional module +elements is a linear transformation:: + + sage: f_4 = f.vector_presentation(4); f_4 + Vector space morphism represented by the matrix: + [0 1 0] + [1 1 1] + [0 1 0] + [0 0 0] + Domain: Vector space of dimension 4 over Finite Field of size 2 + Codomain: Vector space of dimension 3 over Finite Field of size 2 + +This is compatible with the vector presentations of its domain and codomain +modules:: + + sage: f.domain() is M + True + sage: f.codomain() is N + True + sage: f_4.domain() is M.vector_presentation(4) + True + sage: f_4.codomain() is N.vector_presentation(4 + f.degree()) + True + + +AUTHORS: + +- Robert R. Bruner, Michael J. Catanzaro (2012): Initial version. +- Sverre Lunoee--Nielsen and Koen van Woerden (2019-11-29): Updated the + original software to Sage version 8.9. +- Sverre Lunoee--Nielsen (2020-07-01): Refactored the code and added + new documentation and tests. +""" + +#***************************************************************************** +# Copyright (C) 2019 Robert R. Bruner +# and Michael J. Catanzaro +# +# 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.cachefunc import cached_method +from sage.modules.free_module import VectorSpace +from sage.rings.infinity import PlusInfinity +from sage.categories.graded_modules_with_basis import GradedModulesWithBasis +from sage.combinat.free_module import CombinatorialFreeModule + +from .free_element import FreeGradedModuleElement +from .free_homspace import FreeGradedModuleHomspace + +class FreeGradedModule(CombinatorialFreeModule): + r""" + Create a finitely generated free graded module over a connected + graded algebra, with generators in specified degrees. + + INPUT: + + - ``algebra`` -- the connected algebra over which the module is defined. + + - ``generator_degrees`` -- a tuple of integers defining + the number of generators of the module, and their degrees. + + OUTPUT: The finitely generated free graded module on generators with + degrees given by ``generator_degrees``. + + TESTS: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: A = SteenrodAlgebra(2) + sage: FreeGradedModule(A, (-2,2,4)) + Finitely presented free left module on 3 generators over mod 2 Steenrod algebra, milnor basis + """ + def __init__(self, algebra, generator_degrees): + r""" + Create a finitely generated free graded module over a connected graded + algebra. + """ + # If generator_degrees is [d_0, d_1, ...], then + # the generators are indexed by (0,d_0), (1,d_1), ... + keys = [(i,deg) for i,deg in enumerate(generator_degrees)] + self._generator_keys = keys + + if not algebra.base_ring().is_field(): + raise ValueError('the ground ring of the algebra must be a field') + + # Call the base class constructor. + CombinatorialFreeModule.__init__(self, algebra, + basis_keys=keys, + element_class=FreeGradedModuleElement, + category=GradedModulesWithBasis(algebra)) + + + def generator_degrees(self): + r""" + The degrees of the module generators. + + OUTPUT: A tuple containing the degrees of the generators for this + module, in the order that the generators were given when this module + was constructed. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import * + sage: A = SteenrodAlgebra(2) + sage: M = FreeGradedModule(A, (-2,2,4)) + sage: M.generator_degrees() + (-2, 2, 4) + """ + return tuple(a[1] for a in self._generator_keys) + + + def is_trivial(self): + r""" + Decide if this module is trivial or not. + + OUTPUT: The boolean value ``True`` if the module is trivial, and + ``False`` otherwise. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import * + sage: A = SteenrodAlgebra(2) + sage: FreeGradedModule(A, (-2,2,4)).is_trivial() + False + sage: FreeGradedModule(A, ()).is_trivial() + True + """ + return len(self.generator_degrees()) == 0 + + + def connectivity(self): + r""" + The connectivity of this module. + + OUTPUT: An integer equal to the minimal degree of all the generators, if + this module is non-trivial. Otherwise, `+\infty`. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import * + sage: A = SteenrodAlgebra(2) + sage: M = FreeGradedModule(A, (-2,2,4)) + sage: M.connectivity() + -2 + + TESTS: + + sage: M = FreeGradedModule(A, ()) + sage: M.is_trivial() + True + sage: M.connectivity() + +Infinity + """ + return min(self.generator_degrees() + (PlusInfinity(),)) + + + def _element_constructor_(self, coefficients): + r""" + Construct any element of the module. + + This function is used internally by the ()-method when creating + module elements, and should not be called by the user explicitly. + + INPUT: + + - ``coefficients`` -- A tuple of coefficient (i.e. elements of the + algebra for this module), an element of FreeGradedModule, or the zero integer + constant. + + OUTPUT: An instance of the element class with coefficients from + ``coefficients``, the element ``coefficients`` if it already was an + element, or the zero module element. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import * + sage: A = SteenrodAlgebra(2) + sage: M = FreeGradedModule(A, (0,2,4)) + + sage: zero = M(0); zero + <0, 0, 0> + + sage: e = M((Sq(4), Sq(2), 1)); e + + + sage: e is M(e) + True + """ + if isinstance(coefficients, self.element_class): + return coefficients + elif not coefficients: + return self.zero() + else: + B = self.basis() + return sum(c*B[b] for (c,b) in zip(coefficients, self._generator_keys)) + + + def an_element(self, n=None): + r""" + Return an element of the module. + + This function chooses deterministically an element of the module in the + given degree. + + INPUT: + + - ``n`` -- the degree of the element to construct. If the default + value ``None`` is given, a degree will be chosen by the function. + + OUTPUT: An element of the given degree. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import * + sage: A = SteenrodAlgebra(2) + sage: M = FreeGradedModule(A, (0,2,4)) + sage: M.an_element(172) + + + Zero is the only element in the trivial module:: + + sage: FreeGradedModule(A, ()).an_element() + <> + """ + if len(self.generator_degrees()) == 0: + return self.zero() + + if n == None: + n = max(self.generator_degrees()) + 7 + + coefficients = [] + for g in self.generator_degrees(): + basis = self.base_ring().basis(n - g) if n >= g else () + # All of the algebra generators in basis will bring the + # module generator in dimension g to dimension + # g + (topDimension - g) = topDimension. Picking any one of them + # will do, so we pick the one with index (g (mod l)). + l = len(basis) + if l: + coefficients.append(basis[g % l]) + else: + coefficients.append(self.base_ring().zero()) + + return self(coefficients) + + + def _repr_(self): + r""" + Construct a string representation of the module. + + TESTS: + + sage: from sage.modules.fp_graded.free_module import * + sage: A = SteenrodAlgebra(2) + sage: M = FreeGradedModule(A, (0,2,4)) + sage: M._repr_() + 'Finitely presented free left module on 3 generators over mod 2 Steenrod algebra, milnor basis' + """ + return "Finitely presented free left module on %s generator%s over %s"\ + %(len(self.generator_degrees()), "" if len(self.generator_degrees()) == 1 else "s", + self.base_ring()) + + + @cached_method + def basis_elements(self, n): + r""" + A basis for the vector space of degree ``n`` module elements. + + INPUT: + + - ``n`` -- an integer. + + OUTPUT: A sequence of homogeneous module elements of degree ``n`` + which is a basis for the vector space of all degree ``n`` module + elements. + + .. SEEALSO:: + :meth:`vector_presentation`, :meth:`element_from_coordinates` + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import * + sage: A = SteenrodAlgebra(2) + sage: M = FreeGradedModule(A, (0,2,4)) + sage: M.basis_elements(8) + [, + , + , + , + <0, Sq(0,2), 0>, + <0, Sq(3,1), 0>, + <0, Sq(6), 0>, + <0, 0, Sq(1,1)>, + <0, 0, Sq(4)>] + """ + basis_n = [] + for i, generator_degree in enumerate(self.generator_degrees()): + l = n - generator_degree + basis_n += [a*self.generator(i) for a in self.base_ring().basis(l)] + + return basis_n + + + @cached_method + def element_from_coordinates(self, coordinates, n): + r""" + The module element of degree ``n`` having the given coordinates + with respect to the basis of module elements given by + :meth:`basis_elements`. + + INPUT: + + - ``coordinates`` -- a sequence of elements of the ground field. + - ``n`` -- an integer. + + OUTPUT: A module element of degree ``n``. + + .. SEEALSO:: + :meth:`vector_presentation`, and :meth:`basis_elements`. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import * + sage: A = SteenrodAlgebra(2) + sage: M = FreeGradedModule(A, (0,1)) + sage: x = M.element_from_coordinates((0,1,0,1), 5); x + + sage: basis = M.basis_elements(5) + sage: y = 0*basis[0] + 1*basis[1] + 0*basis[2] + 1*basis[3] + sage: x == y + True + + sage: M.element_from_coordinates((0,0,0,0), 5) + <0, 0> + """ + basis_elements = self.basis_elements(n) + + if len(coordinates) != len(basis_elements): + raise ValueError('the given coordinate vector has incorrect length: %d. ' + 'It should have length %d' % (len(coordinates), len(basis_elements))) + + # Adding the condition `if c != 0` improved performance dramatically in this + # real life example: + # + # sage: rels = [ [Sq(1),0,0,0], [Sq(2),0,0,0], [Sq(4),0,0,0], [Sq(8),0,0,0], [0,Sq(1),0, + # ....: 0], [0,Sq(2),0,0], [0,Sq(4),0,0], [Sq(31),Sq(14),0,0], [0,Sq(20),0,0], [0,0,Sq(1 + # ....: ),0], [0,0,Sq(2),0], [0,Sq(31),Sq(6),0], [0,0,Sq(8),0], [0,0,0,Sq(1)], [0,0,Sq(3 + # ....: 1),Sq(2)], [0,0,0,Sq(4)], [0,0,0,Sq(8)] ] + # ....: + # ....: M = FPA_Module([0, 17, 42, 71], A, relations=rels) + # sage: res = M.resolution(2, top_dim=30, verbose=True) + # + # This function was called a total of 2897 times during the computation, + # and the total running time of the entire computation dropped from + # 57 to 21 seconds by adding the optimization. + # + element = sum([c*element for c, element in zip(coordinates, basis_elements) if c != 0]) + if element == 0: + # The previous sum was over the empty list, yielding the integer + # 0 as a result, rather than a module element. + # Fix this by returning the zero element. + return self.zero() + else: + # The sum defining element is of the correct type, so return it. + return element + + + def __getitem__(self, n): + r""" + A vector space isomorphic to the vector space of module elements of + degree ``n``. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import * + sage: A = SteenrodAlgebra(2) + sage: M = FreeGradedModule(A, (0,2,4)) + sage: V = M[4]; V + Vector space of dimension 4 over Finite Field of size 2 + sage: V.dimension() + 4 + + .. SEEALSO:: + This function is an alias for :meth:`vector_presentation`. + """ + return self.vector_presentation(n) + + + @cached_method + def vector_presentation(self, n): + r""" + A vector space over the ground field of the module algebra, + isomorphic to the degree ``n`` elements of this module. + + Let `\mathcal{k}` be the ground field of the algebra over this module is defined, + and let `M_n` be the vector space of module elements of degree ``n``. + + The return value of this function is the vector space + `\mathcal{k}^{r}` where `r = dim(M_n)`. + + The isomorphism between `k^{r}` and `M_n` is given by the + bijection taking the standard basis element `e_i` to the `i`-th + element of the array returned by :meth:`basis_elements`. + + INPUT: + + - ``n`` -- an integer degree. + + OUTPUT: A vector space over the ground field of the algebra over which + this module is defined, isomorphic to the vector space of module + elements of degree ``n``. + + .. SEEALSO:: + :meth:`basis_elements`, :meth:`element_from_coordinates` + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import * + sage: A1 = SteenrodAlgebra(2, profile=[2,1]) + sage: M = FreeGradedModule(A1, (0,)) + sage: M.vector_presentation(3) + Vector space of dimension 2 over Finite Field of size 2 + sage: M.basis_elements(3) + [, ] + sage: [M.vector_presentation(i).dimension() for i in range(-2, 9)] + [0, 0, 1, 1, 1, 2, 1, 1, 1, 0, 0] + """ + return VectorSpace(self.base_ring().base_ring(), len(self.basis_elements(n))) + + + @cached_method + def generator(self, index): + r""" + Return the module generator with the given index. + + OUTPUT: An instance of the element class of this parent. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import * + sage: A = SteenrodAlgebra(2) + sage: M = FreeGradedModule(A, (0,2,4)) + sage: M.generator(0) + <1, 0, 0> + sage: M.generator(1) + <0, 1, 0> + sage: M.generator(2) + <0, 0, 1> + """ + try: + key = self._generator_keys[index] + except IndexError: + raise ValueError('the parent module has generators in the index '\ + 'range [0, %s]; generator %s does not exist' %\ + (len(self.generator_degrees()) - 1, index)) + + return self.monomial(self._generator_keys[index]) + + + def generators(self): + r""" + Return all the module generators. + + OUTPUT: A list consisting instances of the element class of this + parent. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import * + sage: A = SteenrodAlgebra(2) + sage: M = FreeGradedModule(A, (0,1)) + sage: M.generators() + [<1, 0>, <0, 1>] + """ + return [self.generator(i) for i in range(len(self.generator_degrees()))] + + + def _Hom_(self, Y, category): + r""" + The internal hook used by the free function + :meth:`sage.categories.homset.hom.Hom` to create homsets involving this + parent. + + TESTS: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: A = SteenrodAlgebra(2) + sage: M = FreeGradedModule(A, (0,1)) + sage: M._Hom_(M, category=None) + Set of Morphisms from Finitely presented free left module on 2 generators over mod 2 Steenrod algebra, milnor basis to Finitely presented free left module on 2 generators over mod 2 Steenrod algebra, milnor basis in Category of finite dimensional graded modules with basis over mod 2 Steenrod algebra, milnor basis + """ + return FreeGradedModuleHomspace(self, Y, category) + + + def suspension(self, t): + r""" + Suspend the module by the given integer degree. + + INPUT: + + - ``t`` -- An integer. + + OUTPUT: A module which is isomorphic to this module by a shift + of degrees by the integer ``t``. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import * + sage: A = SteenrodAlgebra(2) + sage: M = FreeGradedModule(A, (0,2,4)) + sage: M.suspension(4).generator_degrees() + (4, 6, 8) + sage: M.suspension(-4).generator_degrees() + (-4, -2, 0) + """ + return FreeGradedModule(algebra=self.base_ring(), + generator_degrees=tuple([g + t for g in self.generator_degrees()])) + + + def to_fp_module(self): + """ + Create a finitely presented module from this free module. + + OUTPUT: the finitely presented module having same set of generators + as this module, no relations. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: A = SteenrodAlgebra(2) + sage: F = FreeGradedModule(A, (-2,2,4)) + sage: F.to_fp_module() + Finitely presented left module on 3 generators and 0 relations over mod 2 Steenrod algebra, milnor basis + """ + from .module import FP_Module + return FP_Module(algebra=self.base_ring(), + generator_degrees=self.generator_degrees(), + relations=()) + diff --git a/src/sage/modules/fp_graded/free_morphism.py b/src/sage/modules/fp_graded/free_morphism.py new file mode 100755 index 00000000000..e58de46fb16 --- /dev/null +++ b/src/sage/modules/fp_graded/free_morphism.py @@ -0,0 +1,595 @@ +r""" +Homomorphisms of finitely generated free graded left modules + +This class implements construction and basic manipulation of elements +of the Sage parent +:class:`sage.modules.fp_graded.free_homspace.FreeGradedModuleHomspace`, +which models homomorphisms of free graded left modules over connected +algebras. + +For an overview of the free module API, see :doc:`free_module`. + +AUTHORS: + +- Robert R. Bruner, Michael J. Catanzaro (2012): Initial version. +- Sverre Lunoee--Nielsen and Koen van Woerden (2019-11-29): Updated the + original software to Sage version 8.9. +- Sverre Lunoee--Nielsen (2020-07-01): Refactored the code and added + new documentation and tests. +""" + +#***************************************************************************** +# Copyright (C) 2019 Robert R. Bruner +# and Michael J. Catanzaro +# +# 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 __future__ import absolute_import + +from inspect import isfunction + +from sage.categories.homset import Hom +from sage.categories.morphism import Morphism +from sage.misc.cachefunc import cached_method + +from .free_homspace import is_FreeGradedModuleHomspace + + +class FreeGradedModuleMorphism(Morphism): + r""" + Create a homomorphism between finitely generated free graded modules. + + INPUT: + + - ``parent`` -- A homspace in the category of finitely generated free + modules. + + - ``values`` -- A list of elements in the codomain. Each element + corresponds (by their ordering) to a module generator in the domain. + + OUTPUT: A module homomorphism defined by sending each generator to its + corresponding value. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: A = SteenrodAlgebra(2) + sage: F1 = FreeGradedModule(A, (4,5)) + sage: F2 = FreeGradedModule(A, (3,4)) + sage: F3 = FreeGradedModule(A, (2,3)) + sage: H1 = Hom(F1, F2) + sage: H2 = Hom(F2, F3) + sage: f = H1( ( F2((Sq(4), 0)), F2((0, Sq(4))) ) ) + sage: g = H2( ( F3((Sq(2), 0)), F3((Sq(3), Sq(2))) ) ) + sage: g*f + Module homomorphism of degree 4 defined by sending the generators + [<1, 0>, <0, 1>] + to + [, ] + + TESTS: + + A non-example because the degree is not well-defined:: + + sage: M = FreeGradedModule(A, (0, 0)) + sage: N = FreeGradedModule(A, (0,)) + sage: H = Hom(M, N) + sage: g = N.generator(0) + sage: H([Sq(1)*g, Sq(2)*g]) + Traceback (most recent call last): + ... + ValueError: ill-defined homomorphism: degrees do not match + """ + + def __init__(self, parent, values): + r""" + Create a homomorphism between finitely generated free graded modules. + """ + if not is_FreeGradedModuleHomspace(parent): + raise TypeError('the parent (%s) must be a f.p. free module homset' % parent) + + # Get the values. + C = parent.codomain() + D = parent.domain() + if isfunction(values): + _values = [C(values(g)) for g in D.generators()] + elif values == 0: + _values = len(D.generator_degrees())*[C(0)] + else: + _values = [C(a) for a in values] + + # Check the homomorphism is well defined. + if len(D.generator_degrees()) != len(_values): + raise ValueError('the number of values must equal the number of '\ + 'generators in the domain. Invalid argument: %s' % _values) + + # Compute the degree. + if all(v.is_zero() for v in _values): + # The zero homomorphism does not get a degree. + _degree = None + else: + degrees = [] + for i, value in enumerate(_values): + if not value.is_zero(): + x = value.degree() + xx = D.generator_degrees()[i] + degrees.append(x-xx) + + _degree = min(degrees) + if _degree != max(degrees): + raise ValueError('ill-defined homomorphism: degrees do not match') + + self._degree = _degree + self._values = _values + + Morphism.__init__(self, parent) + + + def degree(self): + r""" + The degree of this homomorphism. + + OUTPUT: The degree of this homomorphism. Raise an error if this is + the trivial homomorphism. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: A = SteenrodAlgebra(2) + sage: homspace = Hom(FreeGradedModule(A, (0,1)), FreeGradedModule(A, (0,))) + sage: N = homspace.codomain() + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: f = homspace(values) + sage: f.degree() + 5 + + The zero homomorphism has no degree:: + + sage: homspace.zero().degree() + Traceback (most recent call last): + ... + ValueError: the zero morphism does not have a well-defined degree + """ + if self._degree is None: + # The zero morphism has no degree. + raise ValueError("the zero morphism does not have a well-defined degree") + return self._degree + + + def values(self): + r""" + The values under this homomorphism corresponding to the generators of + the domain module. + + OUTPUT: A sequence of elements of the codomain module. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: A = SteenrodAlgebra(2) + sage: homspace = Hom(FreeGradedModule(A, (0,1)), FreeGradedModule(A, (2,))) + sage: N = homspace.codomain() + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: f = homspace(values) + sage: f.values() + [, ] + sage: homspace.zero().values() + [<0>, <0>] + """ + return self._values + + + def _richcmp_(self, other, op): + r""" + Compare this homomorphism to the given homomorphism. + + INPUT: + + - ``other`` -- An instance of this class. + + - ``op`` -- An integer specifying the comparison operation to be + carried out: If ``op`` == 2, then return ``True`` if and only if the + homomorphisms are equal. If ``op`` == 3, then return ``True `` if + and only if the homomorphisms are not equal. Otherwise, + return ``False``. + + OUTPUT: A boolean. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: A = SteenrodAlgebra(2) + sage: homspace = Hom(FreeGradedModule(A, (0,1)), FreeGradedModule(A, (2,))) + sage: N = homspace.codomain() + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: f = homspace(values) + sage: f._richcmp_(f, op=2) + True + sage: f._richcmp_(f, op=3) + False + """ + + try: + same = (self - other).is_zero() + except ValueError: + return False + + # Equality + if op == 2: + return same + + # Non-equality + if op == 3: + return not same + + return False + + + def _add_(self, g): + r""" + The pointwise sum of this and the given homomorphism. + + Pointwise addition of two homomorphisms `f` and `g` with the same domain + and codomain is given by the formula `(f+g)(x) = f(x) + g(x)` for + every `x` in the domain of `f`. + + INPUT: + + - ``g`` -- A homomorphism with the same domain and codomain as this + homomorphism. + + OUTPUT: The pointwise sum homomorphism of this and the given + homomorphism. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: A = SteenrodAlgebra(2) + sage: homspace = Hom(FreeGradedModule(A, (0,1)), FreeGradedModule(A, (2,))) + sage: N = homspace.codomain() + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: f = homspace(values) + sage: ff = f.__add__(f) + sage: ff.is_zero() + True + sage: ff.__add__(f) == f + True + sage: ff = f + f + sage: ff.is_zero() + True + """ + if self.domain() != g.domain(): + raise ValueError('morphisms do not have the same domain') + elif self.codomain() != g.codomain(): + raise ValueError('morphisms do not have the same codomain') + elif self.is_zero(): + return g + elif g.is_zero(): + return self + elif self.degree() and g.degree() and self.degree() != g.degree(): + raise ValueError('morphisms do not have the same degree') + + v = [self(x) + g(x) for x in self.domain().generators()] + return self.parent()(v) + + + def _neg_(self): + r""" + The additive inverse of this homomorphism with respect to the group + structure given by pointwise sum. + + OUTPUT: An instance of this class. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: A = SteenrodAlgebra(2) + sage: homspace = Hom(FreeGradedModule(A, (0,1)), FreeGradedModule(A, (2,))) + sage: N = homspace.codomain() + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: f = homspace(values) + sage: f_inverse = -f; f_inverse + Module homomorphism of degree 7 defined by sending the generators + [<1, 0>, <0, 1>] + to + [, ] + sage: (f + f_inverse).is_zero() + True + """ + return self.parent()([-x for x in self.values()]) + + + def _sub_(self, g): + r""" + The pointwise difference between this and the given homomorphism. + + OUTPUT: An instance of this class. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: A = SteenrodAlgebra(2) + sage: homspace = Hom(FreeGradedModule(A, (0,1)), FreeGradedModule(A, (2,))) + sage: N = homspace.codomain() + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: f = homspace(values) + sage: values2 = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: g = homspace(values2) + sage: f - g + The trivial homomorphism. + """ + return self + (-g) + + + # Define __mul__ rather than _mul_, since we want to allow + # "multiplication" by morphisms from different homsets. + def __mul__(self, g): + r""" + The composition of the given homomorphism ``g``, followed by this + homomorphism. + + OUTPUT: A homomorphism from the domain of this homomorphism, into the + codomain of the homomorphism ``g``. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: A = SteenrodAlgebra(2) + sage: M = FreeGradedModule(A, (0,1)) + sage: N = FreeGradedModule(A, (2,)) + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: f = Hom(M, N)(values) + sage: values2 = [Sq(2)*M.generator(0)] + sage: g = Hom(N, M)(values2) + sage: fg = f * g; fg + Module homomorphism of degree 7 defined by sending the generators + [<1>] + to + [] + sage: fg.is_endomorphism() + True + + TESTS: + + sage: fg == f.__mul__(g) + True + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: A = SteenrodAlgebra(2) + sage: M = FreeGradedModule(A, (0,1)) + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: f = Hom(M, N)(values) + sage: f * f + Traceback (most recent call last): + ... + ValueError: morphisms are not composable + """ + if self.parent().domain() != g.parent().codomain(): + raise ValueError('morphisms are not composable') + homset = Hom(g.parent().domain(), self.parent().codomain()) + return homset([self(g(x)) for x in g.domain().generators()]) + + + def is_zero(self): + r""" + Decide if this homomomorphism is trivial. + + OUTPUT: The boolean value ``True`` if this homomorphism is trivial, and + ``False`` otherwise. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: A = SteenrodAlgebra(2) + sage: M = FreeGradedModule(A, (0,1)) + sage: N = FreeGradedModule(A, (2,)) + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: f = Hom(M, N)(values) + sage: f.is_zero() + False + sage: (f-f).is_zero() + True + """ + return all(v.is_zero() for v in self.values()) + + + def is_identity(self): + r""" + Decide if this homomomorphism is the identity endomorphism. + + OUTPUT: The boolean value ``True`` if this homomorphism is the + identity, and ``False`` otherwise. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: A = SteenrodAlgebra(2) + sage: M = FreeGradedModule(A, (0,1)) + sage: N = FreeGradedModule(A, (2,)) + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: f = Hom(M, N)(values) + sage: f.is_identity() + False + sage: id = Hom(M, M)(M.generators()); id + The identity homomorphism. + sage: id.is_identity() + True + """ + if self.parent().is_endomorphism_set(): + return self.parent().identity() == self + else: + return False + + + def __call__(self, x): + r""" + Evaluate the homomorphism at the given domain element ``x``. + + INPUT: + + - ``x`` -- An element of the domain of this morphism. + + OUTPUT: The module element of the codomain which is the value of ``x`` + under this homomorphism. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: A = SteenrodAlgebra(2) + sage: M = FreeGradedModule(A, (0,1)) + sage: N = FreeGradedModule(A, (2,)) + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: f = Hom(M, N)(values) + sage: f.__call__(M.generator(0)) + + sage: f.__call__(M.generator(1)) + + """ + if x.parent() != self.domain(): + raise ValueError('cannot evaluate morphism on element not in the domain') + + value = sum([c*v for c, v in zip( + x.dense_coefficient_list(), self.values())], self.codomain()(0)) + + return value + + + def _repr_(self): + r""" + A string representation of this homomorphism. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: A = SteenrodAlgebra(2) + sage: M = FreeGradedModule(A, (0,1)) + sage: N = FreeGradedModule(A, (2,)) + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + + sage: Hom(M, N)(values)._repr_() + 'Module homomorphism of degree 7 defined by sending the generators\n [<1, 0>, <0, 1>]\nto\n [, ]' + + sage: Hom(M, N).zero()._repr_() + 'The trivial homomorphism.' + + sage: Hom(M, M).identity()._repr_() + 'The identity homomorphism.' + """ + if self.is_zero(): + return "The trivial homomorphism." + elif self.is_identity(): + return "The identity homomorphism." + else: + r = "Module homomorphism of degree {} defined by sending the generators\n {}\nto\n {}" + return r.format(self.degree(), self.domain().generators(), self.values()) + + + def vector_presentation(self, n): + r""" + The restriction of this homomorphism to the domain module elements of + degree ``n``. + + The restriction of a non-zero module homomorphism to the vector space of + module elements of degree `n` is a linear function into the vector space + of elements of degree `n+d` belonging to the codomain. Here `d` is the + degree of this homomorphism. + + When this homomorphism is zero, it has no well defined degree so the + function cannot be presented since we do not know the degree of its + codomain. In this case, an error is raised. + + INPUT: + + - ``n`` -- An integer degree. + + OUTPUT: A linear function of finite dimensional vector spaces over the + ground field of the algebra for this module. The domain is isomorphic + to the vector space of domain elements of degree ``n`` of this free + module, via the choice of basis given by + :meth:`sage.modules.fp_graded.free_module.FreeGradedModule.basis_elements`. + If the morphism is zero, an error is raised. + + .. SEEALSO:: + + :meth:`sage.modules.fp_graded.free_module.FreeGradedModule.vector_presentation`, + :meth:`sage.modules.fp_graded.free_module.FreeGradedModule.basis_elements`. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import FreeGradedModule + sage: A = SteenrodAlgebra(2) + sage: M = FreeGradedModule(A, (0,1)) + sage: N = FreeGradedModule(A, (2,)) + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: f = Hom(M, N)(values) + sage: f.vector_presentation(0) + Vector space morphism represented by the matrix: + [0 1] + Domain: Vector space of dimension 1 over Finite Field of size 2 + Codomain: Vector space of dimension 2 over Finite Field of size 2 + sage: f.vector_presentation(1) + Vector space morphism represented by the matrix: + [0 0 0] + [0 1 0] + Domain: Vector space of dimension 2 over Finite Field of size 2 + Codomain: Vector space of dimension 3 over Finite Field of size 2 + sage: f.vector_presentation(2) + Vector space morphism represented by the matrix: + [0 0 1 1] + [0 0 0 0] + Domain: Vector space of dimension 2 over Finite Field of size 2 + Codomain: Vector space of dimension 4 over Finite Field of size 2 + + TESTS: + + sage: F = FreeGradedModule(A, (0,)) + sage: z = Hom(F, F)([0]) + sage: z.is_zero() + True + sage: z.vector_presentation(0) + Traceback (most recent call last): + ... + ValueError: the zero map has no vector presentation + """ + # The trivial map has no degree, so we can not create the codomain + # of the linear transformation. + if self.is_zero(): + raise ValueError("the zero map has no vector presentation") + + D_n = self.domain().vector_presentation(n) + C_n = self.codomain().vector_presentation(n + self.degree()) + + values = [self(e) for e in self.domain().basis_elements(n)] + return Hom(D_n, C_n)([ + C_n.zero() if e.is_zero() else e.vector_presentation() for e in values]) + + def to_fp_module(self): + r""" + Create a finitely presented module from this morphism. + + OUTPUT: The finitely presented module having presentation + equal to this morphism. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import * + sage: A = SteenrodAlgebra(2) + sage: F1 = FreeGradedModule(A, (2,)) + sage: F2 = FreeGradedModule(A, (0,)) + sage: v = F2([Sq(2)]) + sage: pres = Hom(F1, F2)([v]) + sage: M = pres.to_fp_module(); M + Finitely presented left module on 1 generator and 1 relation over mod 2 Steenrod algebra, milnor basis + sage: M.generator_degrees() + (0,) + sage: M.relations() + [] + """ + from .module import FP_Module + return FP_Module(algebra=self.base_ring(), + generator_degrees=self.codomain().generator_degrees(), + relations=tuple([r.dense_coefficient_list() for r in self.values()])) diff --git a/src/sage/modules/fp_graded/homspace.py b/src/sage/modules/fp_graded/homspace.py new file mode 100755 index 00000000000..dcb17f191b6 --- /dev/null +++ b/src/sage/modules/fp_graded/homspace.py @@ -0,0 +1,521 @@ +r""" +The set of homomorphisms of finitely presented graded modules + +This class implements methods for construction and basic +manipulation of homsets of finitely presented graded modules over a connected +graded `k`-algebra, where `k` is a field. + +.. NOTE:: This class is intended for private use by + :class:`sage.modules.fp_steenrod.fpa_homspace.FPA_ModuleHomspace`. + +TESTS: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: from sage.misc.sage_unittest import TestSuite + sage: A = SteenrodAlgebra(2, profile=(3,2,1)) + sage: F = FP_Module(A, [1,3]) + sage: L = FP_Module(A, [2,3], [[Sq(2),Sq(1)], [0,Sq(2)]]) + sage: homset = Hom(F, L); homset + Set of Morphisms from Finitely presented left module on 2 generators ... + sage: homset.an_element() + Module homomorphism of degree 0 defined by sending the generators + [<1, 0>, <0, 1>] + to + [<0, 0>, ] + sage: homset([L((A.Sq(1), 1)), L((0, A.Sq(2)))]) + Module homomorphism of degree 2 defined by sending the generators + [<1, 0>, <0, 1>] + to + [, <0, Sq(2)>] + sage: Hom(F, L) ([L((A.Sq(1), 1)), L((0, A.Sq(2)))]).kernel() + Module homomorphism of degree 0 defined by sending the generators + [<1, 0>, <0, 1>] + to + (<0, 1>, ) + +AUTHORS: + +- Robert R. Bruner, Michael J. Catanzaro (2012): Initial version. +- Sverre Lunoee--Nielsen and Koen van Woerden (2019-11-29): Updated the + original software to Sage version 8.9. +- Sverre Lunoee--Nielsen (2020-07-01): Refactored the code and added + new documentation and tests. +""" + +#***************************************************************************** +# Copyright (C) 2011 Robert R. Bruner and +# Michael J. Catanzaro +# +# 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 __future__ import absolute_import + +from sage.categories.homset import Homset + +from sage.categories.homset import Hom + +def is_FP_ModuleHomspace(x): + r""" + Check if the given object is of type FP_ModuleHomspace. + + OUTPUT: A boolean which is True if ``x`` is of type FP_ModuleHomspace. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: from sage.modules.fp_graded.homspace import is_FP_ModuleHomspace + sage: A2 = SteenrodAlgebra(2, profile=(3,2,1)) + sage: F = FP_Module(A2, [1,3]) + sage: L = FP_Module(A2, [2,3], [[Sq(2),Sq(1)], [0,Sq(2)]]) + sage: is_FP_ModuleHomspace(Hom(F, L)) + True + sage: is_FP_ModuleHomspace(0) + False + """ + return isinstance(x, FP_ModuleHomspace) + + +class FP_ModuleHomspace(Homset): + # FP_ModuleMorphism contains reference to is_FP_ModuleHomspace, so this import + # statement must not appear before that function. + from .morphism import FP_ModuleMorphism + + # In the category framework, Elements of the class FP_ModuleHomspace are of the + # class FP_ModuleMorphism, see + # http://doc.sagemath.org/html/en/thematic_tutorials/coercion_and_categories.html#implementing-the-category-framework-for-the-elements + Element = FP_ModuleMorphism + + def _element_constructor_(self, values): + r""" + Constructs a morphism contained in this homset. + + This function is not part of the public API, but is used by :meth:Hom + method to create morphisms. + + INPUT: + + - ``values`` -- An iterable of FP_Elements of the codomain. + + OUTPUT: A module homomorphism in this homspace sending the generators + of the domain module to the given values. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A2 = SteenrodAlgebra(2, profile=(3,2,1)) + sage: F = FP_Module(A2, [1,3]) + sage: L = FP_Module(A2, [2,3], [[Sq(2),Sq(1)], [0,Sq(2)]]) + + sage: homset = Hom(F, L) + sage: v1 = L([A2.Sq(1), 1]) + sage: v2 = L([0, A2.Sq(2)]) + sage: f = homset._element_constructor_([v1, v2]) + + Rather than calling ``_element_constructor_`` explicitly, one + can call it implicitly, using the ``call`` syntax:: + + sage: f = homset([v1, v2]); f + Module homomorphism of degree 2 defined by sending the generators + [<1, 0>, <0, 1>] + to + [, <0, Sq(2)>] + + One can construct a homomorphism from another homomorhism:: + + sage: g = homset(f) + sage: f == g + True + + And there is a convenient way of making the trivial homomorphism:: + + sage: z = homset(0); z + The trivial homomorphism. + """ + if isinstance(values, self.element_class): + return values + elif values == 0: + return self.zero() + else: + return self.element_class(self, values) + + + def an_element(self, n=0): + r""" + Create a homomorphism belonging to this homset. + + INPUT: + + - ``n`` -- an integer degree. (optional, default: 0) + + OUTPUT: A module homomorphism of degree ``n``. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A = SteenrodAlgebra(2) + sage: HZ = FP_Module(A, [0], relations=[[Sq(1)]]) + + sage: Hom(HZ, HZ).an_element(3) + Module homomorphism of degree 3 defined by sending the generators + [<1>] + to + [] + + TESTS: + + sage: K = FP_Module(A, [0, 0], [[Sq(2), 0]]) # Using a zero coefficient in the relations. + sage: Hom(K, K).an_element(4) + Module homomorphism of degree 4 defined by sending the generators + [<1, 0>, <0, 1>] + to + [<0, 0>, ] + + sage: K = FP_Module(A, [0, 0], [[Sq(2), 0], [0,0], [Sq(4), Sq(2)*Sq(2)]]) + sage: Hom(K, K).an_element(n=3) + Module homomorphism of degree 3 defined by sending the generators + [<1, 0>, <0, 1>] + to + [<0, 0>, ] + """ + return self._basis_elements(n, basis=False) + + + def basis_elements(self, n): + r""" + Compute a basis for the vector space of degree ``n`` morphisms. + + INPUT: + + - ``n`` -- an integer degree. + + OUTPUT: A basis for the set of all module homomorphisms of degree ``n``. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A = SteenrodAlgebra(2) + sage: Hko = FP_Module(A, [0], relations=[[Sq(2)], [Sq(1)]]) + + sage: Hom(Hko, Hko).basis_elements(21) + [Module homomorphism of degree 21 defined by sending the generators + [<1>] + to + [], + Module homomorphism of degree 21 defined by sending the generators + [<1>] + to + []] + """ + return self._basis_elements(n, basis=True) + + + def zero(self): + r""" + Create the trivial homomorphism in this homset. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A2 = SteenrodAlgebra(2, profile=(3,2,1)) + sage: F = FP_Module(A2, [1,3]) + sage: L = FP_Module(A2, [2,3], [[Sq(2),Sq(1)], [0,Sq(2)]]) + + sage: z = Hom(F, L).zero(); z + The trivial homomorphism. + + sage: z(F.an_element(5)) + <0, 0> + + sage: z(F.an_element(23)) + <0, 0> + """ + return self.element_class(self, [self.codomain().zero() for g in self.domain().generator_degrees()]) + + + def identity(self): + r""" + Create the identity homomorphism. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A2 = SteenrodAlgebra(2, profile=(3,2,1)) + sage: L = FP_Module(A2, [2,3], [[Sq(2),Sq(1)], [0,Sq(2)]]) + + sage: id = Hom(L, L).identity(); id + The identity homomorphism. + + sage: e = L.an_element(5) + sage: e == id(e) + True + + It is an error to call this function when the homset is not a + set of endomorphisms:: + + sage: F = FP_Module(A2, [1,3]) + sage: Hom(F,L).identity() + Traceback (most recent call last): + ... + TypeError: this homspace does not consist of endomorphisms + """ + if self.is_endomorphism_set(): + return self.element_class(self, self.codomain().generators()) + else: + raise TypeError('this homspace does not consist of endomorphisms') + + + def _basis_elements(self, n, basis): + r""" + Compute a basis for the vector space of degree ``n`` homomorphisms. + + This function is private and used by :meth:`basis_elements` and + :meth:`an_element`. + + INPUT: + + - ``n`` -- an integer degree. + - ``basis`` -- boolean to decide if a basis should be returned, or just + a single homomorphism. + + OUTPUT: A basis for the set of all module homomorphisms of degree ``n`` + if ``basis`` is True. Otherwise a single element is returned. In the + latter case, this homomorphism is non-trivial if the vector space of all + homomorphisms is non-trivial. + + TESTS: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A = SteenrodAlgebra(2) + sage: Hko = FP_Module(A, [0], relations=[[Sq(2)], [Sq(1)]]) + sage: Hom(Hko, Hko)._basis_elements(21, basis=True) + [Module homomorphism of degree 21 defined by sending the generators + [<1>] + to + [], + Module homomorphism of degree 21 defined by sending the generators + [<1>] + to + []] + + sage: Hom(Hko, Hko)._basis_elements(21, basis=False) + Module homomorphism of degree 21 defined by sending the generators + [<1>] + to + [] + + sage: F = FP_Module(A, [0]) + sage: Hom(F, Hko)._basis_elements(21, basis=False) + Module homomorphism of degree 21 defined by sending the generators + [<1>] + to + [] + + sage: Hom(F, Hko)._basis_elements(21, basis=False) + Module homomorphism of degree 21 defined by sending the generators + [<1>] + to + [] + + Hom(FPA_Module([0], A, [[Sq(1)]]), FPA_Module([-2], A, [[Sq(1)]])).an_element(0) + The trivial homomorphism. + + Test corner cases involving trivial modules: + + sage: F = FP_Module(A, [0]) # A module without relations. + sage: Z0 = FP_Module(A, []) # A trivial module. + sage: Z1 = FP_Module(A, [0], [[1]]) # A trivial module with a redundant generator and relation. + + Hom(FPA_Module([-1], A), F)._basis_elements(0, basis=True) + [] + Hom(FPA_Module([-1], A), F)._basis_elements(0, basis=False) + The trivial homomorphism. + + sage: from itertools import product + sage: for D,C in product([(F, 'Free'), (Hko, 'Hko'), (Z0, 'Trivial'), (Z1, 'Trivial with redundant generator')], repeat=2): + ....: print('Hom(%s, %s):' % (D[1], C[1])) + ....: print(' basis==False:\n %s' % Hom(D[0], C[0])._basis_elements(n=7, basis=False)) + ....: print(' basis==True:\n %s' % Hom(D[0], C[0])._basis_elements(n=7, basis=True)) + Hom(Free, Free): + basis==False: + Module homomorphism of degree 7 defined by sending the generators + [<1>] + to + [] + basis==True: + [Module homomorphism of degree 7 defined by sending the generators + [<1>] + to + [], Module homomorphism of degree 7 defined by sending the generators + [<1>] + to + [], Module homomorphism of degree 7 defined by sending the generators + [<1>] + to + [], Module homomorphism of degree 7 defined by sending the generators + [<1>] + to + []] + Hom(Free, Hko): + basis==False: + Module homomorphism of degree 7 defined by sending the generators + [<1>] + to + [] + basis==True: + [Module homomorphism of degree 7 defined by sending the generators + [<1>] + to + []] + Hom(Free, Trivial): + basis==False: + The trivial homomorphism. + basis==True: + [] + Hom(Free, Trivial with redundant generator): + basis==False: + The trivial homomorphism. + basis==True: + [] + Hom(Hko, Free): + basis==False: + The trivial homomorphism. + basis==True: + [] + Hom(Hko, Hko): + basis==False: + Module homomorphism of degree 7 defined by sending the generators + [<1>] + to + [] + basis==True: + [Module homomorphism of degree 7 defined by sending the generators + [<1>] + to + []] + Hom(Hko, Trivial): + basis==False: + The trivial homomorphism. + basis==True: + [] + Hom(Hko, Trivial with redundant generator): + basis==False: + The trivial homomorphism. + basis==True: + [] + Hom(Trivial, Free): + basis==False: + The trivial homomorphism. + basis==True: + [] + Hom(Trivial, Hko): + basis==False: + The trivial homomorphism. + basis==True: + [] + Hom(Trivial, Trivial): + basis==False: + The trivial homomorphism. + basis==True: + [] + Hom(Trivial, Trivial with redundant generator): + basis==False: + The trivial homomorphism. + basis==True: + [] + Hom(Trivial with redundant generator, Free): + basis==False: + The trivial homomorphism. + basis==True: + [] + Hom(Trivial with redundant generator, Hko): + basis==False: + The trivial homomorphism. + basis==True: + [] + Hom(Trivial with redundant generator, Trivial): + basis==False: + The trivial homomorphism. + basis==True: + [] + Hom(Trivial with redundant generator, Trivial with redundant generator): + basis==False: + The trivial homomorphism. + basis==True: + [] + """ + from .morphism import _CreateRelationsMatrix + + M = self.domain() + N = self.codomain() + + def _trivial_case(): + ''' + The return value if there are no non-trivial homomorphisms. + ''' + if basis: + # Since the vector space of homomorphisms is trivial, the basis + # is the empty set. + return [] + else: + # Since the vector space of homomorphisms is trivial, it contains + # only the trivial homomorphism. + return self.zero() + + # Deal with the trivial cases first. Note that this covers the case + # where the domain or codomain have no generators. + if N.is_trivial() or M.is_trivial(): + return _trivial_case() + + # Then deal with the case where the domain has no relations. + elif not M.has_relations(): + res = [] + num_generators = len(M.generators()) + for i, g in enumerate(M.generators()): + # The i'th generator can go to any of these basis elements: + base = N[(g.degree() + n)] + for value in base: + values = [N.zero() if i != j else value for j in range(num_generators)] + res.append(Hom(M,N)(values)) + if not basis: + return res[0] + + else: + # Note that this list is non-empty since we dealt with the trivial + # case above. + source_degs = [g.degree() + n for g in M.generators()] + + # Note that this list is non-empty since we dealt with the free + # case above. + target_degs = [r.degree() + n for r in M.relations()] + + block_matrix, R = _CreateRelationsMatrix( + N, [r.dense_coefficient_list() for r in M.relations()], source_degs, target_degs) + + ker = R.right_kernel() + + res = [] + for b in ker.basis(): + n = 0 + + xs = [] + for j,X in enumerate(block_matrix[0]): + k = X.domain().dimension() + xs.append(N.element_from_coordinates(b[n:n+k], source_degs[j])) + n += k + + res.append(Hom(M, N)(xs)) + if not basis: + return res[0] + + # If the code above found a non-trivial homomorphism and ``basis==False``, + # it will have terminated by now. + if len(res) == 0: + return _trivial_case() + else: + return res + diff --git a/src/sage/modules/fp_graded/module.py b/src/sage/modules/fp_graded/module.py new file mode 100755 index 00000000000..c347a6cb2c4 --- /dev/null +++ b/src/sage/modules/fp_graded/module.py @@ -0,0 +1,1087 @@ +r""" +Finitely presented graded modules + +This class implements methods for construction and basic manipulation of +finitely presented graded modules over connected graded algebras. + +.. NOTE:: This class was designed for use by + :class:`sage.modules.fp_graded.fpa_module.FPA_Module`. + As a consequence, all tests and examples consider modules over the + the Steenrod algebra (or a finite sub-Hopf algebra of it). + + However, this class does not assume that the algebra is the Steenrod + algebra and could be a starting point for developers wanting to extend + Sage further. + +============== +Implementation +============== + +Let `R` be a connected graded algebra. A finitely presented module over `R` +is isomorphic to the cokernel of an `R`-linear homomorphism `f:F_1 \to F_0` +of finitely generated free modules: The generators of `F_0` corresponds to the +generators of the module, and the generators of `F_1` corresponds to its +relations, via the map `f`. + +The class constructor of this module class is given a set of generators and +relations, and uses them to construct a presentation, using the class +:class:`sage.modules.fp_graded.free_morphism.FreeGradedModuleMorphism`. + +This package was designed with homological algebra in mind, and its API +focuses on maps rather than objects. A good example of this is the kernel +function :meth:`sage.modules.fp_graded.morphism.FP_ModuleMorphism.kernel` +which computes the kernel of a homomorphism `f: M\to N`. Its return value is +not an instance of the module class, but rather an injective homomorphism +`i: K\to M` with the property that `\operatorname{im}(i) = \ker(f)`. + +AUTHORS: + +- Robert R. Bruner, Michael J. Catanzaro (2012): Initial version. +- Sverre Lunoee--Nielsen and Koen van Woerden (2019-11-29): Updated the + original software to Sage version 8.9. +- Sverre Lunoee--Nielsen (2020-07-01): Refactored the code and added + new documentation and tests. +""" + +#***************************************************************************** +# Copyright (C) 2011 Robert R. Bruner and +# Michael J. Catanzaro +# +# 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.categories.homset import Hom +from sage.misc.cachefunc import cached_method +from sage.rings.infinity import PlusInfinity +from sage.categories.graded_modules_with_basis import GradedModulesWithBasis +from sage.combinat.free_module import CombinatorialFreeModule + +from .free_module import FreeGradedModule +from .free_element import FreeGradedModuleElement +from .element import FP_Element + +# These are not free modules over the algebra, but they are free as +# vector spaces. They have a distinguished set of generators over the +# algebra, and as long as the algebra has a vector space basis +# implemented in Sage, the modules will have a vector space basis as well. +class FP_Module(CombinatorialFreeModule): + r""" + Create a finitely presented module over a connected graded algebra. + + INPUT: + + - ``algebra`` -- The algebra over which the module is defined. + + - ``generator_degrees`` -- A tuple of integer degrees. + + - ``relations`` -- A tuple of relations. A relation is a tuple of + coefficients `(c_1, \ldots, c_n)`, ordered so that they + correspond to the module generators. + + OUTPUT: The finitely presented module over ``algebra`` with + presentation given by ``generator_degrees`` and ``relations``. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A3 = SteenrodAlgebra(2, profile=(4,3,2,1)) + + sage: M = FP_Module(A3, [0, 1], [[Sq(2), Sq(1)]]) + sage: M.generators() + [<1, 0>, <0, 1>] + sage: M.relations() + [] + sage: M.is_trivial() + False + + sage: Z = FP_Module(A3, []) + sage: Z.generators() + [] + sage: Z.relations() + [] + sage: Z.is_trivial() + True + """ + @staticmethod + def __classcall_private__(cls, algebra, generator_degrees, relations=()): + r""" + Normalize input to ensure a unique representation. + + INPUT: + + - ``generator_degrees`` -- an iterable of integer degrees. + + - ``algebra`` -- the connected graded algebra over which the module is defined. + + - ``relations`` -- an iterable of relations. A relation is a tuple of + coefficients `(c_1, \ldots, c_n)` corresponding to the module + generators. + + OUTPUT: The finitely presented module with presentation given by + the ``generator_degrees`` and ``relations``. + + TESTS: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A3 = SteenrodAlgebra(2, profile=(4,3,2,1)) + sage: FP_Module(A3, [0, 1], [[Sq(2), Sq(1)]]) + Finitely presented left module on 2 generators and 1 relation over sub-Hopf algebra of mod 2 Steenrod algebra, milnor basis, profile function [4, 3, 2, 1] + """ + return super(FP_Module, cls).__classcall__(cls, + algebra=algebra, + generator_degrees=tuple(generator_degrees), + relations=tuple([tuple([algebra(x) for x in r]) for r in relations])) + + + def __init__(self, algebra, generator_degrees, relations=()): + r""" + Create a finitely presented module over a connected graded algebra. + """ + self._generator_degrees = generator_degrees + self._relations = relations + # if generator_degrees is [d_0, d_1, ...], then + # the generators are indexed by (0,d_0), (1,d_1), ... + keys = [(i,deg) for i,deg in enumerate(generator_degrees)] + self._generator_keys = keys + + # The free module on the generators of the module. + generatorModule = FreeGradedModule(algebra, + generator_degrees) + # Use the coefficients given for the relations and make module elements + # from them. Filter out the zero elements, as they are redundant. + rels = [v for v in [generatorModule(r) for r in relations] if not v.is_zero()] + + # The free module for the relations of the module. + relationsModule = FreeGradedModule(algebra, + tuple([r.degree() for r in rels])) + + # The module we want to model is the cokernel of the + # following morphism. + self.j = Hom(relationsModule, generatorModule)(rels) + + # Call the base class constructor. + CombinatorialFreeModule.__init__(self, algebra, + basis_keys=keys, + element_class=FP_Element, + category=GradedModulesWithBasis(algebra)) + + + def _free_module(self): + """ + The free module of which this is a quotient + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A = SteenrodAlgebra() + sage: M = FP_Module(A, [0, 1], [[Sq(2), Sq(1)]]) + sage: M.generators() + [<1, 0>, <0, 1>] + sage: F = M._free_module() + sage: F.generators() + [<1, 0>, <0, 1>] + """ + return self.j.codomain() + + @classmethod + def from_free_module(cls, free_module): + r""" + Initialize from a finitely generated free module. + + INPUT: + + - ``free_module`` -- a finitely generated free module. + + OUTPUT: the finitely presented module having same set of generators + as ``free_module``, and no relations. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import * + sage: from sage.modules.fp_graded.module import * + sage: A = SteenrodAlgebra(2) + sage: F = FreeGradedModule(A, (-2,2,4)) + sage: FP_Module.from_free_module(F) + Finitely presented left module on 3 generators and 0 relations over mod 2 Steenrod algebra, milnor basis + """ + return cls(algebra=free_module.base_ring(), + generator_degrees=free_module.generator_degrees(), + relations=()) + + + @classmethod + def from_free_module_morphism(cls, morphism): + r""" + Create a finitely presented module from a morphism of finitely + generated free modules. + + INPUT: + + - ``morphism`` -- a morphism between finitely generated free modules. + + OUTPUT: + + The finitely presented module having presentation equal to the + homomorphism ``morphism``. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.free_module import * + sage: from sage.modules.fp_graded.module import * + sage: A = SteenrodAlgebra(2) + sage: F1 = FreeGradedModule(A, (2,)) + sage: F2 = FreeGradedModule(A, (0,)) + sage: v = F2([Sq(2)]) + sage: pres = Hom(F1, F2)([v]) + sage: M = FP_Module.from_free_module_morphism(pres); M + Finitely presented left module on 1 generator and 1 relation over mod 2 Steenrod algebra, milnor basis + sage: M.generator_degrees() + (0,) + sage: M.relations() + [] + """ + return cls(algebra=morphism.base_ring(), + generator_degrees=morphism.codomain().generator_degrees(), + relations=tuple([r.dense_coefficient_list() for r in morphism.values()])) + + + def change_ring(self, algebra): + r""" + Change the base ring of this module. + + INPUT: + + - ``algebra`` -- a connected graded algebra. + + OUTPUT: The finitely presented module over ``algebra`` defined with the + exact same number of generators of the same degrees and relations as + this module. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A = SteenrodAlgebra(2) + sage: A2 = SteenrodAlgebra(2,profile=(3,2,1)) + + sage: M = FP_Module(A, [0,1], [[Sq(2), Sq(1)]]) + sage: M_ = M.change_ring(A2); M_ + Finitely presented left module on 2 generators and 1 relation over sub-Hopf algebra of mod 2 Steenrod algebra, milnor basis, profile function [3, 2, 1] + + sage: # Changing back yields the original module. + sage: M_.change_ring(A) is M + True + """ + # self.relations() consists of module elements. We need to extra the coefficients. + relations = tuple(r.dense_coefficient_list() for r in self.relations()) + return FP_Module(algebra, self.generator_degrees(), relations) + + + def _element_constructor_(self, x): + r""" + Construct any element of this module. + + This function is used internally by the ()-method when creating + module elements, and should not be called by the user explicitly. + + INPUT: + + - ``x`` -- A tuple of coefficients, an element of FP_Module, or the + zero integer constant. + + OUTPUT: An instance of the element class with coefficients from ``x``, + the element ``x`` if it already was an element, or the zero element. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A = SteenrodAlgebra(2) + sage: M = FP_Module(A, [0,2,4], [[Sq(4), Sq(2), 0]]) + + sage: # Creating an element from coefficients: + sage: e = M((Sq(6), 0, Sq(2))); e + + sage: e in M + True + + sage: # Special syntax for creating the zero element: + sage: z = M(0); z + <0, 0, 0> + sage: z.is_zero() + True + + sage: # Creating an element from another element returns a reference to itself: + sage: M(e) + + sage: e is M(e) + True + """ + if isinstance(x, self.element_class): + return x + if not x: + return self.zero() + B = self.basis() + if isinstance(x, FreeGradedModuleElement): + if x.parent() == self._free_module(): + # x.parent() should have the same generator list as self. + coeffs = x.monomial_coefficients() + return sum(coeffs[idx]*B[idx] for idx in coeffs) + raise ValueError("element is not in this module") + return sum(c*B[b] for (c,b) in zip(x, self._generator_keys)) + + + def _repr_(self): + r""" + Construct a string representation of the module. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A = SteenrodAlgebra(2) + sage: M = FP_Module(A, [0,2,4], [[Sq(4),Sq(2),0]]); M + Finitely presented left module on 3 generators and 1 relation over mod 2 Steenrod algebra, milnor basis + sage: N = FP_Module(A, [0,1], [[Sq(2),Sq(1)], [Sq(2)*Sq(1),Sq(2)]]); N + Finitely presented left module on 2 generators and 2 relations over mod 2 Steenrod algebra, milnor basis + sage: F = FP_Module(A, [2]); F + Finitely presented left module on 1 generator and 0 relations over mod 2 Steenrod algebra, milnor basis + """ + return "Finitely presented left module on %s generator%s and %s relation%s over %s"\ + %(len(self._free_module().generator_degrees()), "" if len(self._free_module().generator_degrees()) == 1 else "s", + len(self.j.values()), "" if len(self.j.values()) == 1 else "s", + self.base_ring()) + + + def connectivity(self): + r""" + The connectivity of this module. + + Since a finitely presented module over a connected algebra is in + particular bounded below, the connectivity is an integer when the + module is non-trivial, and `+\infty` when the module is trivial. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A = SteenrodAlgebra(2) + + sage: M = FP_Module(A, [0,2,4], [[0, Sq(5), Sq(3)], [Sq(7), 0, Sq(2)*Sq(1)]]) + sage: M.connectivity() + 0 + + sage: G = FP_Module(A, [0,2], [[1,0]]) + sage: G.connectivity() + 2 + + TESTS: + + sage: C = FP_Module(SteenrodAlgebra(2, profile=(3,2,1)), [0], relations=[[Sq(1)], [0]]) + sage: C.connectivity() + 0 + + sage: F = FP_Module(A, [-1]) + sage: F.connectivity() + -1 + + sage: F = FP_Module(A, []) + sage: F.connectivity() + +Infinity + + sage: F = FP_Module(A, [0], [[1]]) + sage: F.connectivity() + +Infinity + """ + # In case there are no relations, the connectivity is the equal to + # the connectivity of the free module on the generators. + if self.j._degree == None: + return self._free_module().connectivity() + + # We must check that the generator(s) in the free generator module are + # not hit by relations, since we are not guaranteed that the + # presentation we have is minimal. + X = [x for x in self.generator_degrees()] + X.sort() + + previous = None + for k in X: + if previous != None and k == previous: + continue + if not self.j.vector_presentation(k - self.j._degree).is_surjective(): + return k + previous = k + + return PlusInfinity() + + + def is_trivial(self): + r""" + Decide if this module is isomorphic to the trivial module. + + OUTPUT: Returns ``True`` if the relations generate every non-zero + element of the module, and ``False`` otherwise. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A = SteenrodAlgebra(2) + sage: A2 = SteenrodAlgebra(2, profile=(3,2,1)) + + sage: M = FP_Module(A2, []) + sage: M.is_trivial() + True + + sage: N = FP_Module(A, [1,2]) + sage: N.is_trivial() + False + + sage: P = FP_Module(A, [1,2], [[1,0], [0,1]]) + sage: P.is_trivial() + True + + TESTS: + + sage: C = FP_Module(SteenrodAlgebra(2, profile=(3,2,1)), [0], [[Sq(1)], [0]]) + sage: C.is_trivial() + False + + sage: C = FP_Module(SteenrodAlgebra(2), [0], [[Sq(1)], [1]]) + sage: C.is_trivial() + True + """ + return self.connectivity() == PlusInfinity() + + + def has_relations(self): + r""" + Return ``True`` if no relations are defined, and ``False`` + otherwise. + + .. NOTE:: + + This module is free if this function returns ``False``, but a free + module can have (redundant) relations. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A2 = SteenrodAlgebra(2, profile=(3,2,1)) + + sage: F = FP_Module(A2, [1,2]) + sage: F.has_relations() + False + + sage: M = FP_Module(A2, [1,2], [[Sq(2), Sq(1)]]) + sage: M.has_relations() + True + + sage: # A free module constructed with a redundant + ....: # generator and relation. + sage: N = FP_Module(A2, [0,0], [[0, 1]]) + sage: N.has_relations() + True + sage: # Computing a minimal presentation reveals an + ....: # isomorphic module with no relations. + sage: N_min = N.min_presentation().domain() + sage: N_min.has_relations() + False + """ + return not self.j.is_zero() + + + def an_element(self, n=None): + r""" + An element of this module. + + This function chooses deterministically an element, i.e the output + depends only on the module and its input ``n``. + + INPUT: + + - ``n`` -- The degree of the element to construct. If the default + value ``None`` is given, a degree will be chosen by the function. + + OUTPUT: A module element of the given degree. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A2 = SteenrodAlgebra(2, profile=(3,2,1)) + sage: M = FP_Module(A2, [0,2,4], [[0, Sq(5), Sq(3)], [Sq(7), 0, Sq(2)*Sq(1)]]) + + sage: [M.an_element(i) for i in range(10)] + [<1, 0, 0>, + , + , + , + , + , + , + , + , + ] + """ + a_free_element = self._free_module().an_element(n) + return self(a_free_element) + + + @cached_method + def basis_elements(self, n, verbose=False): + r""" + A basis for the vector space of degree ``n`` module elements. + + INPUT: + + - ``n`` -- an integer. + + - ``verbose`` -- A boolean to control if log messages should be emitted. + (optional, default: ``False``) + + OUTPUT: A list of homogeneous module elements of degree ``n`` which is + a basis for the vector space of all degree ``n`` module elements. + + .. SEEALSO:: + + :meth:`vector_presentation`, :meth:`element_from_coordinates` + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A2 = SteenrodAlgebra(2, profile=(3,2,1)) + sage: M = FP_Module(A2, [0,2], [[Sq(4), Sq(2)], [0, Sq(6)]]) + + sage: M.basis_elements(4) + [, ] + + sage: M.basis_elements(5) + [, , <0, Sq(0,1)>] + + sage: M.basis_elements(25) + [] + + sage: M.basis_elements(0) + [<1, 0>] + + sage: M.basis_elements(2) + [, <0, 1>] + + TESTS: + + sage: Z0 = FP_Module(A2, []) + sage: Z0.basis_elements(n=10) + [] + + sage: Z1 = FP_Module(A2, [1], [[1]]) + sage: Z1.basis_elements(n=10) + [] + """ + return [self.element_from_coordinates(x, n) for\ + x in self.vector_presentation(n, verbose).basis()] + + + @cached_method + def element_from_coordinates(self, coordinates, n): + r""" + The module element in degree ``n`` having the given coordinates with + respect to the basis returned by :meth:`basis_elements`. + + This function is inverse to + :meth:`sage.modules.fp_graded.element.FP_Element.vector_presentation`. + + INPUT: + + - ``coordinates`` -- a vector of coordinates. + + - ``n`` -- the degree of the element to construct. + + OUTPUT: A module element of degree ``n`` having the given coordinates + with respect to the basis returned by :meth:`basis_elements`. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A = SteenrodAlgebra(2) + sage: M = FP_Module(A, [0], [[Sq(4)], [Sq(7)], [Sq(4)*Sq(9)]]) + + sage: M.vector_presentation(12).dimension() + 3 + sage: x = M.element_from_coordinates((0,1,0), 12); x + + + Applying the inverse function brings us back to the coordinate form:: + + sage: x.vector_presentation() + (0, 1, 0) + + TESTS: + + sage: M.element_from_coordinates((0,1,0,0), 12) + Traceback (most recent call last): + ... + ValueError: the given coordinate vector has incorrect length: 4. It should have length 3 + + .. SEEALSO:: + + :meth:`sage.modules.fp_graded.module.FP_Module.vector_presentation` + """ + M_n = self.vector_presentation(n) + + if len(coordinates) != M_n.dimension(): + raise ValueError('the given coordinate vector has incorrect length: %d. ' + 'It should have length %d' % (len(coordinates), M_n.dimension())) + + free_element = self._free_module().element_from_coordinates( + M_n.lift(coordinates), n) + + return self(free_element.dense_coefficient_list()) + + + def __getitem__(self, n): + r""" + A basis for the vector space of degree ``n`` module elements. + + INPUT: + + - ``n`` -- an integer. + + OUTPUT: A list of homogeneous module elements of degree ``n`` which is + a basis for the vector space of all degree ``n`` module elements. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A = SteenrodAlgebra(2) + sage: M = FP_Module(A, [0,2,4], [[Sq(4),Sq(2),0]]) + + sage: M[4] + [, , <0, 0, 1>] + + .. SEEALSO:: + + :meth:`basis_elements` + """ + return self.basis_elements(n) + + + @cached_method + def vector_presentation(self, n, verbose=False): + r""" + A vector space isomorphic to the vector space of module elements of + degree ``n``. + + INPUT: + + - ``n`` -- The degree of the presentation. + + OUTPUT: A vector space. + + .. SEEALSO:: + + :meth:`basis_elements`, :meth:`element_from_coordinates` + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A = SteenrodAlgebra(2) + sage: M = FP_Module(A, [0,2,4], [[Sq(4),Sq(2),0]]) + + sage: V = M.vector_presentation(4) + sage: V.dimension() + 3 + + sage: len(M.basis_elements(4)) + 3 + """ + # Get the vector space presentation of the free module on the + # module generators. + F_n = self._free_module().vector_presentation(n) + + # Compute the sub vector space generated by the relations. + spanning_set = [] + + if verbose: + num_total_iterations = 0 + for relation in self.j.values(): + if relation.is_zero(): + continue + + num_total_iterations += len(self.base_ring().basis(n - relation.degree())) + + progress = 0 + iteration_count = 0 + + for relation in self.j.values(): + + if relation.is_zero(): + continue + + for a in self.base_ring().basis(n - relation.degree()): + if verbose: + iteration_count += 1 + prog = int(100*iteration_count/num_total_iterations) + if prog > progress: + progress = prog + print('Progress: %d/100' % prog) + + # assert: isinstance(FreeElement, relation) + v = (a*relation).vector_presentation() + if not v is None: + spanning_set.append(v) + + R_n = F_n.subspace(spanning_set) + + # Return the quotient of the free part by the relations. + return F_n/R_n + + + def _Hom_(self, Y, category): + r""" + The internal hook used by the free function + :meth:`sage.categories.homset.hom.Hom` to create homsets involving + this parent class. + + TESTS: + + sage: from sage.modules.fp_graded.module import * + sage: A = SteenrodAlgebra(2) + sage: F = FP_Module(A, [1,3]); + sage: L = FP_Module(A, [2,3], [[Sq(2),Sq(1)], [0,Sq(2)]]); + + sage: homset = Hom(F, L); homset + Set of Morphisms from Finitely presented left module on 2 generators ... + """ + from .homspace import FP_ModuleHomspace + if not isinstance(Y, self.__class__): + raise ValueError('cannot create homspace between incompatible types:\n%s ->\n%s' % (self.__class__, type(Y))) + if Y.base_ring() != self.base_ring(): + raise ValueError('the modules are not defined over the same base ring') + + return FP_ModuleHomspace(self, Y, category) + + + def generator_degrees(self): + r""" + The degrees of the generators for this module. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A4 = SteenrodAlgebra(2, profile=(4,3,2,1)) + sage: N = FP_Module(A4, [0, 1], [[Sq(2), Sq(1)]]) + + sage: N.generator_degrees() + (0, 1) + """ + return self._generator_degrees + + + def generators(self): + r""" + The generators of this module. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A4 = SteenrodAlgebra(2, profile=(4,3,2,1)) + + sage: M = FP_Module(A4, [0,2,3]) + sage: M.generators() + [<1, 0, 0>, <0, 1, 0>, <0, 0, 1>] + + sage: N = FP_Module(A4, [0, 1], [[Sq(2), Sq(1)]]) + sage: N.generators() + [<1, 0>, <0, 1>] + + sage: Z = FP_Module(A4, []) + sage: Z.generators() + [] + """ + return [self.generator(i) for i in range(len(self.generator_degrees()))] + + + def generator(self, index): + r""" + The module generator with the given index. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A4 = SteenrodAlgebra(2, profile=(4,3,2,1)) + + sage: M = FP_Module(A4, [0,2,3]) + sage: M.generator(0) + <1, 0, 0> + + sage: N = FP_Module(A4, [0, 1], [[Sq(2), Sq(1)]]) + sage: N.generator(1) + <0, 1> + """ + return self(self._free_module().generator(index)) + + + def relations(self): + r""" + The relations of this module. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A4 = SteenrodAlgebra(2, profile=(4,3,2,1)) + + sage: M = FP_Module(A4, [0,2,3]) + sage: M.relations() + [] + + sage: N = FP_Module(A4, [0, 1], [[Sq(2), Sq(1)]]) + sage: N.relations() + [] + + sage: Z = FP_Module(A4, []) + sage: Z.relations() + [] + """ + return self.j.values() + + + def relation(self, index): + r""" + The module relation of the given index. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A4 = SteenrodAlgebra(2, profile=(4,3,2,1)) + sage: N = FP_Module(A4, [0, 1], [[Sq(2), Sq(1)]]) + sage: N.relation(0) + + """ + return self.j.values()[index] + + + def min_presentation(self, top_dim=None, verbose=False): + r""" + A minimal presentation of this module. + + OUTPUT: An isomorphism `M \to self`, where `M` has minimal presentation. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A2 = SteenrodAlgebra(2, profile=(3,2,1)) + + sage: M = FP_Module(A2, [0,1], [[Sq(2),Sq(1)],[0,Sq(2)],[Sq(3),0]]) + sage: i = M.min_presentation() + sage: M_min = i.domain() + + sage: # i is an isomorphism between M_min and M: + sage: i.codomain() is M + True + sage: i.is_injective() + True + sage: i.is_surjective() + True + + sage: # There are more relations in M than in M_min: + sage: M.relations() + [, <0, Sq(2)>, ] + sage: M_min.relations() + [, <0, Sq(2)>] + + TESTS: + + sage: T = FP_Module(A2, [0], [[1]]) + sage: T_min = T.min_presentation().domain() + sage: T_min.is_trivial() + True + sage: T_min + Finitely presented left module on 0 generators and 0 relations over ... + """ + return Hom(self, self).identity().image(top_dim, verbose) + + + def suspension(self, t): + r""" + The suspension of this module by the given degree. + + INPUT: + + - ``t`` -- An integer degree by which the module is suspended. + + OUTPUT: A module which is identical to this module by a shift of + degrees by the integer ``t``. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A = SteenrodAlgebra(2) + sage: A2 = SteenrodAlgebra(2, profile=(3,2,1)) + + sage: Y = FP_Module(A2, [0], [[Sq(1)]]) + sage: X = Y.suspension(4) + sage: X.generator_degrees() + (4,) + sage: X.relations() + [] + + sage: M = FP_Module(A, [2,3], [[Sq(2), Sq(1)], [0, Sq(2)]]) + sage: Q = M.suspension(1) + sage: Q.generator_degrees() + (3, 4) + sage: Q.relations() + [, <0, Sq(2)>] + sage: Q = M.suspension(-3) + sage: Q.generator_degrees() + (-1, 0) + sage: Q = M.suspension(0) + sage: Q.generator_degrees() + (2, 3) + """ + return FP_Module( + algebra=self.base_ring(), + generator_degrees=tuple([g + t for g in self.generator_degrees()]), + relations=self._relations) + + + def submodule(self, spanning_elements): + r""" + The submodule of this module spanned by the given elements. + + INPUT: + + - ``spanning_elements`` - An iterable of elements of this module. + + OUTPUT: The inclusion of the submodule into this module. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A2 = SteenrodAlgebra(2, profile=(3,2,1)) + + sage: M = FP_Module(A2, [0,1], [[Sq(2),Sq(1)]]) + sage: i = M.submodule([M.generator(0)]) + sage: i.codomain() is M + True + sage: i.is_injective() + True + sage: i.domain().generator_degrees() + (0,) + sage: i.domain().relations() + [] + """ + # Create the free graded module on the set of spanning elements. + degs = [x.degree() for x in spanning_elements] + F = FP_Module(self.base_ring(), tuple(degs)) + + # The submodule is the module generated by the spanning elements. + return Hom(F, self)(spanning_elements).image() + + + def resolution(self, k, top_dim=None, verbose=False): + r""" + A resolution of this module of length ``k``. + + INPUT: + + - ``k`` -- An non-negative integer. + + - ``verbose`` -- A boolean to control if log messages should be emitted. + (optional, default: ``False``) + + OUTPUT: A list of homomorphisms `[\epsilon, f_1, \ldots, f_k]` such that + + `f_i: F_i \to F_{i-1}` for `1, <0, 1>] + to + (<1, 0>, <0, 1>)] + sage: res = M.resolution(4, verbose=True) + Computing f_1 (1/4) + Computing f_2 (2/4) + Resolving the kernel in the range of dimensions [2, 25]: 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25. + Computing f_3 (3/4) + Resolving the kernel in the range of dimensions [8, 31]: 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31. + Computing f_4 (4/4) + Resolving the kernel in the range of dimensions [9, 33]: 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33. + sage: len(res) + 5 + sage: res + [Module homomorphism of degree 0 defined by sending the generators + [<1, 0>, <0, 1>] + to + (<1, 0>, <0, 1>), + Module homomorphism of degree 0 defined by sending the generators + [<1>] + to + (,), + Module homomorphism of degree 0 defined by sending the generators + [<1>] + to + (,), + Module homomorphism of degree 0 defined by sending the generators + [<1, 0>, <0, 1>] + to + (, ), + Module homomorphism of degree 0 defined by sending the generators + [<1, 0>, <0, 1>] + to + (, )] + sage: for i in range(len(res)-1): + ....: if not (res[i]*res[i+1]).is_zero(): + ....: print('The result is not a complex.') + """ + def _print_progress(i, k): + if verbose: + print ('Computing f_%d (%d/%d)' % (i, i, k)) + + if k < 0: + raise ValueError('the length of the resolution must be non-negative') + + complex = [] + + # Epsilon: F_0 -> M + F_0 = FP_Module.from_free_module(self._free_module()) + epsilon = Hom(F_0, self)(tuple(self.generators())) + complex.append(epsilon) + + if k == 0: + return complex + + # f_1: F_1 -> F_0 + _print_progress(1, k) + F_1 = FP_Module.from_free_module(self.j.domain()) + pres = Hom(F_1, F_0)(tuple([ F_0(x.dense_coefficient_list()) for x in self.j.values() ])) + + complex.append(pres) + + from .morphism import FP_ModuleMorphism + + # f_i: F_i -> F_i-1, for i > 1 + for i in range(2, k+1): + _print_progress(i, k) + + f = complex[i-1] + complex.append( + FP_ModuleMorphism._resolve_kernel( + f, + top_dim=top_dim, + verbose=verbose)) + + return complex + diff --git a/src/sage/modules/fp_graded/morphism.py b/src/sage/modules/fp_graded/morphism.py new file mode 100755 index 00000000000..f46b9fb5349 --- /dev/null +++ b/src/sage/modules/fp_graded/morphism.py @@ -0,0 +1,1772 @@ +r""" +Homomorphisms of finitely presented graded modules + +This class implements construction and basic manipulation of elements of the +Sage parent :class:`sage.modules.fp_graded.homspace.FP_ModuleHomspace`, +which models homomorphisms of finitely presented graded modules over connected +algebras. + +.. NOTE:: This class is intended for private use by its derived class + :class:`sage.modules.fp_steenrod.fpa_morphism.FPA_ModuleMorphism`. + +AUTHORS: + +- Robert R. Bruner, Michael J. Catanzaro (2012): Initial version. +- Sverre Lunoee--Nielsen and Koen van Woerden (2019-11-29): Updated the + original software to Sage version 8.9. +- Sverre Lunoee--Nielsen (2020-07-01): Refactored the code and added + new documentation and tests. +""" + +#***************************************************************************** +# Copyright (C) 2011 Robert R. Bruner +# and Michael J. Catanzaro +# +# 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 __future__ import absolute_import +from __future__ import print_function + +import sys + +from sage.categories.homset import End +from sage.categories.homset import Hom +from sage.categories.morphism import Morphism +from sage.misc.cachefunc import cached_method +from .element import FP_Element +from sage.rings.infinity import PlusInfinity + + +def _CreateRelationsMatrix(module, relations, source_degs, target_degs): + r""" + The action by the given relations can be written as multiplication by + the matrix `R = (r_{ij})_{i,j}` where each entry is an algebra element and + each row in the matrix contains the coefficients of a single relation. + + For a given source degree, `n`, the multiplication by `r_{ij}` restricts to + a linear transformation `M_n\to M_{n + \deg(r_{ij})}`. This function returns + the matrix of linear transformations gotten by restricting `R` to the given + source degrees. + + INPUT: + + - ``module`` -- The module where the relations acts. + - ``relations`` -- A list of lists of algebra coefficients defining the + matrix `R`. + - ``source_degs`` -- A list of integer degrees. Its length should be + equal to the number of columns of `R`. + - ``target_degs`` -- A list of integer degrees. Its length should be + equal to the number of rows of `R`. + + Furthermore must the degrees given by the input satisfy the following: + + `\text{source_degs[j]} + \deg(r_{i,j}) = \text{target_degs[i]}` + + for all `i, j`. + + OUTPUT: + + - ``block_matrix`` -- A list of lists representing a matrix of linear + transformations `(T_{ij})`. Each transformtion `T_{ij}` is the linear map + representing multiplication by the coefficient `r_{ij}` restricted to + the module elements of degree ``source_degs[j]``. + - ``R`` -- A matrix representing ``block_matrix`` as a single linear + transformation. + + TESTS: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: from sage.modules.fp_graded.morphism import _CreateRelationsMatrix + sage: A = SteenrodAlgebra(p=2) + sage: blocks, R = _CreateRelationsMatrix(FP_Module(A, [0]), [[Sq(2)]], [4], [6]) + + sage: blocks + [[Vector space morphism represented by the matrix: + [0 1 0] + [0 1 1] + Domain: Vector space quotient V/W of dimension 2 over Finite Field of size 2 where + V: Vector space of dimension 2 over Finite Field of size 2 + W: Vector space of degree 2 and dimension 0 over Finite Field of size 2 + Basis matrix: + [] + Codomain: Vector space quotient V/W of dimension 3 over Finite Field of size 2 where + V: Vector space of dimension 3 over Finite Field of size 2 + W: Vector space of degree 3 and dimension 0 over Finite Field of size 2 + Basis matrix: + []]] + + sage: R + [0 0] + [1 1] + [0 1] + """ + from sage.matrix.constructor import matrix + + if len(relations) == 0: + raise ValueError('no relations given, can not build matrix') + + # Create the block matrix of linear transformations. + block_matrix = [] + for i, r_i in enumerate(relations): + row = [] + target_space = module.vector_presentation(target_degs[i]) + + for j, r_ij in enumerate(r_i): + + values = [] + for b in module.basis_elements(source_degs[j]): + w = r_ij*b + values.append( + target_space.zero() if w.is_zero() else w.vector_presentation()) + + row.append( + Hom(module.vector_presentation(source_degs[j]), target_space)(values)) + + block_matrix.append(row) + + # Deal with the case of zero dimensional matrices first. + total_source_dim = 0 + for el in block_matrix[0]: + total_source_dim += el.domain().dimension() + total_target_dim = 0 + for row in block_matrix: + total_target_dim += row[0].codomain().dimension() + if total_source_dim == 0: + return block_matrix, matrix(total_target_dim, 0) + elif total_target_dim == 0: + return block_matrix, matrix(0, total_source_dim) + + # Create a matrix from the matrix of linear transformations. + entries = [] + for j in range(len(block_matrix[0])): + for n in range(block_matrix[0][j].domain().dimension()): + column = [] + for i in range(len(block_matrix)): + lin_trans = block_matrix[i][j] + column += lin_trans(lin_trans.domain().basis()[n]) + entries.append(column) + + return block_matrix, matrix(module.base_ring().base_ring(), entries).transpose() + + +class FP_ModuleMorphism(Morphism): + r""" + Create a homomorphism between finitely presented graded modules. + + INPUT: + + - ``parent`` -- A homspace of finitely presented graded modules. + - ``values`` -- A list of elements in the codomain. Each element + corresponds to a module generator in the domain. + + OUTPUT: A module homomorphism defined by sending the generator with + index `i` to the corresponding element in ``values``. + + .. NOTE:: Never use this constructor explicitly, but rather the parent's + call method, or this class' __call__ method. The reason for this + is that the dynamic type of the element class changes as a + consequence of the category system. + + TESTS: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: # Trying to map the generators of a non-free module into a + sage: # free module: + sage: A = SteenrodAlgebra(2) + sage: F = FP_Module(A, [2,3]) + sage: Q = FP_Module(A, [2,3], relations=[[Sq(6), Sq(5)]]) + sage: m = Hom(F, Q)( (F((Sq(1), 0)), F((0, 1))) ) + Traceback (most recent call last): + ... + ValueError: ill-defined homomorphism: degrees do not match + + sage: # Trying to map the generators of a non-free module into a + sage: # free module: + sage: w = Hom(Q, F)( (F((1, 0)), F((0, 1))) ) + Traceback (most recent call last): + ... + ValueError: relation is not sent to zero + """ + def __init__(self, parent, values): + r""" + Create a homomorphism between finitely presented graded modules. + """ + + from .homspace import is_FP_ModuleHomspace + + if not is_FP_ModuleHomspace(parent): + raise TypeError('parent (=%s) must be a fp module hom space' % parent) + + self.free_morphism = Hom(parent.domain().j.codomain(), parent.codomain().j.codomain())([v.free_element() for v in values]) + self._values = values + + # Call the base class constructor. + Morphism.__init__(self, parent) + + # Check that the homomorphism is well defined. + for relation in parent.domain().relations(): + # The relation is an element in the free part of the domain. + img = self.free_morphism(relation) + # if not FP_Element(parent.codomain(), self.free_morphism(relation)).is_zero(): + if parent.codomain()(img): + raise ValueError('relation %s is not sent to zero' % relation) + + + def change_ring(self, algebra): + r""" + Change the base ring of this module homomorphism. + + INPUT: + + - ``algebra`` -- a graded algebra. + + OUTPUT: An instance of this class. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A2 = SteenrodAlgebra(2, profile=(3,2,1)) + sage: A3 = SteenrodAlgebra(2, profile=(4,3,2,1)) + sage: M = FP_Module(A2, [0], relations=[[Sq(1)]]) + sage: N = FP_Module(A2, [0], relations=[[Sq(4)],[Sq(1)]]) + + sage: f = Hom(M,N)([A2.Sq(3)*N.generator(0)]); f + Module homomorphism of degree 3 defined by sending the generators + [<1>] + to + [] + + sage: f.base_ring() + sub-Hopf algebra of mod 2 Steenrod algebra, milnor basis, profile function [3, 2, 1] + + sage: g = f.change_ring(A3) + sage: g.base_ring() + sub-Hopf algebra of mod 2 Steenrod algebra, milnor basis, profile function [4, 3, 2, 1] + """ + new_codomain = self.codomain().change_ring(algebra) + # We have to change the ring for the values, too: + new_values = [] + for v in self._values: + new_values.append(new_codomain([algebra(a) for a in v.dense_coefficient_list()])) + return Hom(self.domain().change_ring(algebra), new_codomain)(new_values) + + + def degree(self): + r""" + The degree of this homomorphism. + + OUTPUT: The integer degree of this homomorphism. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A = SteenrodAlgebra(2) + sage: M = FP_Module(A, [0,1], [[Sq(2), Sq(1)]]) + sage: N = FP_Module(A, [2], [[Sq(4)]]) + sage: homspace = Hom(M, N) + + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: f = homspace(values) + sage: f.degree() + 7 + + The trivial homomorphism has no degree:: + + sage: homspace.zero().degree() + Traceback (most recent call last): + ... + ValueError: the zero morphism does not have a well-defined degree + + TESTS: + + sage: M = FP_Module(SteenrodAlgebra(p=2), [7]) + sage: N = FP_Module(SteenrodAlgebra(p=2), [0], relations=[[Sq(1)]]) + sage: f = Hom(M,N)([Sq(1)*N.generator(0)]) + sage: f == Hom(M,N).zero() + True + sage: f.degree() + Traceback (most recent call last): + ... + ValueError: the zero morphism does not have a well-defined degree + """ + if self.is_zero(): + # The zero morphism has no degree. + raise ValueError("the zero morphism does not have a well-defined degree") + return self.free_morphism.degree() + + + def values(self): + r""" + The values under this homomorphism of the module generators of the + domain module. + + OUTPUT: A sequence of module elements of the codomain. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A = SteenrodAlgebra(2) + sage: M = FP_Module(A, [0,1], [[Sq(2), Sq(1)]]) + sage: N = FP_Module(A, [2], [[Sq(4)]]) + sage: homspace = Hom(M, N) + + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: f = homspace(values) + + sage: f.values() + [, ] + + sage: homspace.zero().values() + [<0>, <0>] + """ + return self._values + + + def _richcmp_(self, other, op): + r""" + Compare this homomorphism to the given homomorphism. + + Implementation of this function allows Sage to make sense of the == + operator for instances of this class. + + INPUT: + + - ``other`` -- An instance of this class. + + - ``op`` -- An integer specifying the comparison operation to be + carried out: If ``op`` == 2, then return ``True`` if and only if the + homomorphisms are equal. If ``op`` == 3, then return ``True `` if + and only if the homomorphisms are not equal. Otherwise, + return ``False``. + + OUTPUT: A boolean. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A = SteenrodAlgebra(2) + sage: M = FP_Module(A, [0,1], [[Sq(2), Sq(1)]]) + sage: N = FP_Module(A, [2], [[Sq(4)]]) + sage: homspace = Hom(M, N) + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: f = homspace(values) + sage: f._richcmp_(f, op=2) + True + sage: f._richcmp_(f, op=3) + False + """ + try: + same = (self - other).is_zero() + except ValueError: + return False + + # Equality + if op == 2: + return same + + # Non-equality + if op == 3: + return not same + + return False + + + def __add__(self, g): + r""" + The pointwise sum of this and the given homomorphism. + + Pointwise addition of two homomorphisms `f` and `g` with the same domain + and codomain is given by the formula `(f+g)(x) = f(x) + g(x)` for + every `x` in the domain of `f`. + + INPUT: + + - ``g`` -- A homomorphism with the same domain and codomain as this + homomorphism. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A = SteenrodAlgebra(2) + sage: M = FP_Module(A, [0,1], [[Sq(2), Sq(1)]]) + sage: N = FP_Module(A, [2], [[Sq(4)]]) + sage: homspace = Hom(M, N) + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: f = homspace(values) + sage: ff = f.__add__(f) + sage: ff.is_zero() + True + sage: ff.__add__(f) == f + True + """ + if self.domain() != g.domain(): + raise ValueError('morphisms do not have the same domain') + elif self.codomain() != g.codomain(): + raise ValueError('morphisms do not have the same codomain') + if self.is_zero(): + return g + if g.is_zero(): + return self + if self.degree() and g.degree() and self.degree() != g.degree(): + raise ValueError('morphisms do not have the same degree') + + v = [self(x) + g(x) for x in self.domain().generators()] + + return self.parent()(v) + + + def __neg__(self): + r""" + The additive inverse of this homomorphism with respect to the group + structure given by pointwise sum. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A = SteenrodAlgebra(2) + sage: M = FP_Module(A, [0,1], [[Sq(2), Sq(1)]]) + sage: N = FP_Module(A, [2], [[Sq(4)]]) + sage: homspace = Hom(M, N) + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: f = homspace(values) + sage: f_inverse = f.__neg__(); f_inverse + Module homomorphism of degree 7 defined by sending the generators + [<1, 0>, <0, 1>] + to + [, ] + sage: (f + f_inverse).is_zero() + True + """ + return self.parent()([-x for x in self._values]) + + + def __sub__(self, g): + r""" + The difference between this and the given homomorphism, with + respect to the group structure given by pointwise sum. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A = SteenrodAlgebra(2) + sage: M = FP_Module(A, [0]) + sage: N = FP_Module(A, [0], [[Sq(4)]]) + sage: f = Hom(M, N)( [Sq(3)*N.generator(0)] ) + sage: g = Hom(M, N)( [Sq(0,1)*N.generator(0)] ) + sage: f.__sub__(g) + Module homomorphism of degree 3 defined by sending the generators + [<1>] + to + [] + + sage: f = Hom(M, N)( [Sq(4)*N.generator(0)] ) # the zero map + sage: g = Hom(M, N)( [Sq(1,1)*N.generator(0)] ) + sage: f.__sub__(g) + Module homomorphism of degree 4 defined by sending the generators + [<1>] + to + [] + """ + return self.__add__(g.__neg__()) + + + def __mul__(self, g): + r""" + The composition of the given homomorphism ``g``, followed by this + homomorphisms. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A = SteenrodAlgebra(2) + sage: M = FP_Module(A, [0], [[Sq(1,2)]]) + sage: N = FP_Module(A, [0], [[Sq(2,2)]]) + sage: f = Hom(M, N)( [Sq(2)*N.generator(0)] ) + sage: g = Hom(N, M)( [Sq(2,2)*M.generator(0)] ) + sage: fg = f.__mul__(g); fg + Module homomorphism of degree 10 defined by sending the generators + [<1>] + to + [] + sage: fg.is_endomorphism() + True + + TESTS: + + sage: from sage.modules.fp_graded.free_module import * + sage: A = SteenrodAlgebra(2) + sage: M = FP_Module(A, (0,1)) + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: f = Hom(M, N)(values) + sage: f.__mul__(f) + Traceback (most recent call last): + ... + ValueError: morphisms not composable + """ + if self.parent().domain() != g.parent().codomain(): + raise ValueError('morphisms not composable') + homset = Hom(g.parent().domain(), self.parent().codomain()) + return homset([self(g(x)) for x in g.domain().generators()]) + + + @cached_method + def is_zero(self): + r""" + Decide if this homomomorphism is the zero homomorphism. + + OUTPUT: The boolean value ``True`` if this homomorphism is trivial, and + ``False`` otherwise. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A = SteenrodAlgebra(2) + sage: M = FP_Module(A, [0,1], [[Sq(2), Sq(1)]]) + sage: N = FP_Module(A, [2], [[Sq(4)]]) + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + + sage: f = Hom(M, N)(values) + sage: f.is_zero() + False + + sage: (f-f).is_zero() + True + """ + return all([x.is_zero() for x in self._values]) + + + @cached_method + def is_identity(self): + r""" + Decide if this homomomorphism is the identity endomorphism. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A = SteenrodAlgebra(2) + sage: M = FP_Module(A, [0,1], [[Sq(2), Sq(1)]]) + sage: N = FP_Module(A, [2], [[Sq(4)]]) + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + + sage: f = Hom(M, N)(values) + sage: f.is_identity() + False + + sage: id = Hom(M, M)(M.generators()); id + The identity homomorphism. + + sage: id.is_identity() + True + """ + if self.parent().is_endomorphism_set(): + return self.parent().identity() == self + else: + return False + + + def __call__(self, x): + r""" + Evaluate the homomorphism at the given domain element ``x``. + + INPUT: + + - ``x`` - An element of the domain of the homomorphism. + + OUTPUT: The module element of the codomain which is the value of ``x`` + under this homomorphism. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A = SteenrodAlgebra(2) + sage: M = FP_Module(A, [0,1], [[Sq(2), Sq(1)]]) + sage: N = FP_Module(A, [2], [[Sq(4)]]) + + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: f = Hom(M, N)(values) + + sage: f.__call__(M.generator(0)) + + + sage: f.__call__(M.generator(1)) + + """ + if x.parent() != self.domain(): + raise ValueError('cannot evaluate morphism on element not in domain') + + return self.codomain()(self.free_morphism(x.free_element())) + + + def _repr_(self): + r""" + A string representation of this homomorphism. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import * + sage: A = SteenrodAlgebra(2) + sage: M = FP_Module(A, [0,1], [[Sq(2), Sq(1)]]) + sage: N = FP_Module(A, [2], [[Sq(4)]]) + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: Hom(M, N)(values)._repr_() + 'Module homomorphism of degree 7 defined by sending the generators\n [<1, 0>, <0, 1>]\nto\n [, ]' + sage: Hom(M, N).zero()._repr_() + 'The trivial homomorphism.' + sage: Hom(M, M).identity()._repr_() + 'The identity homomorphism.' + """ + if self.is_zero(): + return "The trivial homomorphism." + elif self.is_identity(): + return "The identity homomorphism." + else: + return "Module homomorphism of degree %d defined by sending "\ + "the generators\n %s\nto\n %s" % (self.degree(), self.domain().generators(), self._values) + + + @cached_method + def vector_presentation(self, n): + r""" + The restriction of this homomorphism to the domain module elements of + degree ``n``. + + The restriction of a non-zero module homomorphism to the vectorspace of + module elements of degree `n` is a linear function into the vectorspace + of elements of degree `n+d` belonging to the codomain. Here `d` is the + degree of this homomorphism. + + When this homomorphism is zero, it has no well defined degree so the + function cannot be presented since we do not know the degree of its + codomain. In this case, the return value is ``None``. + + INPUT: + + - ``n`` -- An integer degree. + + OUTPUT: A linear function of finite dimensional vectorspaces over the + ground field of the algebra for this module. The domain is isomorphic + to the vectorspace of domain elements of degree ``n`` of this free + module, via the choice of basis given by + :meth:`sage.modules.fp_graded.free_module.FreeGradedModule.basis_elements`. + If the morphism is zero, the value ``None`` is returned. + + .. SEEALSO:: + + :meth:`sage.modules.fp_graded.module.FP_Module.vector_presentation`, + :meth:`sage.modules.fp_graded.module.FP_Module.basis_elements`. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A = SteenrodAlgebra(2) + sage: M = FP_Module(A, [0,1], [[Sq(2), Sq(1)]]) + sage: N = FP_Module(A, [2], [[Sq(4)]]) + sage: values = [Sq(5)*N.generator(0), Sq(3,1)*N.generator(0)] + sage: f = Hom(M, N)(values) + sage: f.vector_presentation(0) + Vector space morphism represented by the matrix: + [0] + Domain: Vector space quotient V/W of dimension 1 over Finite Field of size 2 where + V: Vector space of dimension 1 over Finite Field of size 2 + W: Vector space of degree 1 and dimension 0 over Finite Field of size 2 + Basis matrix: + [] + Codomain: Vector space quotient V/W of dimension 1 over Finite Field of size 2 where + V: Vector space of dimension 2 over Finite Field of size 2 + W: Vector space of degree 2 and dimension 1 over Finite Field of size 2 + Basis matrix: + [0 1] + sage: f.vector_presentation(1) + Vector space morphism represented by the matrix: + [0 0] + [0 1] + Domain: Vector space quotient V/W of dimension 2 over Finite Field of size 2 where + V: Vector space of dimension 2 over Finite Field of size 2 + W: Vector space of degree 2 and dimension 0 over Finite Field of size 2 + Basis matrix: + [] + Codomain: Vector space quotient V/W of dimension 2 over Finite Field of size 2 where + V: Vector space of dimension 3 over Finite Field of size 2 + W: Vector space of degree 3 and dimension 1 over Finite Field of size 2 + Basis matrix: + [0 1 1] + sage: f.vector_presentation(2) + Vector space morphism represented by the matrix: + [0 0] + Domain: Vector space quotient V/W of dimension 1 over Finite Field of size 2 where + V: Vector space of dimension 2 over Finite Field of size 2 + W: Vector space of degree 2 and dimension 1 over Finite Field of size 2 + Basis matrix: + [1 1] + Codomain: Vector space quotient V/W of dimension 2 over Finite Field of size 2 where + V: Vector space of dimension 4 over Finite Field of size 2 + W: Vector space of degree 4 and dimension 2 over Finite Field of size 2 + Basis matrix: + [0 0 1 0] + [0 0 0 1] + + + TESTS: + + sage: F = FP_Module(A, [0]) + sage: Q = FP_Module(A, [0], [[Sq(2)]]) + sage: z = Hom(F, Q)([Sq(2)*Q.generator(0)]) + sage: z.is_zero() + True + sage: z.vector_presentation(0) is None + True + """ + # The trivial map has no degree, so we can not create the codomain + # of the linear transformation. + if self.is_zero(): + return None + + D_n = self.domain().vector_presentation(n) + C_n = self.codomain().vector_presentation(self.degree() + n) + + values = [self(e) for e in self.domain().basis_elements(n)] + + return Hom(D_n, C_n)([ + C_n.zero() if e.is_zero() else e.vector_presentation() for e in values]) + + + def solve(self, x): + r""" + Find an element in the inverse image of the given element. + + INPUT: + + - ``x`` -- An element of the codomain of this morphism. + + OUTPUT: An element of the domain which maps to ``x`` under this + morphism, or ``None`` if ``x`` was not in the image of this morphism. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A = SteenrodAlgebra(2) + sage: M = FP_Module(A, [0], [[Sq(3)]]) + sage: N = FP_Module(A, [0], [[Sq(2,2)]]) + sage: f = Hom(M, N)( [Sq(2)*N.generator(0)] ) + sage: y = Sq(1,1)*N.generator(0); y + + sage: x = f.solve(y); x + + sage: y == f(x) + True + + Trying to lift an element which is not in the image results in a ``None`` value:: + + sage: z = f.solve(Sq(1)*N.generator(0)) + sage: z is None + True + + TESTS: + + sage: f.solve(Sq(2,2)*M.generator(0)) + Traceback (most recent call last): + ... + ValueError: the given element is not in the codomain of this homomorphism + """ + if x.parent() != self.codomain(): + raise ValueError('the given element is not in the codomain of this homomorphism') + + # The zero element lifts over all morphisms. + if x.is_zero(): + return self.domain().zero() + + # Handle the trivial homomorphism since it does not have a well defined + # degree. + if self.is_zero(): + return None + + # Handle the case where both the morhism and the element is non-trivial. + n = x.degree() - self.degree() + f_n = self.vector_presentation(n) + + v = x.vector_presentation() + + # Return None if ``x`` cannot be lifted. + if not v in f_n.image(): + return None + + u = f_n.matrix().solve_left(v) + return self.domain().element_from_coordinates(u, n) + + + def lift(self, f, verbose=False): + r""" + A lift of this homomorphism over the given homomorphism ``f``. + + INPUT: + + - ``f`` -- A homomorphism with codomain equal to the codomain of this + homomorphism. + - ``verbose`` -- A boolean to enable progress messages. (optional, + default: ``False``) + + OUTPUT: A homomorphism `g` with the property that this homomorphism + equals `f\circ g`. If no lift exist, ``None`` is returned. + + ALGORITHM: + + Let `L` be the domain of this homomorphism, and choose `x_1, \ldots, x_N` + such that `f(x_i) = self(g_i)` where the `g_i`'s are the module + generators of `L`. + + The linear function sending `g_i` to `x_i` for every `i` is well + defined if and only if the vector `x = (x_1,\ldots, x_N)` lies + in the nullspace of the coefficient matrix `R = (r_{ij})` given by the + relations of `L`. + + Let `k \in \ker(f)` solve the matrix equation: + + `R\cdot k = R\cdot x`. + + Define a module homomorphism by sending the generators of `L` to + `x_1 - k_1, \ldots, x_N - k_N`. This is well defined, and is also a + lift of this homomorphism over `f`. + + Note that it does not matter how we choose the initial elements `x_i`: + If `x'` is another choice then `x' - x\in \ker(f)` and + `R\cdot k = R\cdot x` if and only if `R\cdot (k + x' - x) = R\cdot x'`. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A = SteenrodAlgebra(2) + + Lifting a map from a free module is always possible:: + + sage: M = FP_Module(A, [0], [[Sq(3)]]) + sage: N = FP_Module(A, [0], [[Sq(2,2)]]) + sage: F = FP_Module(A, [0]) + sage: f = Hom(M,N)([Sq(2)*N.generator(0)]) + sage: k = Hom(F,N)([Sq(1)*Sq(2)*N.generator(0)]) + sage: f_ = k.lift(f) + sage: f*f_ == k + True + sage: f_ + Module homomorphism of degree 1 defined by sending the generators + [<1>] + to + [] + + A split projection:: + + sage: A_plus_HZ = FP_Module(A, [0,0], [[0, Sq(1)]]) + sage: HZ = FP_Module(A, [0], [[Sq(1)]]) + sage: q = Hom(A_plus_HZ, HZ)([HZ([1]), HZ([1])]) + sage: # We can construct a splitting of `q` manually: + sage: split = Hom(HZ,A_plus_HZ)([A_plus_HZ.generator(1)]) + sage: q*split + The identity homomorphism. + sage: # Thus, lifting the identity homomorphism over `q` should be possible: + sage: id = Hom(HZ,HZ).identity() + sage: j = id.lift(q); j + Module homomorphism of degree 0 defined by sending the generators + [<1>] + to + [<0, 1>] + sage: q*j + The identity homomorphism. + + Lifting over the inclusion of the image sub module:: + + sage: A = SteenrodAlgebra(2) + sage: M = FP_Module(A, [0], relations=[[Sq(0,1)]]) + sage: f = Hom(M,M)([Sq(2)*M.generator(0)]) + sage: im = f.image(top_dim=10) + sage: f.lift(im) + Module homomorphism of degree 2 defined by sending the generators + [<1>] + to + [<1>] + + When a lift cannot be found, the ``None`` value is returned. By setting the + verbose argument to ``True``, an explanation of why the lifting failed will + be displayed:: + + sage: F2 = FP_Module(A, [0,0]) + sage: non_surjection = Hom(F2, F2)([F2([1, 0]), F2([0, 0])]) + sage: lift = Hom(F2, F2).identity().lift(non_surjection, verbose=True) + The generators of the domain of this homomorphism do not map into the image of the homomorphism we are lifting over. + sage: lift is None + True + + TESTS: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A = SteenrodAlgebra(2) + sage: # The trivial map often involved in corner cases.. + sage: trivial_map = Hom(FP_Module(A, [0]), FP_Module(A, [])).zero() + sage: trivial_map.lift(trivial_map) + The trivial homomorphism. + + sage: F = FP_Module(A, [0]) + sage: HZ = FP_Module(A, [0], relations=[[Sq(1)]]) + sage: f = Hom(F,HZ)(HZ.generators()) + sage: split = Hom(HZ, HZ).identity().lift(f, verbose=True) + The homomorphism cannot be lifted in any way such that the relations of the domain are respected: matrix equation has no solutions + sage: split is None + True + + sage: Hom(F, F).identity().lift(f, verbose=true) + Traceback (most recent call last): + ... + ValueError: the codomains of this homomorphism and the homomorphism we are lifting over are different + + sage: f.lift(Hom(HZ, HZ).zero(), verbose=True) + This homomorphism cannot lift over a trivial homomorphism since it is non-trivial. + + sage: Ap = SteenrodAlgebra(p=2, profile=(2,2,2,1)) + sage: Hko = FP_Module(Ap, [0], [[Sq(2)], [Sq(1)]]) + sage: f = Hom(Hko, Hko)([(Ap.Sq(0,0,3) + Ap.Sq(0,2,0,1))*Hko.generator(0)]) + sage: f*f == 0 + True + sage: k = f.kernel() # long time + sage: f.lift(k) # long time + Module homomorphism of degree 21 defined by sending the generators + [<1>] + to + [<0, Sq(1), 0>] + + Corner cases involving trivial maps:: + + sage: M = FP_Module(A, [1]) + sage: M1 = FP_Module(A, [0]) + sage: M2 = FP_Module(A, [0], [[Sq(1)]]) + sage: q = Hom(M1, M2)([M2.generator(0)]) + sage: z = Hom(M, M2).zero() + sage: lift = z.lift(q) + sage: lift.domain() is M and lift.codomain() is M1 + True + + .. SEEALSO:: + :meth:`split` + """ + from sage.modules.free_module_element import vector + + # self + # L -------> N + # \ ^ + # \ | + # lift \ | f + # \ | + # _| | + # M + L = self.domain() + N = self.codomain() + M = f.domain() + + # It is an error to call this function with incompatible arguments. + if not f.codomain() is N: + raise ValueError('the codomains of this homomorphism and the homomorphism '\ + 'we are lifting over are different') + + # The trivial map lifts over any other map. + if self.is_zero(): + return Hom(L, M).zero() + + # A non-trivial map never lifts over the trivial map. + if f.is_zero(): + if verbose: + print('This homomorphism cannot lift over a trivial homomorphism since it is non-trivial.') + return None + + xs = [f.solve(self(g)) for g in L.generators()] + + # If some of the generators are not in the image of f, there is no + # hope finding a lift. + if None in xs: + if verbose: + print('The generators of the domain of this homomorphism do '\ + 'not map into the image of the homomorphism we are lifting over.') + return None + + # If L is free there are no relations to take into consideration. + if not L.has_relations(): + return Hom(L, M)(xs) + + # The degree of the lifted map f_. + lift_deg = self.degree() - f.degree() + + # Compute the kernel of f. The equations we will solve will live in + # this submodule. + iK = f.kernel(top_dim=max([r.degree() + lift_deg for r in L.relations()])) + + source_degs = [g.degree() + lift_deg for g in L.generators()] + target_degs = [r.degree() + lift_deg for r in L.relations()] + + # Act on the liftings xs by the relations. + ys = [] + K = iK.domain() + all_zero = True + + for r in L.relations(): + target_degree = r.degree() + lift_deg + + y = iK.solve(sum([c*x for c,x in zip(r.dense_coefficient_list(), xs)])) + if y is None: + if verbose: + print('The homomorphism cannot be lifted in any ' + 'way such that the relations of the domain are ' + 'respected.') + return None + + if y.is_zero(): + dim = len(K[target_degree]) + ys += dim*[0] # The zero vector of the appropriate dimension. + else: + all_zero = False + ys += list(y.vector_presentation()) + + # If the initial guess already fits the relations, we are done. + if all_zero: + return Hom(L, M)(xs) + + block_matrix, R = _CreateRelationsMatrix( + K, [r.dense_coefficient_list() for r in L.relations()], source_degs, target_degs) + + try: + solution = R.solve_right(vector(ys)) + except ValueError as error: + if str(error) == 'matrix equation has no solutions': + if verbose: + print('The homomorphism cannot be lifted in any ' + 'way such that the relations of the domain ' + 'are respected: %s' % error) + + return None + else: + raise ValueError(error) + + # Interpret the solution vector as a vector in the direct sum + # $ K_1\oplus K_2\oplus \ldots \oplus K_n $. + n = 0 + for j, source_degree in enumerate(source_degs): + + source_dimension = block_matrix[0][j].domain().dimension() + + w = K.element_from_coordinates( + solution[n:n + source_dimension], source_degree) + + # Subtract the solution w_i from our initial choice of lift + # for the generator g_i. + xs[j] -= iK(w) + + n += source_degree + + return Hom(L, M)(xs) + + + def split(self, verbose=False): + r""" + A split of this homomorphism. + + INPUT: + + - ``verbose`` -- A boolean to enable progress messages. (optional, + default: ``False``) + + OUTPUT: A homomorphism with the property that the composite + homomorphism `self \circ f = id` is the identity homomorphism. If no + such split exist, ``None`` is returned. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A = SteenrodAlgebra(2) + sage: M = FP_Module(A, [0,0], [[0, Sq(1)]]) + sage: N = FP_Module(A, [0], [[Sq(1)]]) + sage: p = Hom(M, N)([N.generator(0), N.generator(0)]) + sage: s = p.split(); s + Module homomorphism of degree 0 defined by sending the generators + [<1>] + to + [<0, 1>] + sage: # Verify that `s` is a splitting: + sage: p*s + The identity homomorphism. + + TESTS: + + sage: F = FP_Module(A, [0]) + sage: N = FP_Module(A, [0], [[Sq(1)]]) + sage: p = Hom(F, N)([N.generator(0)]) + sage: p.split(verbose=True) is None + The homomorphism cannot be lifted in any way such that the relations of the domain are respected: matrix equation has no solutions + True + + .. SEEALSO:: + :meth:`lift` + """ + id = End(self.codomain()).identity() + return id.lift(self, verbose) + + + def homology(self, f, top_dim=None, verbose=False): + r""" + Compute the sub-quotient module `H(self, f) = \ker(self)/\operatorname{im}(f)`, in + a range of degrees. + + For a pair of composable morphisms `f: M\to N` and `g: N \to Q` of + finitely presented modules, the homology module is a finitely + presented quotient of the kernel sub module `\ker(g) \subset N`. + + INPUT: + + - ``f`` -- A homomorphism with codomain equal to the domain of this + homomorphism, and image contained in the kernel of this homomorphism. + + - ``top_dim`` -- An integer used by this function to stop the + computation at the given degree, or the value ``None`` if no termination + should be enforced. (optional, default: ``None``) + + - ``verbose`` -- A boolean to enable progress messages. (optional, + default: ``False``) + + OUTPUT: A quotient homomorphism `\ker(self) \to H`, where `H` is + isomorphic to `H(self, f)` in degrees less than or equal to ``top_dim``. + + .. NOTE:: + + If the algebra for this module is finite, then no ``top_dim`` + needs to be specified in order to ensure that this function terminates. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A = SteenrodAlgebra(2, profile=(3,2,1)) + sage: M = FP_Module(A, [0], [[Sq(3)]]) + sage: N = FP_Module(A, [0], [[Sq(2,2)]]) + sage: F = FP_Module(A, [0]) + sage: f = Hom(M,N)([A.Sq(2)*N.generator(0)]) + sage: g = Hom(F, M)([A.Sq(4)*A.Sq(1,2)*M.generator(0)]) + sage: ho = f.homology(g) + sage: ho.codomain() + Finitely presented left module on 1 generator and 5 relations over sub-Hopf algebra of mod 2 Steenrod algebra, milnor basis, profile function [3, 2, 1] + sage: ho.codomain().is_trivial() + False + """ + k = self.kernel(top_dim, verbose) + f_ = f.lift(k) + if f_ is None: + raise ValueError('the image of the given homomorphism is not contained ' + 'in the kernel of this homomorphism. The homology is ' + 'therefore not defined for this pair of maps') + + return f_.cokernel() + + + def suspension(self, t): + r""" + The suspension of this morphism by the given degree ``t``. + + INPUT: + + - ``t`` -- An integer by which the morphism is suspended. + + OUTPUT: The morphism which is the suspension of this morphism by the + degree ``t``. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A = SteenrodAlgebra(2) + sage: F1 = FP_Module(A, [4,5]) + sage: F2 = FP_Module(A, [5]) + + sage: f = Hom(F1, F2)( ( F2([Sq(4)]), F2([Sq(5)]) ) ); f + Module homomorphism of degree 5 defined by sending the generators + [<1, 0>, <0, 1>] + to + (, ) + + sage: e1 = F1([1, 0]) + sage: e2 = F1([0, 1]) + sage: f(e1) + + sage: f(e2) + + + sage: sf = f.suspension(4); sf + Module homomorphism of degree 5 defined by sending the generators + [<1, 0>, <0, 1>] + to + [, ] + + sage: sf.domain() is f.domain().suspension(4) + True + + sage: sf.codomain() is f.codomain().suspension(4) + True + """ + if t == 0: + return self + else: + D = self.domain().suspension(t) + C = self.codomain().suspension(t) + return Hom(D, C)([C(x.free_element().dense_coefficient_list()) for x in self._values]) + + + def cokernel(self): + r""" + Compute the cokernel of this homomorphism. + + OUTPUT: The natural projection from the codomain of this homomorphism + to its cokernel. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A1 = SteenrodAlgebra(2, profile=(2,1)) + sage: M = FP_Module(A1, [0], [[Sq(2)]]) + sage: F = FP_Module(A1, [0]) + + sage: r = Hom(F, M)([A1.Sq(1)*M.generator(0)]) + sage: co = r.cokernel(); co + Module homomorphism of degree 0 defined by sending the generators + [<1>] + to + [<1>] + + sage: co.domain().is_trivial() + False + """ + from .module import FP_Module + new_relations = [x.dense_coefficient_list() for x in self.codomain().relations()] +\ + [x.dense_coefficient_list() for x in self._values] + + coker = FP_Module(self.base_ring(), + self.codomain().generator_degrees(), + relations=tuple(new_relations)) + + projection = Hom(self.codomain(), coker)(coker.generators()) + + return projection + + + def kernel(self, top_dim=None, verbose=False): + r""" + Compute the kernel of this homomorphism. + + INPUT: + + - ``top_dim`` -- An integer used by this function to stop the + computation at the given degree, or the value ``None`` if no + termination should be enforced. (optional, default: ``None``) + + - ``verbose`` -- A boolean to enable progress messages. (optional, + default: ``False``) + + OUTPUT: A homomorphism into `\ker(self)` which is an isomorphism in + degrees less than or equal to ``top_dim``. + + .. NOTE:: + + If the algebra for this module is finite, then no ``top_dim`` needs + to be specified in order to ensure that this function terminates. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A3 = SteenrodAlgebra(2, profile=(4,3,2,1)) + sage: F = FP_Module(A3, [1,3]); + sage: L = FP_Module(A3, [2,3], [[Sq(2),Sq(1)], [0,Sq(2)]]); + sage: H = Hom(F, L); + + sage: H([L((A3.Sq(1), 1)), L((0, A3.Sq(2)))]).kernel() # long time + Module homomorphism of degree 0 defined by sending the generators + [<1, 0>, <0, 1>] + to + (<0, 1>, ) + + sage: M = FP_Module(A3, [0,7], [[Sq(1), 0], [Sq(2), 0], [Sq(4), 0], [Sq(8), Sq(1)], [0, Sq(7)], [0, Sq(0,1,1)+Sq(4,2)]]) + sage: F2 = FP_Module(A3, [0], [[Sq(1)], [Sq(2)], [Sq(4)], [Sq(8)], [Sq(15)]]) + sage: H = Hom(M, F2) + sage: f = H([F2([1]), F2([0])]) + + sage: K = f.kernel(verbose=True, top_dim=17) + 1. Computing the generators of the kernel presentation: + Resolving the kernel in the range of dimensions [0, 17]: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17. + 2. Computing the relations of the kernel presentation: + Resolving the kernel in the range of dimensions [7, 17]: 7 8 9 10 11 12 13 14 15 16 17. + + sage: K.domain().generators() + [<1>] + sage: K.domain().relations() + [, + , + , + ] + + sage: K + Module homomorphism of degree 0 defined by sending the generators + [<1>] + to + (<0, 1>,) + """ + if verbose: + print('1. Computing the generators of the kernel presentation:') + j0 = self._resolve_kernel(top_dim, verbose) + if verbose: + print('2. Computing the relations of the kernel presentation:') + j1 = j0._resolve_kernel(top_dim, verbose) + + # Create a module isomorphic to the ker(self). + K = j1.to_fp_module() + + # Return an injection of K into the domain of self such that + # its image equals ker(self). + return Hom(K, j0.codomain())(j0.values()) + + + def image(self, top_dim=None, verbose=False): + r""" + Compute the image of this homomorphism. + + INPUT: + + - ``top_dim`` -- An integer used by this function to stop the + computation at the given degree, or the value ``None`` if no + termination should be enforced. (optional, default: ``None``) + + - ``verbose`` -- A boolean to enable progress messages. (optional, + default: ``False``) + + OUTPUT: A homomorphism into `\operatorname{im}(self)` which is an + isomorphism in degrees less than or equal to ``top_dim``. + + .. NOTE:: + + If the algebra for this module is finite, then no ``top_dim`` + needs to be specified in order to ensure that this function + terminates. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A3 = SteenrodAlgebra(2, profile=(4,3,2,1)) + sage: F = FP_Module(A3, [1,3]); + sage: L = FP_Module(A3, [2,3], [[Sq(2),Sq(1)], [0,Sq(2)]]); + sage: H = Hom(F, L); + + sage: H([L((A3.Sq(1), 1)), L((0, A3.Sq(2)))]).image() # long time + Module homomorphism of degree 0 defined by sending the generators + [<1>] + to + (,) + + sage: M = FP_Module(A3, [0,7], [[Sq(1), 0], [Sq(2), 0], [Sq(4), 0], [Sq(8), Sq(1)], [0, Sq(7)], [0, Sq(0,1,1)+Sq(4,2)]]) + sage: F2 = FP_Module(A3, [0], [[Sq(1)], [Sq(2)], [Sq(4)], [Sq(8)], [Sq(15)]]) + sage: H = Hom(M, F2) + sage: f = H([F2([1]), F2([0])]) + sage: K = f.image(verbose=True, top_dim=17) + 1. Computing the generators of the image presentation: + Resolving the image in the range of dimensions [0, 17]: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17. + 2. Computing the relations of the image presentation: + Resolving the kernel in the range of dimensions [0, 17]: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17. + + sage: K.is_injective() # long time + True + sage: K.domain().generator_degrees() + (0,) + sage: K.domain().relations() + [, , , ] + sage: K.domain().is_trivial() + False + + """ + if verbose: + print('1. Computing the generators of the image presentation:') + j0 = self._resolve_image(top_dim, verbose) + if verbose: + print('2. Computing the relations of the image presentation:') + j1 = j0._resolve_kernel(top_dim, verbose) + + # Create a module isomorphic to the im(self). + I = j1.to_fp_module() + + # Return an injection of I into the codomain of self such that + # its image equals im(self) + return Hom(I, j0.codomain())(j0.values()) + + + def is_injective(self, top_dim=None, verbose=False): + r""" + Return ``True`` if and only if this homomorphism has a trivial kernel. + + INPUT: + + - ``top_dim`` -- An integer used by this function to stop the + computation at the given degree, or the value ``None`` if no termination + should be enforced. (optional, default: ``None``) + + - ``verbose`` -- A boolean to enable progress messages. (optional, + default: ``False``) + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A = SteenrodAlgebra(2) + + sage: K = FP_Module(A, [2], [[Sq(2)]]) + sage: HZ = FP_Module(A, [0], [[Sq(1)]]) + + sage: f = Hom(K, HZ)([Sq(2)*HZ([1])]) + sage: f.is_injective(top_dim=23) + True + + TESTS: + + sage: Z = FP_Module(A, []) + sage: Hom(Z, HZ).zero().is_injective(top_dim=8) + True + + """ + j0 = self._resolve_kernel(top_dim, verbose) + return j0.domain().is_trivial() + + + def is_surjective(self): + r""" + Return ``True`` if and only if this homomorphism has a trivial cokernel. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A = SteenrodAlgebra(2) + sage: F = FP_Module(A, [0]) + + sage: f = Hom(F,F)([Sq(1)*F.generator(0)]) + sage: f.is_surjective() + False + + TESTS: + + sage: Z = FP_Module(A, []) + sage: Hom(F, Z).zero().is_surjective() + True + + """ + return self.cokernel().is_zero() + + + def _resolve_kernel(self, top_dim=None, verbose=False): + r""" + Resolve the kernel of this homomorphism by a free module. + + INPUT: + + - ``top_dim`` -- An integer used by this function to stop the + computation at the given degree, or the value ``None`` if no termination + should be enforced. (optional, default: ``None``) + + - ``verbose`` -- A boolean to enable progress messages. (optional, + default: ``False``) + + OUTPUT: A homomorphism `j: F \rightarrow D` where `D` is the domain of + this homomorphism, `F` is free and such that + `\ker(self) = \operatorname{im}(j)` in all degrees less than or equal + to ``top_dim``. + + .. NOTE:: + + If the algebra for this module is finite, then no ``top_dim`` + needs to be specified in order to ensure that this function terminates. + + TESTS: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A = SteenrodAlgebra(2) + sage: F = FP_Module(A, [0,0]) + sage: L = FP_Module(A, [0,0], [[Sq(3),Sq(0,1)], [0,Sq(2)]]) + sage: f = Hom(F, L)([L([Sq(2),0]), L([0, Sq(2)])]) + sage: f._resolve_kernel() + Traceback (most recent call last): + ... + ValueError: a top dimension must be specified for this calculation to terminate + sage: f._resolve_kernel(top_dim=20) + Module homomorphism of degree 0 defined by sending the generators + [<1, 0, 0>, <0, 1, 0>, <0, 0, 1>] + to + (<0, 1>, , ) + sage: A3 = SteenrodAlgebra(2, profile=(4,3,2,1)) + sage: f.change_ring(A3)._resolve_kernel() # long time + Module homomorphism of degree 0 defined by sending the generators + [<1, 0, 0>, <0, 1, 0>, <0, 0, 1>] + to + (<0, 1>, , ) + """ + # Let + # + # 1) `j` be a homomorphism into `\ker(self)`, and + # 2) 'n' be an integer. + # + # The induction loop starts each iteration assuming that that `j` is onto + # the kernel in degrees below `n`. Each iteration of the loop then + # extends the map `j` minimally so that `j_n` becomes onto the kernel. + # + # This induction step is then repeated for all `n \leq` ``top_dim``. + # + + if self.is_zero(): + # Epsilon: F_0 -> M + M = self.domain() + F_0 = self.domain().j.codomain().to_fp_module() + epsilon = Hom(F_0, M)(tuple(M.generators())) + return epsilon + + # Create the trivial module F_ to start with. + F_ = self.domain().__class__(self.base_ring(), ()) + j = Hom(F_, self.domain())(()) + + dim = self.domain().connectivity() + if dim == PlusInfinity(): + if verbose: + print ('The domain of the morphism is trivial, so there is nothing to resolve.') + return j + + limit = PlusInfinity() if not self.base_ring().is_finite() else\ + (self.base_ring().top_class().degree() + max(self.domain().generator_degrees())) + + if not top_dim is None: + limit = min(top_dim, limit) + + if limit == PlusInfinity(): + raise ValueError('a top dimension must be specified for this calculation to terminate') + + if verbose: + if dim > limit: + print('The dimension range is empty: [%d, %d]' % (dim, limit)) + else: + print('Resolving the kernel in the range of dimensions [%d, %d]:' % (dim, limit), end='') + + # The induction loop. + for n in range(dim, limit+1): + + if verbose: + print(' %d' % n, end='') + sys.stdout.flush() + + # We have taken care of the case when self is zero, so the + # vector presentation exists. + self_n = self.vector_presentation(n) + kernel_n = self_n.kernel() + + if kernel_n.dimension() == 0: + continue + + generator_degrees = tuple((x.degree() for x in F_.generators())) + + if j.is_zero(): + # The map j is not onto in degree `n` of the kernel. + new_generator_degrees = tuple(kernel_n.dimension()*(n,)) + F_ = self.domain().__class__(self.base_ring(), + generator_degrees + new_generator_degrees) + + new_values = tuple([ + self.domain().element_from_coordinates(q, n) for q in kernel_n.basis()]) + + else: + Q_n = kernel_n.quotient(j.vector_presentation(n).image()) + + if Q_n.dimension() == 0: + continue + + # The map j is not onto in degree `n` of the kernel. + new_generator_degrees = tuple(Q_n.dimension()*(n,)) + F_ = self.domain().__class__(self.base_ring(), + generator_degrees + new_generator_degrees) + + new_values = tuple([ + self.domain().element_from_coordinates(Q_n.lift(q), n) for q in Q_n.basis()]) + + # Create a new homomorphism which is surjective onto the kernel + # in all degrees less than, and including `n`. + j = Hom(F_, self.domain()) (j.values() + new_values) + + if verbose: + print('.') + return j + + + def _resolve_image(self, top_dim=None, verbose=False): + r""" + Resolve the image of this homomorphism by a free module. + + INPUT: + + - ``top_dim`` -- An integer used by this function to stop the + computation at the given degree, or the value ``None`` if no termination + should be enforced. (optional, default: ``None``) + + - ``verbose`` -- A boolean to enable progress messages. (optional, + default: ``False``) + + OUTPUT: A homomorphism `j: F \rightarrow C` where `C` is the codomain + of this homomorphism, `F` is free, and + `\operatorname{im}(self) = \operatorname{im}(j)` in all degrees less + than or equal to ``top_dim``. + + .. NOTE:: + + If the algebra for this module is finite, then no ``top_dim`` needs + to be specified in order to ensure that this function terminates. + + TESTS: + + sage: from sage.modules.fp_graded.module import * + sage: A = SteenrodAlgebra(2) + sage: F = FP_Module(A, [0,0]) + sage: L = FP_Module(A, [0,0], [[Sq(3),Sq(0,1)], [0,Sq(2)]]) + sage: f = Hom(F, L)([L([Sq(2),0]), L([0, Sq(2)])]) + sage: f._resolve_image() + Traceback (most recent call last): + ... + ValueError: a top dimension must be specified for this calculation to terminate + sage: f._resolve_image(top_dim=20) + Module homomorphism of degree 0 defined by sending the generators + [<1>] + to + (,) + sage: A3 = SteenrodAlgebra(2, profile=(4,3,2,1)) + sage: f.change_ring(A3)._resolve_image() # long time + Module homomorphism of degree 0 defined by sending the generators + [<1>] + to + (,) + """ + # Let + # + # 1) `j` be a homomorphism into `\im(self)`, and + # 2) 'n' be an integer. + # + # The induction loop starts each iteration assuming that that `j` is onto + # the image in degrees below `n`. Each iteration of the loop then + # extends the map `j` minimally so that `j_n` becomes onto the image. + # + # This induction step is then repeated for all `n \leq` ``top_dim``. + # + + # Create the trivial module F_ to start with. + F_ = self.domain().__class__(self.base_ring(), ()) + j = Hom(F_, self.codomain())(()) + + dim = self.codomain().connectivity() + if dim == PlusInfinity(): + if verbose: + print ('The codomain of the morphism is trivial, so there is nothing to resolve.') + return j + + self_degree = self.degree() + if self_degree is None: + if verbose: + print ('The homomorphism is trivial, so there is nothing to resolve.') + return j + + degree_values = [0] + [v.degree() for v in self.values() if v] + limit = PlusInfinity() if not self.base_ring().is_finite() else\ + (self.base_ring().top_class().degree() + max(degree_values)) + + if not top_dim is None: + limit = min(top_dim, limit) + + if limit == PlusInfinity(): + raise ValueError('a top dimension must be specified for this calculation to terminate') + + if verbose: + if dim > limit: + print('The dimension range is empty: [%d, %d]' % (dim, limit)) + else: + print('Resolving the image in the range of dimensions [%d, %d]:' % (dim, limit), end='') + + for n in range(dim, limit+1): + + if verbose: + print(' %d' % n, end='') + sys.stdout.flush() + + self_n = self.vector_presentation(n - self_degree) + image_n = self_n.image() + + if image_n.dimension() == 0: + continue + + + generator_degrees = tuple((x.degree() for x in F_.generators())) + if j.is_zero(): + # The map j is not onto in degree `n` of the image. + new_generator_degrees = tuple(image_n.dimension()*(n,)) + F_ = self.domain().__class__(self.base_ring(), + generator_degrees + new_generator_degrees) + + new_values = tuple([ + self.codomain().element_from_coordinates(q, n) for q in image_n.basis()]) + + else: + + j_n = j.vector_presentation(n) + Q_n = image_n.quotient(j_n.image()) + + if Q_n.dimension() == 0: + continue + + # The map j is not onto in degree `n` of the image. + new_generator_degrees = tuple(Q_n.dimension()*(n,)) + F_ = self.domain().__class__(self.base_ring(), + generator_degrees + new_generator_degrees) + + new_values = tuple([ + self.codomain().element_from_coordinates(Q_n.lift(q), n) for q in Q_n.basis()]) + + # Create a new homomorphism which is surjective onto the image + # in all degrees less than, and including `n`. + j = Hom(F_, self.codomain()) (j.values() + new_values) + + if verbose: + print('.') + return j + + def to_fp_module(self): + r""" + Create a finitely presented module from this morphism. + + OUTPUT: The finitely presented module having presentation + equal to this morphism, as long as the domain and codomain are free. + + EXAMPLES:: + + sage: from sage.modules.fp_graded.module import FP_Module + sage: A = SteenrodAlgebra(2) + sage: F1 = FP_Module(A, (2,)) + sage: F2 = FP_Module(A, (0,)) + sage: v = F2([Sq(2)]) + sage: pres = Hom(F1, F2)([v]) + sage: M = pres.to_fp_module(); M + Finitely presented left module on 1 generator and 1 relation over mod 2 Steenrod algebra, milnor basis + sage: M.generator_degrees() + (0,) + sage: M.relations() + [] + + sage: F3 = FP_Module(A, (0,), [[Sq(4)]]) + sage: pres = Hom(F1, F3)([v]) + sage: pres.to_fp_module() + Traceback (most recent call last): + ... + ValueError: this is not a morphism between free modules + """ + if self.domain().has_relations() or self.codomain().has_relations(): + raise ValueError("this is not a morphism between free modules") + from .module import FP_Module + return FP_Module(algebra=self.base_ring(), + generator_degrees=self.codomain().generator_degrees(), + relations=tuple([r.dense_coefficient_list() for r in self.values()])) +