# sage_setup: distribution = sagemath-modules
r"""
Morphisms of modules with a basis

This module contains a hierarchy of classes for morphisms of modules
with a basis (category :class:`Modules.WithBasis`):

- :class:`ModuleMorphism`
- :class:`ModuleMorphismByLinearity`
- :class:`ModuleMorphismFromMatrix`
- :class:`ModuleMorphismFromFunction`
- :class:`TriangularModuleMorphism`
- :class:`TriangularModuleMorphismByLinearity`
- :class:`TriangularModuleMorphismFromFunction`

These are internal classes; it is recommended *not* to use them
directly, and instead to construct morphisms through the
:meth:`ModulesWithBasis.ParentMethods.module_morphism` method of the
domain, or through the homset. See the former for an overview
of the possible arguments.

EXAMPLES:

We construct a morphism through the method
:meth:`ModulesWithBasis.ParentMethods.module_morphism`, by specifying
the image of each element of the distinguished basis::

    sage: X = CombinatorialFreeModule(QQ, [1,2,3]);   x = X.basis()
    sage: Y = CombinatorialFreeModule(QQ, [1,2,3,4]); y = Y.basis()
    sage: on_basis = lambda i: Y.monomial(i) + 2*Y.monomial(i+1)

    sage: phi1 = X.module_morphism(on_basis, codomain=Y)
    sage: phi1(x[1])
    B[1] + 2*B[2]

    sage: phi1
    Generic morphism:
      From: Free module generated by {1, 2, 3} over Rational Field
      To:   Free module generated by {1, 2, 3, 4} over Rational Field
    sage: phi1.parent()
    Set of Morphisms from Free module generated by {1, 2, 3} over Rational Field to Free module generated by {1, 2, 3, 4} over Rational Field in Category of finite dimensional vector spaces with basis over Rational Field
    sage: phi1.__class__
    <class 'sage.modules.with_basis.morphism.ModuleMorphismByLinearity_with_category'>

Constructing the same morphism from the homset::

    sage: H = Hom(X,Y)
    sage: phi2 = H(on_basis=on_basis)
    sage: phi1 == phi2
    True

Constructing the same morphism directly using the class; no backward
compatibility is guaranteed in this case::

    sage: from sage.modules.with_basis.morphism import ModuleMorphismByLinearity
    sage: phi3 = ModuleMorphismByLinearity(X, on_basis, codomain=Y)
    sage: phi3 == phi1
    True

.. WARNING::

    The hierarchy of classes implemented in this module is one of the
    first non-trivial hierarchies of classes for morphisms. It is
    hitting a couple scaling issues:

    - There are many independent properties from which module
      morphisms can get code (being defined by linearity, from a
      matrix, or a function; being triangular, being diagonal,
      ...). How to mitigate the class hierarchy growth?

      This will become even more stringent as more properties are
      added (e.g. being defined from generators for an algebra
      morphism, ...)

      Categories, whose primary purpose is to provide infrastructure
      for handling such large hierarchy of classes, can't help at this
      point: there is no category whose morphisms are triangular
      morphisms, and it's not clear such a category would be sensible.

    - How to properly handle ``__init__`` method calls and
      multiple inheritance?

    - Who should be in charge of setting the default category: the
      classes themselves, or
      :meth:`ModulesWithBasis.ParentMethods.module_morphism`?

    Because of this, the hierarchy of classes, and the specific APIs,
    is likely to be refactored as better infrastructure and best
    practices emerge.

AUTHORS:

- Nicolas M. Thiery (2008-2015)

- Jason Bandlow and Florent Hivert (2010): Triangular Morphisms

- Christian Stump (2010): :issue:`9648` module_morphism's to a wider class
  of codomains

Before :issue:`8678`, this hierarchy of classes used to be in
sage.categories.modules_with_basis; see :issue:`8678` for the complete log.
"""

# ****************************************************************************
#  Copyright (C) 2015 Nicolas M. Thiery <nthiery at users.sf.net>
#
# 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.fields import Fields
from sage.categories.modules import Modules
from sage.misc.call import attrcall
# The identity function would deserve a more canonical location
from sage.misc.c3_controlled import identity
from sage.categories.commutative_additive_semigroups import CommutativeAdditiveSemigroups
from sage.categories.homset import Hom
from sage.categories.modules_with_basis import ModulesWithBasis
from sage.categories.morphism import SetMorphism, Morphism
from sage.categories.sets_cat import Sets
from sage.categories.sets_with_partial_maps import SetsWithPartialMaps
from sage.structure.richcmp import op_EQ, op_NE
from sage.structure.element import Matrix
from sage.structure.sage_object import SageObject


class ModuleMorphism(Morphism):
    """
    The top abstract base class for module with basis morphisms.

    INPUT:

    - ``domain`` -- a parent in ``ModulesWithBasis(...)``
    - ``codomain`` -- a parent in ``Modules(...)``
    - ``category`` -- a category or ``None`` (default: ``None``)
    - ``affine`` -- whether we define an affine module morphism
      (default: ``False``)

    Construct a module morphism from ``domain`` to ``codomain`` in the
    category ``category``. By default, the category is the first of
    ``Modules(R).WithBasis().FiniteDimensional()``,
    ``Modules(R).WithBasis()``, ``Modules(R)``,
    ``CommutativeAdditiveMonoids()`` that contains both the domain and
    the codomain. If initializing an affine morphism, then `Sets()` is
    used instead.

    .. SEEALSO::

        - :meth:`ModulesWithBasis.ParentMethods.module_morphism` for
          usage information and examples;
        - :mod:`sage.modules.with_basis.morphism` for a technical
          overview of the classes for module morphisms;
        - :class:`ModuleMorphismFromFunction` and
          :class:`TriangularModuleMorphism`.

    The role of this class is minimal: it provides an :meth:`__init__`
    method which:

    - handles the choice of the default category
    - handles the proper inheritance from categories by updating the
      class of ``self`` upon construction.
    """
    def __init__(self, domain, codomain=None, category=None, affine=False):
        """
        Initialization of module morphisms.

        TESTS::

            sage: X = CombinatorialFreeModule(ZZ, [-2, -1, 1, 2])
            sage: Y = CombinatorialFreeModule(ZZ, [1, 2])
            sage: from sage.modules.with_basis.morphism import ModuleMorphismByLinearity
            sage: phi = ModuleMorphismByLinearity(X, on_basis=Y.monomial * abs)
            sage: TestSuite(phi).run()
        """
        if category is None:
            if domain not in ModulesWithBasis:
                raise ValueError("domain(=%s) should be a module with basis" % codomain)
            base_ring = domain.base_ring()

            if not hasattr(codomain, 'base_ring'):
                raise ValueError("codomain(=%s) needs to have a base_ring attribute" % codomain)
            # codomain should be a module over base_ring
            # The natural test would be ``codomains in Modules(base_ring)``
            # But this is not properly implemented yet::
            #
            #     sage: CC in Modules(QQ)
            #     False
            #     sage: QQ in Modules(QQ)
            #     False
            #     sage: CC[x] in Modules(QQ)
            #     False

            # The test below is a bit more restrictive
            if (not codomain.base_ring().has_coerce_map_from(base_ring)) \
               and (not codomain.has_coerce_map_from(base_ring)):
                raise ValueError("codomain(=%s) should be a module over the base ring of the domain(=%s)" % (codomain, domain))

            if affine:
                # We don't yet have a category whose morphisms are affine morphisms
                category = Sets()
            else:
                C = Modules(base_ring)
                for D in [C.WithBasis().FiniteDimensional(),
                          C.WithBasis(),
                          C,
                          # QQ is not in Modules(QQ)!
                          CommutativeAdditiveSemigroups()]:
                    if codomain in D and domain in D:
                        category = D
                        break
                if category is None:
                    raise ValueError("codomain=(%s) should at least be a commutative additive semigroup")

        H = Hom(domain, codomain, category=category)
        Morphism.__init__(self, H)

        # If needed, this creates the element class from the concrete
        # class (ModuleMorphismByLinearity, DiagonalModuleMorphism,
        # ...) and the abstract classes from the categories.
        #
        # The category infrastructure handles this automatically for
        # parents with a single element class. But for now we still
        # need to do it by hand here, since H may have many different
        # element classes
        if not issubclass(self.__class__, H._abstract_element_class):
            self.__class__ = H.__make_element_class__(self.__class__)


class ModuleMorphismFromFunction(ModuleMorphism, SetMorphism):
    """
    A class for module morphisms implemented by a plain function.

    INPUT:

    - ``domain``, ``codomain``, ``category`` -- as for :class:`ModuleMorphism`

    - ``function`` -- any function or callable from domain to codomain

    .. SEEALSO::

        - :meth:`ModulesWithBasis.ParentMethods.module_morphism` for
          usage information and examples;
        - :mod:`sage.modules.with_basis.morphism` for a technical
          overview of the classes for module morphisms;
        - :class:`ModuleMorphismFromFunction` and
          :class:`TriangularModuleMorphism`.
    """

    def __init__(self, domain, function, codomain=None, category=None):
        """
        TESTS::

            sage: X = CombinatorialFreeModule(ZZ, [1,2,3]); X.rename('X'); x = X.basis()
            sage: from sage.modules.with_basis.morphism import ModuleMorphismFromFunction
            sage: def f(x): return 3*x
            sage: import __main__; __main__.f = f  # Fake f being defined in a python module
            sage: phi = ModuleMorphismFromFunction(X, f, codomain=X)
            sage: phi(x[1])
            3*B[1]
            sage: TestSuite(phi).run()
        """
        # Caveat: This calls Morphism.__init__ twice ...
        ModuleMorphism.__init__(self, domain, codomain, category=category)
        SetMorphism.__init__(self, self.parent(), function)


class ModuleMorphismByLinearity(ModuleMorphism):
    """
    A class for module morphisms obtained by extending a function by linearity.

    INPUT:

    - ``domain``, ``codomain``, ``category`` -- as for :class:`ModuleMorphism`
    - ``on_basis`` -- a function which accepts indices of the basis of
      ``domain`` as ``position``-th argument
    - ``codomain`` -- a parent in ``Modules(...)``
        (default: ``on_basis.codomain()``)
    - ``position`` -- nonnegative integer (default: 0)
    - ``zero`` -- the zero of the codomain (defaults: ``codomain.zero()``)

    .. SEEALSO::

        - :meth:`ModulesWithBasis.ParentMethods.module_morphism` for
          usage information and examples;
        - :mod:`sage.modules.with_basis.morphism` for a technical
          overview of the classes for module morphisms;
        - :class:`ModuleMorphismFromFunction` and
          :class:`TriangularModuleMorphism`.

    .. NOTE::

        ``on_basis`` may alternatively be provided in derived classes
        by passing ``None`` as argument, and implementing or setting
        the attribute ``_on_basis``
    """
    def __init__(self, domain, on_basis=None, codomain=None, category=None,
                 position=0, zero=None):
        """
        TESTS::

            sage: X = CombinatorialFreeModule(ZZ, [-2, -1, 1, 2])
            sage: Y = CombinatorialFreeModule(ZZ, [1, 2])
            sage: from sage.modules.with_basis.morphism import ModuleMorphismByLinearity
            sage: phi = ModuleMorphismByLinearity(X, on_basis=Y.monomial * abs)

            sage: TestSuite(phi).run()
        """
        # Might want to assert that domain is a module with basis
        base_ring = domain.base_ring()

        if codomain is None and hasattr(on_basis, 'codomain'):
            codomain = on_basis.codomain()
        if zero is None:
            zero = codomain.zero()
        self._zero = zero
        self._position = position
        if on_basis is not None:
            self._on_basis = on_basis

        self._is_module_with_basis_over_same_base_ring = \
            codomain in ModulesWithBasis(base_ring) and zero == codomain.zero()

        ModuleMorphism.__init__(self, domain, codomain,
                                category=category,
                                affine=(zero != codomain.zero()))

    def _richcmp_(self, other, op):
        r"""
        Return whether this morphism and ``other`` satisfy ``op``.

        TESTS::

            sage: X = CombinatorialFreeModule(ZZ, [-2, -1, 1, 2])
            sage: Y = CombinatorialFreeModule(ZZ, [1, 2])
            sage: f  = X.module_morphism(on_basis=Y.monomial * abs)
            sage: g  = X.module_morphism(on_basis=Y.monomial * abs)
            sage: h1 = X.module_morphism(on_basis=X.monomial * abs)
            sage: h2 = X.module_morphism(on_basis=X.monomial * factorial)
            sage: h3 = X.module_morphism(on_basis=Y.monomial * abs, category=Modules(ZZ))
            sage: f == g, f == h1, f == h2, f == h3, f == 1, 1 == f
            (True, False, False, False, False, False)
        """
        if op == op_EQ:
            return (self.__class__ is other.__class__
                    and self._zero == other._zero
                    and self._on_basis == other._on_basis
                    and self._position == other._position
                    and self._is_module_with_basis_over_same_base_ring == other._is_module_with_basis_over_same_base_ring)
        if op == op_NE:
            return not (self == other)
        return NotImplemented

    def on_basis(self):
        """
        Return the action of this morphism on basis elements, as per
        :meth:`ModulesWithBasis.Homsets.ElementMethods.on_basis`.

        OUTPUT:

        - a function from the indices of the basis of the domain to the
          codomain

        EXAMPLES::

            sage: X = CombinatorialFreeModule(ZZ, [-2, -1, 1, 2])
            sage: Y = CombinatorialFreeModule(ZZ, [1, 2])
            sage: phi_on_basis = Y.monomial * abs
            sage: phi = sage.modules.with_basis.morphism.ModuleMorphismByLinearity(X, on_basis = phi_on_basis, codomain=Y)
            sage: x = X.basis()
            sage: phi.on_basis()(-2)
            B[2]
            sage: phi.on_basis() == phi_on_basis
            True
        """
        return self._on_basis

    def __call__(self, *args):
        r"""
        Apply this morphism to ``*args``.

        EXAMPLES::

            sage: X = CombinatorialFreeModule(ZZ, [-2, -1, 1, 2])
            sage: Y = CombinatorialFreeModule(ZZ, [1, 2])
            sage: def phi_on_basis(i): return Y.monomial(abs(i))
            sage: phi = sage.modules.with_basis.morphism.ModuleMorphismByLinearity(X, on_basis = Y.monomial * abs, codomain=Y)
            sage: x = X.basis()
            sage: phi(x[1]), phi(x[-2]), phi(x[1] + 3 * x[-2])
            (B[1], B[2], B[1] + 3*B[2])

        .. TODO::

            Add more tests for multi-parameter module morphisms.
        """
        before = args[0:self._position]
        after = args[self._position + 1:len(args)]
        x = args[self._position]
        assert x.parent() is self.domain()

        mc = x.monomial_coefficients(copy=False)
        if self._is_module_with_basis_over_same_base_ring:
            return self.codomain().linear_combination(
                (self._on_basis(*(before + (index,) + after)), coeff)
                for (index, coeff) in mc.items())
        else:
            return sum((coeff * self._on_basis(*(before + (index,) + after))
                        for (index, coeff) in mc.items()), self._zero)

    # As per the specs of Map, we should in fact implement _call_.
    # However we currently need to abuse Map.__call__ (which strict
    # type checking) for multi-parameter module morphisms
    # To be cleaned up
    _call_ = __call__


class TriangularModuleMorphism(ModuleMorphism):
    r"""
    An abstract class for triangular module morphisms.

    Let `X` and `Y` be modules over the same base ring, with
    distinguished bases `F` indexed by `I` and `G` indexed by `J`,
    respectively.

    A module morphism `\phi` from `X` to `Y` is *triangular* if its
    representing matrix in the distinguished bases of `X` and `Y` is
    upper triangular (echelon form).

    More precisely, `\phi` is *upper triangular* w.r.t. a total order
    `<` on `J` if, for any `j\in J`, there exists at most one index
    `i\in I` such that the leading support of `\phi(F_i)` is `j` (see
    :meth:`leading_support()`). We denote by `r(j)` this index,
    setting `r(j)` to ``None`` if it does not exist.

    *Lower triangular* morphisms are defined similarly, taking the
    trailing support instead (see :meth:`trailing_support()`).

    A triangular morphism is *unitriangular* if all its pivots
    (i.e. coefficient of `j` in each `\phi(F[r(j)])`) are `1`.

    INPUT:

    - ``domain`` -- a module with basis `X`
    - ``codomain`` -- a module with basis `Y` (default: `X`)
    - ``category`` -- a category, as for :class:`ModuleMorphism`

    - ``triangular`` -- ``'upper'`` or ``'lower'`` (default: ``'upper'``)
    - ``unitriangular`` -- boolean (default: ``False``)
      As a shorthand, one may use ``unitriangular='lower'``
      for ``triangular='lower', unitriangular=True``.

    - ``key`` -- a comparison key on `J`
      (default: the usual comparison of elements of `J`)

    - ``inverse_on_support`` -- a function `J \to I\cup \{None\}`
      implementing `r` (default: the identity function).
      If set to "compute", the values of `r(j)` are precomputed by
      running through the index set `I` of the basis of the
      domain. This of course requires the domain to be finite
      dimensional.

    - ``invertible`` -- boolean or ``None`` (default: ``None``); can
      be set to specify that `\phi` is known to be (or not to be)
      invertible. If the domain and codomain share the same indexing
      set, this is by default automatically set to ``True`` if
      ``inverse_on_support`` is the identity, or in the finite
      dimensional case.

    .. SEEALSO::

        - :meth:`ModulesWithBasis.ParentMethods.module_morphism` for
          usage information and examples;
        - :mod:`sage.modules.with_basis.morphism` for a technical
          overview of the classes for module morphisms;
        - :class:`ModuleMorphismFromFunction` and
          :class:`TriangularModuleMorphism`.

    OUTPUT: a morphism from `X` to `Y`

    .. WARNING::

        This class is meant to be used as a complement for a concrete
        morphism class.  In particular, the :meth:`__init__` method
        focuses on setting up the data structure describing the
        triangularity of the morphism. It purposely does *not* call
        :meth:`ModuleMorphism.__init__` which should be called
        (directly or indirectly) beforehand.

    EXAMPLES:

    We construct and invert an upper unitriangular module morphism between
    two free `\QQ`-modules::

        sage: I = range(1,200)
        sage: X = CombinatorialFreeModule(QQ, I); X.rename('X'); x = X.basis()
        sage: Y = CombinatorialFreeModule(QQ, I); Y.rename('Y'); y = Y.basis()
        sage: ut = Y.sum_of_monomials * divisors   # This * is map composition.
        sage: phi = X.module_morphism(ut, unitriangular='upper', codomain=Y)
        sage: phi(x[2])
        B[1] + B[2]
        sage: phi(x[6])
        B[1] + B[2] + B[3] + B[6]
        sage: phi(x[30])
        B[1] + B[2] + B[3] + B[5] + B[6] + B[10] + B[15] + B[30]
        sage: phi.preimage(y[2])
        -B[1] + B[2]
        sage: phi.preimage(y[6])
        B[1] - B[2] - B[3] + B[6]
        sage: phi.preimage(y[30])
        -B[1] + B[2] + B[3] + B[5] - B[6] - B[10] - B[15] + B[30]
        sage: (phi^-1)(y[30])
        -B[1] + B[2] + B[3] + B[5] - B[6] - B[10] - B[15] + B[30]

    A lower triangular (but not unitriangular) morphism::

        sage: X = CombinatorialFreeModule(QQ, [1, 2, 3]); X.rename('X'); x = X.basis()
        sage: def lt(i): return sum(j*x[j] for j in range(i, 4))
        sage: phi = X.module_morphism(lt, triangular='lower', codomain=X)
        sage: phi(x[2])
        2*B[2] + 3*B[3]
        sage: phi.preimage(x[2])
        1/2*B[2] - 1/2*B[3]
        sage: phi(phi.preimage(x[2]))
        B[2]

    Using the ``key`` keyword, we can use triangularity even if
    the map becomes triangular only after a permutation of the basis::

        sage: X = CombinatorialFreeModule(QQ, [1, 2, 3]); X.rename('X'); x = X.basis()
        sage: def ut(i): return (x[1] + x[2] if i == 1 else x[2] + (x[3] if i == 3 else 0))
        sage: perm = [0, 2, 1, 3]
        sage: phi = X.module_morphism(ut, triangular='upper', codomain=X,
        ....:                         key=lambda a: perm[a])
        sage: [phi(x[i]) for i in range(1, 4)]
        [B[1] + B[2], B[2], B[2] + B[3]]
        sage: [phi.preimage(x[i]) for i in range(1, 4)]
        [B[1] - B[2], B[2], -B[2] + B[3]]

    The same works in the lower-triangular case::

        sage: def lt(i): return (x[1] + x[2] + x[3] if i == 2 else x[i])
        sage: phi = X.module_morphism(lt, triangular='lower', codomain=X,
        ....:                         key=lambda a: perm[a])
        sage: [phi(x[i]) for i in range(1, 4)]
        [B[1], B[1] + B[2] + B[3], B[3]]
        sage: [phi.preimage(x[i]) for i in range(1, 4)]
        [B[1], -B[1] + B[2] - B[3], B[3]]

    An injective but not surjective morphism cannot be inverted,
    but the ``inverse_on_support`` keyword allows Sage to find a
    partial inverse::

        sage: X = CombinatorialFreeModule(QQ, [1,2,3]); x = X.basis()
        sage: Y = CombinatorialFreeModule(QQ, [1,2,3,4,5]); y = Y.basis()
        sage: ult = lambda i: sum(  y[j] for j in range(i+1,6)  )
        sage: phi = X.module_morphism(ult, unitriangular='lower', codomain=Y,
        ....:        inverse_on_support=lambda i: i-1 if i in [2,3,4] else None)
        sage: phi(x[2])
        B[3] + B[4] + B[5]
        sage: phi.preimage(y[3])
        B[2] - B[3]

    The ``inverse_on_support`` keyword can also be used if the
    bases of the domain and the codomain are identical but one of
    them has to be permuted in order to render the morphism
    triangular. For example::

        sage: X = CombinatorialFreeModule(QQ, [1, 2, 3]); X.rename('X'); x = X.basis()
        sage: def ut(i):
        ....:     return (x[3] if i == 1 else x[1] if i == 2
        ....:             else x[1] + x[2])
        sage: def perm(i):
        ....:     return (2 if i == 1 else 3 if i == 2 else 1)
        sage: phi = X.module_morphism(ut, triangular='upper', codomain=X,
        ....:                         inverse_on_support=perm)
        sage: [phi(x[i]) for i in range(1, 4)]
        [B[3], B[1], B[1] + B[2]]
        sage: [phi.preimage(x[i]) for i in range(1, 4)]
        [B[2], -B[2] + B[3], B[1]]

    The same works if the permutation induces lower triangularity::

        sage: X = CombinatorialFreeModule(QQ, [1, 2, 3]); X.rename('X'); x = X.basis()
        sage: def lt(i):
        ....:     return (x[3] if i == 1 else x[2] if i == 2
        ....:             else x[1] + x[2])
        sage: def perm(i):
        ....:     return 4 - i
        sage: phi = X.module_morphism(lt, triangular='lower', codomain=X,
        ....:                         inverse_on_support=perm)
        sage: [phi(x[i]) for i in range(1, 4)]
        [B[3], B[2], B[1] + B[2]]
        sage: [phi.preimage(x[i]) for i in range(1, 4)]
        [-B[2] + B[3], B[2], B[1]]

    In the finite dimensional case, one can ask Sage to recover
    ``inverse_on_support`` by a precomputation::

        sage: X = CombinatorialFreeModule(QQ, [1, 2, 3]); x = X.basis()
        sage: Y = CombinatorialFreeModule(QQ, [1, 2, 3, 4]); y = Y.basis()
        sage: ut = lambda i: sum(  y[j] for j in range(1,i+2) )
        sage: phi = X.module_morphism(ut, triangular='upper', codomain=Y,
        ....:                         inverse_on_support='compute')
        sage: tx = "{} {} {}"
        sage: for j in Y.basis().keys():
        ....:     i = phi._inverse_on_support(j)
        ....:     print(tx.format(j, i, phi(x[i]) if i is not None else None))
        1 None None
        2 1 B[1] + B[2]
        3 2 B[1] + B[2] + B[3]
        4 3 B[1] + B[2] + B[3] + B[4]

    The ``inverse_on_basis`` and ``key`` keywords can be combined::

        sage: X = CombinatorialFreeModule(QQ, [1, 2, 3]); X.rename('X')
        sage: x = X.basis()
        sage: def ut(i):
        ....:     return (2*x[2] + 3*x[3] if i == 1
        ....:             else x[1] + x[2] + x[3] if i == 2
        ....:             else 4*x[2])
        sage: def perm(i):
        ....:     return (2 if i == 1 else 3 if i == 2 else 1)
        sage: perverse_key = lambda a: (a - 2) % 3
        sage: phi = X.module_morphism(ut, triangular='upper', codomain=X,
        ....:                         inverse_on_support=perm, key=perverse_key)
        sage: [phi(x[i]) for i in range(1, 4)]
        [2*B[2] + 3*B[3], B[1] + B[2] + B[3], 4*B[2]]
        sage: [phi.preimage(x[i]) for i in range(1, 4)]
        [-1/3*B[1] + B[2] - 1/12*B[3], 1/4*B[3], 1/3*B[1] - 1/6*B[3]]
    """
    def __init__(self, triangular='upper', unitriangular=False,
                 key=None, inverse=None, inverse_on_support=identity, invertible=None):
        """
        TESTS::

            sage: X = CombinatorialFreeModule(QQ, [1, 2, 3]); X.rename('X'); x = X.basis()
            sage: def lt(i): return sum(j*x[j] for j in range(i, 4))
            sage: import __main__; __main__.lt = lt  # Fake lt being defined in a python module
            sage: phi = X.module_morphism(lt, triangular='lower', codomain=X)
            sage: phi.__class__
            <class 'sage.modules.with_basis.morphism.TriangularModuleMorphismByLinearity_with_category'>
            sage: phi._invertible
            True
            sage: TestSuite(phi).run()

        With the option ``compute``::

            sage: phi = X.module_morphism(lt, triangular='lower', codomain=X,
            ....:                         inverse_on_support='compute')
            sage: TestSuite(phi).run(skip=["_test_pickling"])

        Pickling works in Python3 (:issue:`17957`)::

            sage: phi = X.module_morphism(lt, triangular='lower', codomain=X,
            ....:                         inverse_on_support='compute')
            sage: loads(dumps(phi))
            Generic endomorphism of X
            sage: phi._inverse_on_support
            <built-in method get of dict object at ...>
            sage: ldp = loads(dumps(phi._inverse_on_support))
            sage: [ldp(i) == phi._inverse_on_support(i) for i in range(1, 4)]
            [True, True, True]
        """
        if key is not None:
            self._key_kwds = {"key": key}
        else:
            self._key_kwds = {}

        if triangular == "upper":
            self._dominant_item = attrcall("leading_item", **self._key_kwds)
        else:
            self._dominant_item = attrcall("trailing_item", **self._key_kwds)
        # We store those two just be able to pass them down to the inverse function
        self._triangular = triangular

        domain = self.domain()
        codomain = self.codomain()
        on_basis = self.on_basis()
        self._unitriangular = unitriangular
        self._inverse = inverse

        if inverse_on_support == "compute":
            inverse_on_support = {self._dominant_item(on_basis(i))[0]: i
                                  for i in self.domain().basis().keys()
                                  }.get

        self._inverse_on_support = inverse_on_support

        if invertible is None and (domain.basis().keys() == codomain.basis().keys()) and \
           (self._inverse_on_support == identity or domain in Modules.FiniteDimensional):
            invertible = True
        self._invertible = invertible

    def _richcmp_(self, other, op):
        r"""
        Return whether this morphism and ``other`` satisfy ``op``.

        TESTS::

            sage: X = CombinatorialFreeModule(QQ, [1, 2, 3]); X.rename('X'); x = X.basis()
            sage: def ut(i): return (x[1] + x[2] if i == 1 else x[2] + (x[3] if i == 3 else 0))
            sage: perm = [0, 2, 1, 3]
            sage: our_key = lambda a: perm[a]
            sage: phi = X.module_morphism(ut, triangular='upper', codomain=X, key=our_key)
            sage: def ut2(i): return (x[1] + 7*x[2] if i == 1 else x[2] + (x[3] if i == 3 else 0))
            sage: phi2 = X.module_morphism(ut2, triangular='upper', codomain=X, key=our_key)
            sage: def lt(i): return (x[1] + x[2] + x[3] if i == 2 else x[i])
            sage: psi = X.module_morphism(lt, triangular='lower', codomain=X, key=our_key)
            sage: phi == phi
            True
            sage: phi == phi2
            False
            sage: phi == psi
            False
        """
        if op == op_EQ:
            return (self.__class__ is other.__class__
                    and self._triangular == other._triangular
                    and self._unitriangular == other._unitriangular
                    and self._inverse_on_support == other._inverse_on_support
                    and self._invertible == other._invertible
                    and self._dominant_item == other._dominant_item)
        if op == op_NE:
            return not (self == other)
        return NotImplemented

    def _test_triangular(self, **options):
        """
        Test that ``self`` is actually triangular.

        See also: :class:`sage.misc.sage_unittest.TestSuite`.

        EXAMPLES::

            sage: X = CombinatorialFreeModule(QQ, [1, 2, 3]); x = X.basis()
            sage: Y = CombinatorialFreeModule(QQ, [1, 2, 3]); y = Y.basis()
            sage: lt = lambda i: sum(  y[j] for j in range(i,4)  )
            sage: phi = X.module_morphism(lt, triangular='lower', codomain=Y)
            sage: phi._test_triangular()

            sage: lt = lambda i: sum(  y[j] for j in range(i+1,4)  )
            sage: phi = X.module_morphism(lt, triangular='lower', codomain=Y)
            sage: phi._test_triangular()
            Traceback (most recent call last):
            ...
            AssertionError: morphism is not triangular on 1

            sage: X = CombinatorialFreeModule(QQ, [1,2,3]); x = X.basis()
            sage: Y = CombinatorialFreeModule(QQ, [1,2,3,4,5]); y = Y.basis()
            sage: ult = lambda i: sum(  y[j] for j in range(i+1,6)  )
            sage: phi = X.module_morphism(ult, unitriangular='lower', codomain=Y,
            ....:      inverse_on_support=lambda i: i-1 if i in [2,3,4] else None)
            sage: phi._test_triangular()

            sage: ult = lambda i: sum(  2*y[j] for j in range(i+1,6)  )
            sage: phi = X.module_morphism(ult, unitriangular='lower', codomain=Y,
            ....:      inverse_on_support=lambda i: i-1 if i in [2,3,4] else None)
            sage: phi._test_triangular()
            Traceback (most recent call last):
            ...
            AssertionError: morphism is not unitriangular on 1
        """
        from sage.misc.lazy_format import LazyFormat
        tester = self._tester(**options)
        on_basis = self.on_basis()
        for x in self.domain().basis().keys().some_elements():
            # is there any better set to use ?
            bs, co = self._dominant_item(on_basis(x))
            if self._unitriangular:
                tester.assertEqual(co, self.domain().base_ring().one(),
                    LazyFormat("morphism is not unitriangular on %s") % x)
            xback = self._inverse_on_support(bs)
            tester.assertEqual(x, xback,
                LazyFormat("morphism is not triangular on %s") % x)

    def __invert__(self):
        """
        Return the triangular morphism which is the inverse of ``self``.

        Raises an error if ``self`` is not invertible.

        TESTS::

            sage: X = CombinatorialFreeModule(QQ, [1, 2, 3]); x = X.basis()
            sage: Y = CombinatorialFreeModule(QQ, [1, 2, 3]); y = Y.basis()
            sage: uut = lambda i: sum(  y[j] for j in range(1,i+1)) # uni-upper
            sage: ult = lambda i: sum(  y[j] for j in range(i,4)  ) # uni-lower
            sage: ut =  lambda i: sum(j*y[j] for j in range(1,i+1)) # upper
            sage: lt =  lambda i: sum(j*y[j] for j in range(i,4  )) # lower
            sage: f_uut = X.module_morphism(uut, codomain=Y,
            ....:                           unitriangular='upper')
            sage: f_ult = X.module_morphism(ult, codomain=Y,
            ....:                           unitriangular='lower')
            sage: f_ut  = X.module_morphism(ut, codomain=Y, triangular='upper')
            sage: f_lt  = X.module_morphism(lt, codomain=Y, triangular='lower')
            sage: (~f_uut)(y[2])
            -B[1] + B[2]
            sage: (~f_ult)(y[2])
            B[2] - B[3]
            sage: (~f_ut)(y[2])
            -1/2*B[1] + 1/2*B[2]
            sage: (~f_lt)(y[2])
            1/2*B[2] - 1/2*B[3]
        """
        if self._invertible is True:
            return self.section()
        elif self._invertible is False:
            raise ValueError("Non invertible morphism")
        else:
            raise ValueError("Morphism not known to be invertible; see the invertible option of module_morphism")

    def section(self):
        """
        Return the section (partial inverse) of ``self``.

        This returns a partial triangular morphism which is a section of
        ``self``. The section morphism raises a :exc:`ValueError` if
        asked to apply on an element which is not in the image of ``self``.

        EXAMPLES::

            sage: X = CombinatorialFreeModule(QQ, [1,2,3]); x = X.basis()
            sage: X.rename('X')
            sage: Y = CombinatorialFreeModule(QQ, [1,2,3,4,5]); y = Y.basis()
            sage: ult = lambda i: sum(  y[j] for j in range(i+1,6)  ) # uni-lower
            sage: phi = X.module_morphism(ult, triangular='lower', codomain=Y,
            ....:      inverse_on_support=lambda i: i-1 if i in [2,3,4] else None)
            sage: ~phi
            Traceback (most recent call last):
            ...
            ValueError: Morphism not known to be invertible;
            see the invertible option of module_morphism
            sage: phiinv = phi.section()
            sage: list(map(phiinv*phi, X.basis().list())) == X.basis().list()
            True
            sage: phiinv(Y.basis()[1])
            Traceback (most recent call last):
            ...
            ValueError: B[1] is not in the image
        """
        if self._inverse is not None:
            return self._inverse
        if self._inverse_on_support == identity:
            retract_dom = None
        else:
            on_basis = self.on_basis()

            def retract_dom(i):
                self._dominant_item(on_basis(i))[0]

        if self._invertible:
            return self.__class__(
                domain=self.codomain(),
                on_basis=self._invert_on_basis,
                codomain=self.domain(), category=self.category_for(),
                unitriangular=self._unitriangular, triangular=self._triangular,
                inverse=self, inverse_on_support=retract_dom,
                invertible=self._invertible, **self._key_kwds)
        else:
            return SetMorphism(Hom(self.codomain(), self.domain(),
                                   SetsWithPartialMaps()),
                               self.preimage)

    # This should be removed and optimized as soon as triangular
    # morphisms not defined by linearity are available
    # (the inverse should not be computed on the basis).
    def _invert_on_basis(self, i):
        r"""
        Return the image, by the inverse of ``self``, of the basis element
        indexed by ``i``.

        TESTS::

            sage: X = CombinatorialFreeModule(QQ, [1, 2, 3]); x = X.basis()
            sage: Y = CombinatorialFreeModule(QQ, [1, 2, 3]); y = Y.basis()
            sage: ult = lambda i: sum(  y[j] for j in range(i,4)  ) # uni-lower
            sage: phi = X.module_morphism(ult, triangular='lower', codomain=Y)
            sage: phi._invert_on_basis(2)
            B[2] - B[3]
        """
        return self.preimage(self.codomain().monomial(i))

    def preimage(self, f):
        r"""
        Return the preimage of `f` under ``self``.

        EXAMPLES::

            sage: X = CombinatorialFreeModule(QQ, [1, 2, 3]); x = X.basis()
            sage: Y = CombinatorialFreeModule(QQ, [1, 2, 3]); y = Y.basis()
            sage: ult = lambda i: sum(  y[j] for j in range(i,4)  ) # uni-lower
            sage: phi = X.module_morphism(ult, triangular='lower', codomain=Y)
            sage: phi.preimage(y[1] + y[2])
            B[1] - B[3]

        The morphism need not be surjective. In the following example,
        the codomain is of larger dimension than the domain::

            sage: X = CombinatorialFreeModule(QQ, [1, 2, 3]); x = X.basis()
            sage: Y = CombinatorialFreeModule(QQ, [1, 2, 3, 4]); y = Y.basis()
            sage: lt = lambda i: sum(  y[j] for j in range(i,5)  )
            sage: phi = X.module_morphism(lt, triangular='lower', codomain=Y)
            sage: phi.preimage(y[1] + y[2])
            B[1] - B[3]

        Here are examples using ``inverse_on_support`` to handle a
        morphism that shifts the leading indices by `1`::

            sage: X = CombinatorialFreeModule(QQ, [1, 2, 3]); x = X.basis()
            sage: Y = CombinatorialFreeModule(QQ, [1, 2, 3, 4, 5]); y = Y.basis()
            sage: lt = lambda i: sum(  y[j] for j in range(i+1,6)  ) # lower
            sage: phi = X.module_morphism(lt, triangular='lower', codomain=Y,
            ....:         inverse_on_support=lambda i: i-1 if i in [2,3,4] else None)
            sage: phi(x[1])
            B[2] + B[3] + B[4] + B[5]
            sage: phi(x[3])
            B[4] + B[5]
            sage: phi.preimage(y[2] + y[3])
            B[1] - B[3]
            sage: phi(phi.preimage(y[2] + y[3])) == y[2] + y[3]
            True
            sage: el = x[1] + 3*x[2] + 2*x[3]
            sage: phi.preimage(phi(el)) == el
            True

            sage: phi.preimage(y[1])
            Traceback (most recent call last):
            ...
            ValueError: B[1] is not in the image
            sage: phi.preimage(y[4])
            Traceback (most recent call last):
            ...
            ValueError: B[4] is not in the image

        Over a base ring like `\ZZ`, the morphism need not be
        surjective even when the dimensions match::

            sage: X = CombinatorialFreeModule(ZZ, [1, 2, 3]); x = X.basis()
            sage: Y = CombinatorialFreeModule(ZZ, [1, 2, 3]); y = Y.basis()
            sage: lt = lambda i: sum( 2* y[j] for j in range(i,4)  ) # lower
            sage: phi = X.module_morphism(lt, triangular='lower', codomain=Y)
            sage: phi.preimage(2*y[1] + 2*y[2])
            B[1] - B[3]

        The error message in case of failure could be more specific though::

            sage: phi.preimage(y[1] + y[2])
            Traceback (most recent call last):
              ...
            TypeError: no conversion of this rational to integer
        """
        F = self.domain()
        G = self.codomain()
        on_basis = self.on_basis()
        if f not in G:
            raise ValueError("f(={}) must be in the codomain of the morphism to have a preimage under the latter".format(f))

        remainder = f

        out = F.zero()
        while not remainder.is_zero():
            (j, c) = self._dominant_item(remainder)

            j_preimage = self._inverse_on_support(j)
            if j_preimage is None:
                raise ValueError("{} is not in the image".format(f))
            s = on_basis(j_preimage)
            if not j == self._dominant_item(s)[0]:
                raise ValueError("The morphism (={}) is not triangular at {}, and therefore a preimage cannot be computed".format(f, s))

            if not self._unitriangular:
                # What's the appropriate way to request an exact
                # division within the base ring and get an error if
                # this is not possible?
                c = c.parent()(c / s[j])

            # Before this was ``remainder -= s._lmul_(c)`` for speed, but
            #   not every module implements scalar multiplication this way.
            remainder -= s * c
            out += F.term(j_preimage, c)

        return out

    def coreduced(self, y):
        r"""
        Return `y` reduced w.r.t. the image of ``self``.

        INPUT:

        - ``self`` -- a triangular morphism over a field, or a
          unitriangular morphism over a ring
        - ``y`` -- an element of the codomain of ``self``

        Suppose that ``self`` is a morphism from `X` to `Y`. Then, for
        any `y \in Y`, the call ``self.coreduced(y)`` returns a
        normal form for `y` in the quotient `Y / I` where `I` is the
        image of ``self``.

        EXAMPLES::

            sage: X = CombinatorialFreeModule(QQ, [1,2,3]); x = X.basis()
            sage: Y = CombinatorialFreeModule(QQ, [1,2,3,4,5]); y = Y.basis()
            sage: ult = lambda i: sum(  y[j] for j in range(i+1,6)  )
            sage: phi = X.module_morphism(ult, unitriangular='lower', codomain=Y,
            ....:        inverse_on_support=lambda i: i-1 if i in [2,3,4] else None)
            sage: [phi(v) for v in X.basis()]
            [B[2] + B[3] + B[4] + B[5],
                    B[3] + B[4] + B[5],
                           B[4] + B[5]]
            sage: [phi.coreduced(y[1]-2*y[4])]
            [B[1] + 2*B[5]]
            sage: [phi.coreduced(v) for v in y]
            [B[1], 0, 0, -B[5], B[5]]

        Now with a non unitriangular morphism::

            sage: lt = lambda i: sum( j*y[j] for j in range(i+1,6)  )
            sage: phi = X.module_morphism(lt, triangular='lower', codomain=Y,
            ....:       inverse_on_support=lambda i: i-1 if i in [2,3,4] else None)
            sage: [phi(v) for v in X.basis()]
            [2*B[2] + 3*B[3] + 4*B[4] + 5*B[5],
                      3*B[3] + 4*B[4] + 5*B[5],
                               4*B[4] + 5*B[5]]
            sage: [phi.coreduced(y[1]-2*y[4])]
            [B[1] + 5/2*B[5]]
            sage: [phi.coreduced(v) for v in y]
            [B[1], 0, 0, -5/4*B[5], B[5]]

        For general rings, this method is only implemented for
        unitriangular morphisms::

            sage: X = CombinatorialFreeModule(ZZ, [1,2,3]); x = X.basis()
            sage: Y = CombinatorialFreeModule(ZZ, [1,2,3,4,5]); y = Y.basis()
            sage: phi = X.module_morphism(ult, unitriangular='lower', codomain=Y,
            ....:       inverse_on_support=lambda i: i-1 if i in [2,3,4] else None)
            sage: [phi.coreduced(y[1]-2*y[4])]
            [B[1] + 2*B[5]]
            sage: [phi.coreduced(v) for v in y]
            [B[1], 0, 0, -B[5], B[5]]

            sage: phi = X.module_morphism(lt, triangular='lower', codomain=Y,
            ....:       inverse_on_support=lambda i: i-1 if i in [2,3,4] else None)
            sage: [phi.coreduced(v) for v in y]
            Traceback (most recent call last):
            ...
            NotImplementedError: coreduce for a triangular but not unitriangular morphism over a ring

        .. NOTE:: Before :issue:`8678` this method used to be called co_reduced.
        """
        G = self.codomain()
        if G.base_ring() not in Fields() and not self._unitriangular:
            raise NotImplementedError("coreduce for a triangular but not unitriangular morphism over a ring")
        on_basis = self.on_basis()
        assert y in G

        result = G.zero()
        remainder = y

        while not remainder.is_zero():
            (j, c) = self._dominant_item(remainder)
            j_preimage = self._inverse_on_support(j)
            if j_preimage is None:
                dom_term = G.term(j, c)
                remainder -= dom_term
                result += dom_term
            else:
                s = on_basis(j_preimage)
                assert j == self._dominant_item(s)[0]
                if not self._unitriangular:
                    c = c / s[j]  # the base ring is a field
                # Before this was ``remainder -= s._lmul_(c)`` for speed, but
                #   not every module implements scalar multiplication this way.
                remainder -= s * c
        return result

    def cokernel_basis_indices(self):
        r"""
        Return the indices of the natural monomial basis of the cokernel of ``self``.

        INPUT:

        - ``self`` -- a triangular morphism over a field or a
          unitriangular morphism over a ring, with a finite
          dimensional codomain.

        OUTPUT:

        A list `E` of indices of the basis `(B_e)_e` of the codomain
        of ``self`` so that `(B_e)_{e\in E}` forms a basis of a
        supplementary of the image set of ``self``.

        Thinking of this triangular morphism as a row echelon matrix,
        this returns the complementary of the characteristic
        columns. Namely `E` is the set of indices which do not appear
        as leading support of some element of the image set of
        ``self``.

        EXAMPLES::

            sage: X = CombinatorialFreeModule(ZZ, [1,2,3]); x = X.basis()
            sage: Y = CombinatorialFreeModule(ZZ, [1,2,3,4,5]); y = Y.basis()
            sage: uut = lambda i: sum(  y[j] for j in range(i+1,6)  ) # uni-upper
            sage: phi = X.module_morphism(uut, unitriangular='upper', codomain=Y,
            ....:       inverse_on_support=lambda i: i-1 if i in [2,3,4] else None)
            sage: phi.cokernel_basis_indices()
            [1, 5]

            sage: phi = X.module_morphism(uut, triangular='upper', codomain=Y,
            ....:       inverse_on_support=lambda i: i-1 if i in [2,3,4] else None)
            sage: phi.cokernel_basis_indices()
            Traceback (most recent call last):
            ...
            NotImplementedError: cokernel_basis_indices for a triangular but not unitriangular morphism over a ring

            sage: Y = CombinatorialFreeModule(ZZ, NN); y = Y.basis()
            sage: phi = X.module_morphism(uut, unitriangular='upper', codomain=Y,
            ....:       inverse_on_support=lambda i: i-1 if i in [2,3,4] else None)
            sage: phi.cokernel_basis_indices()
            Traceback (most recent call last):
            ...
            NotImplementedError: cokernel_basis_indices implemented only for morphisms with a finite dimensional codomain
        """
        R = self.domain().base_ring()
        if R not in Fields() and not self._unitriangular:
            raise NotImplementedError("cokernel_basis_indices for a triangular but not unitriangular morphism over a ring")
        if self.codomain() not in Modules(R).FiniteDimensional():
            raise NotImplementedError("cokernel_basis_indices implemented only for morphisms with a finite dimensional codomain")
        return [i for i in self.codomain().basis().keys() if self._inverse_on_support(i) is None]

    def cokernel_projection(self, category=None):
        """
        Return a projection on the co-kernel of ``self``.

        INPUT:

        - ``category`` -- the category of the result

        EXAMPLES::

            sage: X = CombinatorialFreeModule(QQ, [1,2,3]); x = X.basis()
            sage: Y = CombinatorialFreeModule(QQ, [1,2,3,4,5]); y = Y.basis()
            sage: lt = lambda i: sum(  y[j] for j in range(i+1,6)  ) # lower
            sage: phi = X.module_morphism(lt, triangular='lower', codomain=Y,
            ....:      inverse_on_support=lambda i: i-1 if i in [2,3,4] else None)
            sage: phipro = phi.cokernel_projection()
            sage: phipro(y[1] + y[2])
            B[1]
            sage: all(phipro(phi(x)).is_zero() for x in X.basis())
            True
            sage: phipro(y[1])
            B[1]
            sage: phipro(y[4])
            -B[5]
            sage: phipro(y[5])
            B[5]
        """
        codomain = self.codomain()
        category = ModulesWithBasis(codomain.base_ring()).or_subcategory(category)
        return codomain.module_morphism(function=self.coreduced,
                                        codomain=codomain, category=category)


class TriangularModuleMorphismByLinearity(ModuleMorphismByLinearity, TriangularModuleMorphism):
    r"""
    A concrete class for triangular module morphisms obtained by extending a function by linearity.

    .. SEEALSO::

        - :meth:`ModulesWithBasis.ParentMethods.module_morphism` for
          usage information and examples;
        - :mod:`sage.modules.with_basis.morphism` for a technical
          overview of the classes for module morphisms;
        - :class:`ModuleMorphismByLinearity` and
          :class:`TriangularModuleMorphism`.
    """
    def __init__(self, domain, on_basis, codomain=None, category=None, **keywords):
        r"""
        TESTS::

            sage: X = CombinatorialFreeModule(QQ, ZZ)
            sage: from sage.modules.with_basis.morphism import TriangularModuleMorphismByLinearity
            sage: def on_basis(i): return X.sum_of_monomials(range(i-2, i+1))
            sage: import __main__; __main__.on_basis = on_basis  # Fake on_basis being defined in a python module
            sage: phi = TriangularModuleMorphismByLinearity(
            ....:           X, on_basis=on_basis, codomain=X)
            sage: TestSuite(phi).run(skip=["_test_nonzero_equal"])
        """
        ModuleMorphismByLinearity.__init__(self, on_basis=on_basis,
                                           domain=domain, codomain=codomain, category=category)
        TriangularModuleMorphism.__init__(self, **keywords)

    def _richcmp_(self, other, op):
        r"""
        Return whether this morphism and ``other`` satisfy ``op``.

        TESTS::

            sage: X = CombinatorialFreeModule(QQ, ZZ)
            sage: from sage.modules.with_basis.morphism import TriangularModuleMorphismByLinearity
            sage: def on_basis(i): return X.sum_of_monomials(range(i-2, i+1))
            sage: phi = TriangularModuleMorphismByLinearity(
            ....:           X, on_basis=on_basis, codomain=X)
            sage: phi == phi
            True
        """
        if op == op_EQ:
            return (ModuleMorphismByLinearity._richcmp_(self, other, op)
                    and TriangularModuleMorphism._richcmp_(self, other, op))
        if op == op_NE:
            return not (self == other)
        return NotImplemented


class TriangularModuleMorphismFromFunction(ModuleMorphismFromFunction, TriangularModuleMorphism):
    r"""
    A concrete class for triangular module morphisms implemented by a function.

    .. SEEALSO::

        - :meth:`ModulesWithBasis.ParentMethods.module_morphism` for
          usage information and examples;
        - :mod:`sage.modules.with_basis.morphism` for a technical
          overview of the classes for module morphisms;
        - :class:`ModuleMorphismFromFunction` and
          :class:`TriangularModuleMorphism`.
    """
    def __init__(self, domain, function, codomain=None, category=None, **keywords):
        r"""
        TESTS::

            sage: X = CombinatorialFreeModule(QQ, [0,1,2,3,4])
            sage: from sage.modules.with_basis.morphism import TriangularModuleMorphismFromFunction
            sage: def f(x): return x + X.term(0, sum(x.coefficients()))
            sage: import __main__; __main__.f = f  # Fake f being defined in a python module
            sage: phi = TriangularModuleMorphismFromFunction(
            ....:           X, function=f, codomain=X)
            sage: TestSuite(phi).run()
        """
        ModuleMorphismFromFunction.__init__(self, function=function,
                                            domain=domain, codomain=codomain,
                                            category=category)
        TriangularModuleMorphism.__init__(self, **keywords)


class ModuleMorphismFromMatrix(ModuleMorphismByLinearity):
    r"""
    A class for module morphisms built from a matrix in the
    distinguished bases of the domain and codomain.

    .. SEEALSO::

        - :meth:`ModulesWithBasis.ParentMethods.module_morphism`
        - :meth:`ModulesWithBasis.FiniteDimensional.MorphismMethods.matrix`

    INPUT:

    - ``domain``, ``codomain`` -- two finite dimensional modules over
      the same base ring `R` with basis `F` and `G`, respectively

    - ``matrix`` -- a matrix with base ring `R` and dimensions
      matching that of `F` and `G`, respectively

    - ``side`` -- ``'left'`` or ``'right'`` (default: ``'left'``)

        If ``side`` is "left", this morphism is considered as
        acting on the left; i.e. each column of the matrix
        represents the image of an element of the basis of the
        domain.

    - ``category`` -- a category or ``None`` (default: ``None``)

    EXAMPLES::

        sage: X = CombinatorialFreeModule(ZZ, [1,2]); X.rename('X'); x = X.basis()
        sage: Y = CombinatorialFreeModule(ZZ, [3,4]); Y.rename('Y'); y = Y.basis()
        sage: m = matrix([[1,2],[3,5]])
        sage: phi = X.module_morphism(matrix=m, codomain=Y)
        sage: phi.parent()
        Set of Morphisms from X to Y in Category of finite dimensional modules with basis over Integer Ring
        sage: phi.__class__
        <class 'sage.modules.with_basis.morphism.ModuleMorphismFromMatrix_with_category'>
        sage: phi(x[1])
        B[3] + 3*B[4]
        sage: phi(x[2])
        2*B[3] + 5*B[4]

        sage: m = matrix([[1,2],[3,5]])
        sage: phi = X.module_morphism(matrix=m, codomain=Y, side='right',
        ....:                         category=Modules(ZZ).WithBasis())
        sage: phi.parent()
        Set of Morphisms from X to Y
        in Category of modules with basis over Integer Ring
        sage: phi(x[1])
        B[3] + 2*B[4]
        sage: phi(x[2])
        3*B[3] + 5*B[4]

    .. TODO::

        Possibly implement rank, addition, multiplication, matrix,
        etc, from the stored matrix.
    """
    def __init__(self, domain, matrix, codomain=None, category=None, side='left'):
        r"""
        Initialize ``self``.

        TESTS::

            sage: from sage.modules.with_basis.morphism import ModuleMorphismFromMatrix
            sage: X = CombinatorialFreeModule(ZZ, [1,2]); X.rename('X'); x = X.basis()
            sage: Y = CombinatorialFreeModule(ZZ, [3,4]); Y.rename('Y'); y = Y.basis()
            sage: m = matrix([[1,2],[3,5]])
            sage: phi = ModuleMorphismFromMatrix(matrix=m, domain=X, codomain=Y, side='right')
            sage: phi.__class__
            <class 'sage.modules.with_basis.morphism.ModuleMorphismFromMatrix_with_category'>
            sage: phi.matrix(side='right') == m
            True
            sage: TestSuite(phi).run(skip=["_test_pickling"])

        Pickling works (:issue:`17957`) in Python 3::

            sage: phi._on_basis
            <built-in method __getitem__ of dict object at ...>
            sage: loads(dumps(phi)) == phi
            True

        The matrix is stored in the morphism, as if it was for an
        action on the right::

            sage: phi._matrix
            [1 2]
            [3 5]

            sage: phi = ModuleMorphismFromMatrix(matrix=m, side='left',
            ....:                                domain=X, codomain=Y)
            sage: phi._matrix
            [1 3]
            [2 5]
        """
        C = ModulesWithBasis(domain.base_ring()).FiniteDimensional()
        if domain not in C:
            raise ValueError("The domain %s should be finite dimensional" % domain)
        if codomain is None:
            raise ValueError("The codomain %s should be specified")
        if codomain not in C:
            raise ValueError("The codomain %s should be finite dimensional" % codomain)
        if not isinstance(matrix, Matrix):
            raise ValueError("matrix (=%s) should be a matrix" % matrix)
        import sage.combinat.ranker
        indices = tuple(domain.basis().keys())
        rank_domain = sage.combinat.ranker.rank_from_list(indices)
        if side == "left":
            matrix = matrix.transpose()
        if matrix.nrows() != len(indices):
            raise ValueError("The dimension of the matrix (%s) does not match with the dimension of the domain (%s)"
                             % (matrix.nrows(), len(indices)))
        if matrix.ncols() != codomain.dimension():
            raise ValueError("The dimension of the matrix (%s) does not match with the dimension of the codomain (%s)"
                             % (matrix.ncols(), codomain.dimension()))
        self._matrix = matrix
        d = {xt: codomain.from_vector(matrix.row(rank_domain(xt)))
             for xt in domain.basis().keys()}

        ModuleMorphismByLinearity.__init__(self, on_basis=d.__getitem__,
                                           domain=domain, codomain=codomain,
                                           category=category)

    def _richcmp_(self, other, op):
        r"""
        Return whether this morphism and ``other`` satisfy ``op``.

        TESTS::

            sage: from sage.modules.with_basis.morphism import ModuleMorphismFromMatrix
            sage: X = CombinatorialFreeModule(ZZ, [1,2]); X.rename('X'); x = X.basis()
            sage: Y = CombinatorialFreeModule(ZZ, [3,4]); Y.rename('Y'); y = Y.basis()
            sage: m = matrix([[1,2],[3,5]])
            sage: phi = ModuleMorphismFromMatrix(matrix=m, domain=X, codomain=Y, side='right')
            sage: phi2 = ModuleMorphismFromMatrix(matrix=m, domain=X, codomain=Y, side='right')
            sage: phi == phi2
            True
            sage: phi is phi2
            False
            sage: m2 = matrix([[1,2],[4,5]])
            sage: phi2 = ModuleMorphismFromMatrix(matrix=m2, domain=X, codomain=Y, side='right')
            sage: phi == phi2
            False
        """
        if op == op_EQ:
            # We skip the on_basis check since the matrix defines the morphism
            return (self.__class__ is other.__class__
                    and self._zero == other._zero
                    and self._position == other._position
                    and self._is_module_with_basis_over_same_base_ring == other._is_module_with_basis_over_same_base_ring
                    and self._matrix == other._matrix)
        if op == op_NE:
            return not (self == other)
        return NotImplemented


class DiagonalModuleMorphism(ModuleMorphismByLinearity):
    r"""
    A class for diagonal module morphisms.

    See :meth:`ModulesWithBasis.ParentMethods.module_morphism`.

    INPUT:

    - ``domain``, ``codomain`` -- two modules with basis `F` and `G`,
      respectively
    - ``diagonal`` -- a function `d`

    Assumptions:

    - ``domain`` and ``codomain`` have the same base ring `R`,
    - their respective bases `F` and `G` have the same index set `I`,
    - `d` is a function `I \to R`.

    Return the diagonal module morphism from ``domain`` to ``codomain``
    sending `F(i) \mapsto d(i) G(i)` for all `i \in I`.

    By default, ``codomain`` is currently assumed to be ``domain``.
    (Todo: make a consistent choice with ``*ModuleMorphism``.)

    .. TODO::

        - Implement an optimized ``_call_()`` function.
        - Generalize to a mapcoeffs.
        - Generalize to a mapterms.

    EXAMPLES::

        sage: X = CombinatorialFreeModule(QQ, [1, 2, 3]); X.rename('X')
        sage: phi = X.module_morphism(diagonal=factorial, codomain=X)
        sage: x = X.basis()
        sage: phi(x[1]), phi(x[2]), phi(x[3])
        (B[1], 2*B[2], 6*B[3])
    """
    def __init__(self, domain, diagonal, codomain=None, category=None):
        r"""
        Initialize ``self``.

        TESTS::

            sage: X = CombinatorialFreeModule(QQ, [1, 2, 3]); X.rename('X')
            sage: phi = X.module_morphism(diagonal=factorial, codomain=X)
            sage: phi.__class__
            <class 'sage.modules.with_basis.morphism.DiagonalModuleMorphism_with_category'>
            sage: TestSuite(phi).run()
        """
        if codomain is None:
            raise ValueError("The codomain should be specified")
        if not (domain.basis().keys() == codomain.basis().keys() and
                domain.base_ring() == codomain.base_ring()):
            raise ValueError("The domain and codomain should have the same base ring "
                             "and the same basis indexing")
        from collections.abc import Callable
        if not isinstance(diagonal, Callable):
            raise ValueError("diagonal (=%s) should be a function" % diagonal)
        if category is None:
            category = ModulesWithBasis(domain.base_ring())
        ModuleMorphismByLinearity.__init__(
            self, domain=domain, codomain=codomain, category=category)
        self._diagonal = diagonal

    def _richcmp_(self, other, op):
        r"""
        Return whether this morphism and ``other`` satisfy ``op``.

        TESTS::

            sage: X = CombinatorialFreeModule(QQ, [1, 2, 3]); X.rename('X')
            sage: phi = X.module_morphism(diagonal=factorial, codomain=X)
            sage: psi = X.module_morphism(diagonal=factorial, codomain=X)
            sage: phi == psi
            True
            sage: phi is psi
            False
        """
        if op == op_EQ:
            return (self.__class__ is other.__class__
                    and self._diagonal == other._diagonal)
        if op == op_NE:
            return not (self == other)
        return NotImplemented

    def _on_basis(self, i):
        """
        Return the image by ``self`` of the basis element indexed by ``i``.

        TESTS::

            sage: X = CombinatorialFreeModule(QQ, [1, 2, 3]); X.rename('X'); x = X.basis()
            sage: Y = CombinatorialFreeModule(QQ, [1, 2, 3]); Y.rename('Y'); y = Y.basis()
            sage: phi = X.module_morphism(diagonal=factorial, codomain=X)
            sage: phi._on_basis(3)
            6*B[3]
        """
        return self.codomain().term(i, self._diagonal(i))

    def __invert__(self):
        r"""
        Return the inverse diagonal morphism.

        EXAMPLES::

            sage: X = CombinatorialFreeModule(QQ, [1, 2, 3]); X.rename('X'); x = X.basis()
            sage: Y = CombinatorialFreeModule(QQ, [1, 2, 3]); Y.rename('Y'); y = Y.basis()
            sage: phi = X.module_morphism(diagonal=factorial, codomain=X)
            sage: phi_inv = ~phi
            sage: phi_inv
            Generic endomorphism of Y
            sage: phi_inv(y[3])
            1/6*B[3]

        Caveat: this inverse morphism is only well defined if
        `d(\lambda)` is always invertible in the base ring. This
        condition is *not* tested for, so using an ill defined inverse
        morphism will trigger arithmetic errors.
        """
        return self.codomain().module_morphism(
            diagonal=pointwise_inverse_function(self._diagonal),
            codomain=self.domain(), category=self.category_for())


def pointwise_inverse_function(f):
    r"""
    Return the function `x \mapsto 1 / f(x)`.

    INPUT:

    - ``f`` -- a function

    EXAMPLES::

        sage: from sage.modules.with_basis.morphism import pointwise_inverse_function
        sage: def f(x): return x
        sage: g = pointwise_inverse_function(f)
        sage: g(1), g(2), g(3)
        (1, 1/2, 1/3)

    :func:`pointwise_inverse_function` is an involution::

        sage: f is pointwise_inverse_function(g)
        True

    .. TODO::

        This has nothing to do here!!! Should there be a library for
        pointwise operations on functions somewhere in Sage?
    """
    if hasattr(f, "pointwise_inverse"):
        return f.pointwise_inverse()
    return PointwiseInverseFunction(f)


class PointwiseInverseFunction(SageObject):
    r"""
    A class for pointwise inverse functions.

    The pointwise inverse function of a function `f` is the function
    sending every `x` to `1 / f(x)`.

    EXAMPLES::

        sage: from sage.modules.with_basis.morphism import PointwiseInverseFunction
        sage: f = PointwiseInverseFunction(factorial)
        sage: f(0), f(1), f(2), f(3)
        (1, 1, 1/2, 1/6)
    """
    def __init__(self, f):
        """
        TESTS::

            sage: from sage.modules.with_basis.morphism import PointwiseInverseFunction
            sage: f = PointwiseInverseFunction(factorial)
            sage: f(0), f(1), f(2), f(3)
            (1, 1, 1/2, 1/6)
            sage: TestSuite(f).run()
        """
        self._pointwise_inverse = f

    def __eq__(self, other):
        r"""
        Return whether this function is equal to ``other``.

        TESTS::

            sage: from sage.modules.with_basis.morphism import PointwiseInverseFunction
            sage: f = PointwiseInverseFunction(factorial)
            sage: g = PointwiseInverseFunction(factorial)
            sage: f is g
            False
            sage: f == g
            True
        """
        return (self.__class__ is other.__class__
                and self._pointwise_inverse == other._pointwise_inverse)

    def __ne__(self, other):
        r"""
        Return whether this function is not equal to ``other``.

        TESTS::

            sage: from sage.modules.with_basis.morphism import PointwiseInverseFunction
            sage: f = PointwiseInverseFunction(factorial)
            sage: g = PointwiseInverseFunction(factorial)
            sage: f != g
            False
        """
        return not (self == other)

    def __call__(self, *args):
        """
        TESTS::

            sage: from sage.modules.with_basis.morphism import PointwiseInverseFunction
            sage: g = PointwiseInverseFunction(operator.mul)
            sage: g(5,7)
            1/35
        """
        return ~(self._pointwise_inverse(*args))

    def pointwise_inverse(self):
        """
        TESTS::

            sage: from sage.modules.with_basis.morphism import PointwiseInverseFunction
            sage: g = PointwiseInverseFunction(operator.mul)
            sage: g.pointwise_inverse() is operator.mul
            True
        """
        return self._pointwise_inverse
