Repository: sagemath/sage

Issue:
Introduce extension of scalars coercion of CombinatorialFreeModules
<div id="comment:0"></div>

Currently, `CombinatorialFreeModule` does not allow coercion from another `CombinatorialFreeModule` with the same indices but with a base ring coercion:

```sage
sage: C = CombinatorialFreeModule(ZZ, Set([1,2]))
sage: CQ = CombinatorialFreeModule(QQ, Set([1,2]))
sage: CQ.has_coerce_map_from(C)
False
```
In this ticket, we introduce such a coercion and also implement `C.change_ring(QQ)` (which also makes `C.base_extend(QQ)` work).


CC:  @sagetrac-sage-combinat @nthiery @darijgr @fchapoton

Component: **combinatorics**

Keywords: **combinatorial free module**

Author: **Travis Scrimshaw, Matthias Koeppe**

Reviewer: **Darij Grinberg**

_Issue created by migration from https://trac.sagemath.org/ticket/21405_




Discussion:

Resolving patch:
diff --git a/src/sage/combinat/free_module.py b/src/sage/combinat/free_module.py
--- a/src/sage/combinat/free_module.py
+++ b/src/sage/combinat/free_module.py
@@ -482,6 +482,29 @@ def construction(self):
         return VectorFunctor(None, True, None, with_basis='standard',
                              basis_keys=self.basis().keys()), self.base_ring()
 
+    def change_ring(self, R):
+        r"""
+        Return the base change of ``self`` to `R`.
+
+        EXAMPLES::
+
+            sage: F = CombinatorialFreeModule(ZZ, ['a','b','c']); F
+            Free module generated by {'a', 'b', 'c'} over Integer Ring
+            sage: F_QQ = F.change_ring(QQ); F_QQ
+            Free module generated by {'a', 'b', 'c'} over Rational Field
+            sage: F_QQ.change_ring(ZZ) == F
+            True
+        """
+        if R is self.base_ring():
+            return self
+        construction = self.construction()
+        if construction is not None:
+            functor, arg = construction
+            from sage.categories.pushout import VectorFunctor
+            if isinstance(functor, VectorFunctor):
+                return functor(R)
+        raise NotImplementedError('the method change_ring() has not yet been implemented')
+
     # For backwards compatibility
     _repr_term = IndexedGenerators._repr_generator
     _latex_term = IndexedGenerators._latex_generator
@@ -644,16 +667,19 @@ def _element_constructor_(self, x):
         are systematically defined (and mathematically meaningful) for
         algebras.
 
-        Conversions between distinct free modules are not allowed any
-        more::
+        A coercion between free modules with the same indices exists
+        whenever a coercion map is defined between their base rings::
 
             sage: F = CombinatorialFreeModule(ZZ, ["a", "b"]);      F.rename("F")
             sage: G = CombinatorialFreeModule(QQ, ["a", "b"]);      G.rename("G")
-            sage: H = CombinatorialFreeModule(ZZ, ["a", "b", "c"]); H.rename("H")
             sage: G(F.monomial("a"))
-            Traceback (most recent call last):
-            ...
-            TypeError: do not know how to make x (= B['a']) an element of self (=G)
+            B['a']
+            sage: G(-3*F.monomial("a"))
+            -3*B['a']
+
+        Otherwise, there is no conversion between distinct free modules::
+
+            sage: H = CombinatorialFreeModule(ZZ, ["a", "b", "c"]); H.rename("H")
             sage: H(F.monomial("a"))
             Traceback (most recent call last):
             ...
@@ -675,12 +701,13 @@ def _element_constructor_(self, x):
             sage: pp(a)
             1/4*p[1, 1] # p[1, 1] + 1/4*p[1, 1] # p[2] + 1/4*p[2] # p[1, 1] + 1/4*p[2] # p[2]
 
-        Extensions of the ground ring should probably be reintroduced
+        General extensions of the ground ring should probably be reintroduced
         at some point, but via coercions, and with stronger sanity
         checks (ensuring that the codomain is really obtained by
         extending the scalar of the domain; checking that they share
         the same class is not sufficient).
 
+
         TESTS:
 
         Conversion from the ground ring is implemented for algebras::
@@ -781,6 +808,49 @@ def _first_ngens(self, n):
         it = iter(self._indices)
         return tuple(B[next(it)] for i in range(n))
 
+    def _coerce_map_from_(self, R):
+        """
+        Return ``True`` if there is a coercion map from ``R`` into ``self``.
+
+        There exists a coercion map from:
+
+        - a free module whose base ring coerces into the base ring of
+          ``self``, and which has the same indices as ``self``
+
+        EXAMPLES::
+
+            sage: C = CombinatorialFreeModule(ZZ, Set([1,2]))
+            sage: CQ = CombinatorialFreeModule(QQ, Set([1,2]))
+            sage: CQ.has_coerce_map_from(C)
+            True
+            sage: c = C.monomial(2)
+            sage: cq = CQ(c); cq
+            B[2]
+            sage: cq.leading_coefficient().parent()
+            Rational Field
+            sage: C.has_coerce_map_from(CQ)
+            False
+
+            sage: CF2 = CombinatorialFreeModule(GF(2), Set([1,2]))
+            sage: CF2.has_coerce_map_from(C)
+            True
+            sage: c = C.monomial(1)
+            sage: CF2(2*c)
+            0
+            sage: CF2(3*c)
+            B[1]
+        """
+        if isinstance(R, CombinatorialFreeModule):
+            try:
+                CR = R.base_extend(self.base_ring())
+            except (NotImplementedError, TypeError):
+                pass
+            else:
+                if CR == self:
+                    return lambda parent, x: self._from_dict(x._monomial_coefficients,
+                                                             coerce=True, remove_zeros=True)
+        return super()._coerce_map_from_(R)
+
     def dimension(self):
         """
         Return the dimension of the free module (which is given


Test patch: