Repository: sagemath/sage

Issue:
cleaning and enhancement to PolyDict
<div id="comment:0"></div>

We provide some cleaning and speed up to the `PolyDict` class by always enforcing keys to be `ETuple` (some parts of the code did assume that was the case). As a consequence we deprecate the usage of the constructor arguments `force_int_exponents` and `force_etuples`.

We also simplify the design by getting rid of `PolyDict.__zero` (and hence deprecating the argument `zero` of the `PolyDict` constructor).

Along the way, we fix

```
sage: R = ZZ['x']['y,z']
sage: y, z = R.gens()
sage: y.integral(y)
1/2*y^2
sage: parent(y.integral(y))
Multivariate Polynomial Ring in y, z over Univariate Polynomial Ring in x over Integer Ring
```
and

```
sage: from sage.rings.polynomial.polydict import ETuple
sage: e = ETuple([0, 1, 0])
sage: e.eadd_p(0, 0).nonzero_positions()
[0, 1]
```

CC:  @xcaruso @mezzarobba @saraedum

Component: **algebra**

Author: **Vincent Delecroix**

_Issue created by migration from https://trac.sagemath.org/ticket/34000_




Discussion:

Resolving patch:
diff --git a/src/sage/rings/polynomial/multi_polynomial_element.py b/src/sage/rings/polynomial/multi_polynomial_element.py
--- a/src/sage/rings/polynomial/multi_polynomial_element.py
+++ b/src/sage/rings/polynomial/multi_polynomial_element.py
@@ -40,6 +40,7 @@
 #   Sage: Open Source Mathematical Software
 #
 #       Copyright (C) 2005 William Stein <wstein@gmail.com>
+#                     2022 Vincent Delecroix <20100.delecroix@gmail.com>
 #
 #  Distributed under the terms of the GNU General Public License (GPL)
 #
@@ -53,9 +54,12 @@
 #                  https://www.gnu.org/licenses/
 #*****************************************************************************
 
-from sage.structure.element import CommutativeRingElement, coerce_binop
+import operator
+
+from sage.structure.element import CommutativeRingElement, coerce_binop, get_coercion_model, parent
 from sage.misc.misc_c import prod
 import sage.rings.integer
+import sage.rings.integer_ring
 from sage.rings.qqbar_decorators import handle_AA_and_QQbar
 from . import polydict
 from sage.structure.factorization import Factorization
@@ -76,6 +80,20 @@ def is_MPolynomial(x):
 
 
 class MPolynomial_element(MPolynomial):
+    r"""
+    Generic multivariate polynomial.
+
+    This implementation is based on the :class:`~sage.rings.polynomial.polydict.PolyDict`.
+
+    .. TODO::
+
+        As mentioned in their docstring,
+        :class:`~sage.rings.polynomial.polydict.PolyDict` objects never clear
+        zeros. In all arithmetic operations on :class:`MPolynomial_element`
+        there is an additional call to the method ``remove_zeros`` to clear
+        them. This is not ideal because of the presence of inexact zeros, see
+        :trac:`35174`.
+    """
     def __init__(self, parent, x):
         """
         EXAMPLES::
@@ -254,17 +272,61 @@ def number_of_terms(self):
 
     hamming_weight = number_of_terms
 
+    def __neg__(self):
+        """
+        Return the negative of ``self``.
+
+        EXAMPLES::
+
+            sage: R.<x,y> = QQbar[]
+            sage: -x
+            -x
+            sage: -(y-1)
+            -y + 1
+        """
+        return self.__class__(self.parent(), -self.__element)
+
     def _add_(self, right):
-        #return self.parent()(self.__element + right.__element)
-        return self.__class__(self.parent(),self.__element + right.__element)
+        """
+        Return the sum of ``self`` and ``right``.
+
+        EXAMPLES::
+
+            sage: R.<x,y> = QQbar[]
+            sage: x + y
+            x + y
+        """
+        elt = self.__element + right.__element
+        elt.remove_zeros()
+        return self.__class__(self.parent(), elt)
 
     def _sub_(self, right):
-        # return self.parent()(self.__element - right.__element)
-        return self.__class__(self.parent(),self.__element - right.__element)
+        """
+        Return the difference between ``self`` and ``right``.
+
+        EXAMPLES::
+
+            sage: R.<x,y> = QQbar[]
+            sage: x - y
+            x - y
+        """
+        elt = self.__element - right.__element
+        elt.remove_zeros()
+        return self.__class__(self.parent(), elt)
 
     def _mul_(self, right):
-        #return self.parent()(self.__element * right.__element)
-        return self.__class__(self.parent(),self.__element * right.__element)
+        """
+        Return the product between ``self`` and ``right``.
+
+        EXAMPLES::
+
+            sage: R.<x,y> = QQbar[]
+            sage: x * y
+            x*y
+        """
+        elt = self.__element * right.__element
+        elt.remove_zeros()
+        return self.__class__(self.parent(), elt)
 
     def _lmul_(self, a):
         """
@@ -283,7 +345,9 @@ def _lmul_(self, a):
             sage: 3*f
             3*x + 3*y
         """
-        return self.__class__(self.parent(),self.__element.scalar_lmult(a))
+        elt = self.__element.scalar_lmult(a)
+        elt.remove_zeros()
+        return self.__class__(self.parent(), elt)
 
     def _rmul_(self, a):
         """
@@ -302,7 +366,9 @@ def _rmul_(self, a):
             sage: f*3
             3*x + 3*y
         """
-        return self.__class__(self.parent(),self.__element.scalar_rmult(a))
+        elt = self.__element.scalar_rmult(a)
+        elt.remove_zeros()
+        return self.__class__(self.parent(), elt)
 
     def _div_(self, right):
         r"""
@@ -333,9 +399,9 @@ def _div_(self, right):
             sage: x/S(2)
             1/2*x
         """
-        if right in self.base_ring():
-            inv = self.base_ring().one()/self.base_ring()(right)
-            return inv*self
+        if right.is_constant():
+            inv = self.base_ring().one() / right.constant_coefficient()
+            return inv * self
         return self.parent().fraction_field()(self, right, coerce=False)
 
     def __rpow__(self, n):
@@ -396,7 +462,8 @@ def __init__(self, parent, x):
             True
         """
         if not isinstance(x, polydict.PolyDict):
-            x = polydict.PolyDict(x, parent.base_ring().zero(), remove_zero=True)
+            x = polydict.PolyDict(x)
+        x.remove_zeros()
         MPolynomial_element.__init__(self, parent, x)
 
     def _new_constant_poly(self, x, P):
@@ -417,18 +484,6 @@ def _new_constant_poly(self, x, P):
         """
         return MPolynomial_polydict(P, {P._zero_tuple:x})
 
-    def __neg__(self):
-        """
-        EXAMPLES::
-
-            sage: R.<x,y>=QQbar[]
-            sage: -x
-            -x
-            sage: -(y-1)
-            -y + 1
-        """
-        return self*(-1)
-
     def _repr_(self):
         """
         EXAMPLES::
@@ -535,8 +590,8 @@ def degrees(self):
             sage: R(0).degrees()
             (0, 0, 0, 0)
         """
-        if self.is_zero():
-            return polydict.ETuple({},self.parent().ngens())
+        if not self:
+            return polydict.ETuple({}, self.parent().ngens())
         else:
             return self._MPolynomial_element__element.max_exp()
 
@@ -751,10 +806,11 @@ def monomial_coefficient(self, mon):
             sage: f.monomial_coefficient(x)
             -a
         """
-        if not (isinstance(mon, MPolynomial) and mon.parent() is self.parent() and mon.is_monomial()):
-            raise TypeError("mon must be a monomial in the parent of self.")
-        R = self.parent().base_ring()
-        return R(self.element().monomial_coefficient(mon.element().dict()))
+        if parent(mon) is not self.parent():
+            raise TypeError("mon must be a monomial in the parent of self")
+        exp = polydict.monomial_exponent(mon.element())
+        zero = self.parent().base_ring().zero()
+        return self.element().get(exp, zero)
 
     def dict(self):
         """
@@ -803,11 +859,7 @@ def __iter__(self):
         ring = self.parent()
         one = ring.base_ring().one()
         for exp in self._exponents:
-            yield (elt[exp],
-                   MPolynomial_polydict(ring, polydict.PolyDict({exp:one},
-                                                                force_int_exponents=False,
-                                                                force_etuples=False))
-                   )
+            yield (elt[exp], MPolynomial_polydict(ring, polydict.PolyDict({exp: one}, check=False)))
 
     def __getitem__(self, x):
         """
@@ -1274,6 +1326,7 @@ def _homogenize(self, var):
         if self.is_homogeneous():
             return self
         X = self.element().homogenize(var)
+        X.remove_zeros()
         R = self.parent()
         return R(X)
 
@@ -1420,7 +1473,7 @@ def monomials(self):
         """
         ring = self.parent()
         one = ring.base_ring().one()
-        return [MPolynomial_polydict(ring, polydict.PolyDict({m:one}, force_int_exponents=False, force_etuples=False))
+        return [MPolynomial_polydict(ring, polydict.PolyDict({m: one}, check=False))
                 for m in self._exponents]
 
     def constant_coefficient(self):
@@ -1669,9 +1722,9 @@ def lm(self):
             if self.is_zero():
                 return self
             R = self.parent()
-            f = self._MPolynomial_element__element.lcmt( R.term_order().greater_tuple )
+            f = self._MPolynomial_element__element.lcmt(R.term_order().greater_tuple)
             one = R.base_ring().one()
-            self.__lm = MPolynomial_polydict(R,polydict.PolyDict({f:one},zero=R.base_ring().zero(),force_int_exponents=False,  force_etuples=False))
+            self.__lm = MPolynomial_polydict(R,polydict.PolyDict({f: one}, check=False))
             return self.__lm
 
     def lc(self):
@@ -1728,11 +1781,11 @@ def lt(self):
                 return self
             R = self.parent()
             f = self._MPolynomial_element__element.dict()
-            res = self._MPolynomial_element__element.lcmt( R.term_order().greater_tuple )
-            self.__lt = MPolynomial_polydict(R,polydict.PolyDict({res:f[res]},zero=R.base_ring().zero(),force_int_exponents=False, force_etuples=False))
+            res = self._MPolynomial_element__element.lcmt(R.term_order().greater_tuple)
+            self.__lt = MPolynomial_polydict(R, polydict.PolyDict({res: f[res]}, check=False))
             return self.__lt
 
-    def __eq__(self,right):
+    def __eq__(self, right):
         if not isinstance(right, MPolynomial_polydict):
             # we want comparison with zero to be fast
             if not right:
@@ -1740,7 +1793,7 @@ def __eq__(self,right):
             return CommutativeRingElement.__eq__(self, right)
         return self._MPolynomial_element__element == right._MPolynomial_element__element
 
-    def __ne__(self,right):
+    def __ne__(self, right):
         if not isinstance(right, MPolynomial_polydict):
             # we want comparison with zero to be fast
             if not right:
@@ -1759,7 +1812,7 @@ def __bool__(self):
 
            This is much faster than actually writing ``self == 0``.
         """
-        return self._MPolynomial_element__element.dict()!={}
+        return bool(self._MPolynomial_element__element)
 
     def _floordiv_(self, right):
         r"""
@@ -1835,30 +1888,25 @@ def _derivative(self, var=None):
             raise ValueError("must specify which variable to differentiate with respect to")
 
         P = self.parent()
-        gens = list(P.gens())
 
         # check if var is one of the generators
-        try:
-            index = gens.index(var)
-        except ValueError:
+        index = polydict.gen_index(P(var).element())
+        if index == -1:
             # var is not a generator; do term-by-term differentiation recursively
             # var may be, for example, a generator of the base ring
             d = dict([(e, x._derivative(var)) for (e, x) in self.dict().items()])
-            d = polydict.PolyDict(d, P.base_ring().zero(), remove_zero=True)
+            d = polydict.PolyDict(d, check=False)
+            d.remove_zeros()
             return MPolynomial_polydict(P, d)
 
         # differentiate w.r.t. indicated variable
-        d = {}
-        v = polydict.ETuple({index:1}, len(gens))
-        for (exp, coeff) in self.dict().items():
-            if exp[index] > 0:
-                d[exp.esub(v)] = coeff * exp[index]
-        d = polydict.PolyDict(d, P.base_ring().zero(), remove_zero=True)
-        return MPolynomial_polydict(P, d)
+        elt = self.element().derivative_i(index)
+        elt.remove_zeros()
+        return MPolynomial_polydict(P, elt)
 
     def integral(self, var=None):
         r"""
-        Integrates ``self`` with respect to variable ``var``.
+        Integrate ``self`` with respect to variable ``var``.
 
         .. NOTE::
 
@@ -1878,6 +1926,11 @@ def integral(self, var=None):
             sage: it.parent() == x.parent()
             True
 
+            sage: R = ZZ['x']['y, z']
+            sage: y, z = R.gens()
+            sage: R.an_element().integral(y).parent()
+            Multivariate Polynomial Ring in y, z over Univariate Polynomial Ring in x over Rational Field
+
         On polynomials with coefficients in power series::
 
             sage: R.<t> = PowerSeriesRing(QQbar)
@@ -1901,32 +1954,47 @@ def integral(self, var=None):
             Traceback (most recent call last):
             ...
             ValueError: must specify which variable to integrate with respect to
+
+        :trac:`34000`::
+
+            sage: R = ZZ['x']['y,z']
+            sage: y, z = R.gens()
+            sage: parent(y.integral(y))
+            Multivariate Polynomial Ring in y, z over Univariate Polynomial Ring in x over Rational Field
         """
         if var is None:
             raise ValueError("must specify which variable to integrate "
                              "with respect to")
 
+
+        # TODO:
+        # calling the coercion model bin_op is much more accurate than using the
+        # true division (which is bypassed by polynomials). But it does not work
+        # in all cases!!
+        # See similar in polynomial_element.pyx
         P = self.parent()
-        gens = list(P.gens())
+        cm = get_coercion_model()
+        try:
+            S = cm.bin_op(P.one(), sage.rings.integer_ring.ZZ.one(), operator.truediv).parent()
+        except TypeError:
+            Q = (P.base_ring().one() / sage.rings.integer_ring.ZZ.one()).parent()
+            S = P.change_ring(Q)
+
+        if P is not S:
+            return S.coerce(self).integral(var)
 
         # check if var is one of the generators
-        try:
-            index = gens.index(var)
-        except ValueError:
+        index = polydict.gen_index(P(var).element())
+        if index == -1:
             # var is not a generator; do term-by-term integration recursively
             # var may be, for example, a generator of the base ring
-            d = dict([(e, x.integral(var))
-                      for (e, x) in self.dict().items()])
-            d = polydict.PolyDict(d, P.base_ring().zero(),
-                                  remove_zero=True)
-            return MPolynomial_polydict(P, d)
-
-        # integrate w.r.t. indicated variable
-        d = {}
-        v = polydict.ETuple({index:1}, len(gens))
-        for (exp, coeff) in self.dict().items():
-            d[exp.eadd(v)] = coeff / (1+exp[index])
-        d = polydict.PolyDict(d, P.base_ring().zero(), remove_zero=True)
+            d = {e: x.integral(var)
+                 for e, x in self.dict().items()}
+            d = polydict.PolyDict(d, check=False)
+            d.remove_zeros()
+        else:
+            # integrate w.r.t. indicated variable
+            d = self.element().integral_i(index)
         return MPolynomial_polydict(P, d)
 
     def factor(self, proof=None):
diff --git a/src/sage/rings/polynomial/multi_polynomial_ring.py b/src/sage/rings/polynomial/multi_polynomial_ring.py
--- a/src/sage/rings/polynomial/multi_polynomial_ring.py
+++ b/src/sage/rings/polynomial/multi_polynomial_ring.py
@@ -639,9 +639,7 @@ def monomial_quotient(self, f, g, coeff=False):
 
         res = f.esub(g)
 
-        return MPolynomial_polydict(self, PolyDict({res: coeff},
-                                                   force_int_exponents=False,
-                                                   force_etuples=False))
+        return MPolynomial_polydict(self, PolyDict({res: coeff}))
 
     def monomial_lcm(self, f, g):
         """
@@ -690,8 +688,7 @@ def monomial_lcm(self, f, g):
         res = {i: max(f[i], g[i])
                for i in f.common_nonzero_positions(g)}
 
-        return self(PolyDict({ETuple(res, length): one},
-                             force_int_exponents=False, force_etuples=False))
+        return self(PolyDict({ETuple(res, length): one}))
 
     def monomial_reduce(self, f, G):
         r"""
@@ -891,11 +888,37 @@ def addwithcarry(tempvector, maxvector, pos):
 
         while tempvector != maxvector:
             tempvector = addwithcarry(list(tempvector), maxvector, pos)
-            M.append(R(PolyDict({ETuple(tempvector): one},
-                                force_int_exponents=False,
-                                force_etuples=False)))
+            M.append(R(PolyDict({ETuple(tempvector): one})))
         return M
 
+    def sum(self, terms):
+        r"""
+        Return a sum of elements of this multipolynomial ring.
+
+        This is method is much faster than the Python builtin sum.
+
+        EXAMPLES::
+
+            sage: R = QQ['x']
+            sage: S = R['y, z']
+            sage: x = R.gen()
+            sage: y, z = S.gens()
+            sage: S.sum([x*y, 2*x^2*z - 2*x*y, 1 + y + z])
+            (-x + 1)*y + (2*x^2 + 1)*z + 1
+
+        Comparison with builtin sum::
+
+            sage: sum([x*y, 2*x^2*z - 2*x*y, 1 + y + z])
+            (-x + 1)*y + (2*x^2 + 1)*z + 1
+        """
+        elt = PolyDict({}, check=False)
+        for t in terms:
+            elt += self(t).element()
+        # NOTE: here we should be using self.element_class but polynomial rings are not complient
+        # with categories...
+        from sage.rings.polynomial.multi_polynomial_element import MPolynomial_polydict
+        return MPolynomial_polydict(self, elt)
+
 
 class MPolynomialRing_polydict_domain(IntegralDomain,
                                       MPolynomialRing_polydict):


Test patch: