Repository: sympy/sympy

Issue:
Inconsistency with summation order and empty sequence
```
In [9]: Sum(1, (x, 6, 5), (y, -oo, oo)).doit()
Out[9]: 0

In [10]: Sum(1, (y, -oo, oo), (x, 6, 5)).doit()
Out[10]: NaN
```

The reason is that summing over `(x, 6, 5)` leads to 0 as it represents an empty summation range. But summing NaN over an empty summation range is apparently still NaN. Should it be NaN? NaN*0 = 0, so maybe it should be 0?
￼
 

Discussion:
sympy-bot: :white_check_mark:

Hi, I am the [SymPy bot](https://github.com/sympy/sympy-bot) (v147). I'm here to help you write a release notes entry. Please read the [guide on how to write release notes](https://github.com/sympy/sympy/wiki/Writing-Release-Notes).



Your release notes are in good order.

Here is what the release notes will look like:
* concrete
  * Better assumptions on `Sum` and `Product`. ([#17414](https://github.com/sympy/sympy/pull/17414) by [@oscargus](https://github.com/oscargus))

This will be added to https://github.com/sympy/sympy/wiki/Release-Notes-for-1.5.

Note: This comment will be updated with the latest check if you edit the pull request. You need to reload the page to see it. <details><summary>Click here to see the pull request description that was parsed.</summary>

    <!-- Your title above should be a short description of what
    was changed. Do not include the issue number in the title. -->

    #### References to other Issues or PRs
    <!-- If this pull request fixes an issue, write "Fixes #NNNN" in that exact
    format, e.g. "Fixes #1234". See
    https://github.com/blog/1506-closing-issues-via-pull-requests . Please also
    write a comment on that issue linking back to this pull request once it is
    open. -->


    #### Brief description of what is fixed or changed
    Inspired by #17412 I added assumptions on Sum and Products. This solves some of the cases handled in #17412, but not all, so both PRs are worthwhile. This may have positive implications elsewhere as well, but not really clear where.

    #### Other comments


    #### Release Notes

    <!-- Write the release notes for this release below. See
    https://github.com/sympy/sympy/wiki/Writing-Release-Notes for more information
    on how to write release notes. The bot will check your release notes
    automatically to see if they are formatted correctly. -->

    <!-- BEGIN RELEASE NOTES -->
    * concrete
       * Better assumptions on `Sum` and `Product`.
    <!-- END RELEASE NOTES -->


</details><p>


Patch:
diff --git a/sympy/concrete/expr_with_intlimits.py b/sympy/concrete/expr_with_intlimits.py
index ec04c6eeb0..77277110f3 100644
--- a/sympy/concrete/expr_with_intlimits.py
+++ b/sympy/concrete/expr_with_intlimits.py
@@ -2,6 +2,7 @@
 
 from sympy.concrete.expr_with_limits import ExprWithLimits
 from sympy.core.singleton import S
+from sympy.core.relational import Eq
 
 class ReorderError(NotImplementedError):
     """
@@ -283,3 +284,57 @@ def reorder_limit(expr, x, y):
             return type(expr)(expr.function, *limits)
         else:
             raise ReorderError(expr, "could not interchange the two limits specified")
+
+    @property
+    def has_empty_sequence(self):
+        """
+        Returns True if the Sum or Product is computed for an empty sequence.
+
+        Examples
+        ========
+
+        >>> from sympy import Sum, Product, Symbol
+        >>> m = Symbol('m')
+        >>> Sum(m, (m, 1, 0)).has_empty_sequence
+        True
+
+        >>> Sum(m, (m, 1, 1)).has_empty_sequence
+        False
+
+        >>> M = Symbol('M', integer=True, positive=True)
+        >>> Product(m, (m, 1, M)).has_empty_sequence
+        False
+
+        >>> Product(m, (m, 2, M)).has_empty_sequence
+
+        >>> Product(m, (m, M + 1, M)).has_empty_sequence
+        True
+
+        >>> N = Symbol('N', integer=True, positive=True)
+        >>> Sum(m, (m, N, M)).has_empty_sequence
+
+        >>> N = Symbol('N', integer=True, negative=True)
+        >>> Sum(m, (m, N, M)).has_empty_sequence
+        False
+
+        See Also
+        ========
+
+        has_reversed_limits
+        has_finite_limits
+
+        """
+        ret_None = False
+        for lim in self.limits:
+            dif = lim[1] - lim[2]
+            eq = Eq(dif, 1)
+            if eq == True:
+                return True
+            elif eq == False:
+                continue
+            else:
+                ret_None = True
+
+        if ret_None:
+            return None
+        return False
diff --git a/sympy/concrete/expr_with_limits.py b/sympy/concrete/expr_with_limits.py
index 5322447b10..3a2a8e9a22 100644
--- a/sympy/concrete/expr_with_limits.py
+++ b/sympy/concrete/expr_with_limits.py
@@ -370,6 +370,111 @@ def _eval_subs(self, old, new):
 
         return self.func(func, *limits)
 
+    @property
+    def has_finite_limits(self):
+        """
+        Returns True if the limits are known to be finite, either by the
+        explicit bounds, assumptions on the bounds, or assumptions on the
+        variables.  False if known to be infinite, based on the bounds.
+        None if not enough information is available to determine.
+
+        Examples
+        ========
+
+        >>> from sympy import Sum, Integral, Product, oo, Symbol
+        >>> x = Symbol('x')
+        >>> Sum(x, (x, 1, 8)).has_finite_limits
+        True
+
+        >>> Integral(x, (x, 1, oo)).has_finite_limits
+        False
+
+        >>> M = Symbol('M')
+        >>> Sum(x, (x, 1, M)).has_finite_limits
+
+        >>> N = Symbol('N', integer=True)
+        >>> Product(x, (x, 1, N)).has_finite_limits
+        True
+
+        See Also
+        ========
+
+        has_reversed_limits
+
+        """
+
+        ret_None = False
+        for lim in self.limits:
+            if len(lim) == 3:
+                if any(l.is_infinite for l in lim[1:]):
+                    # Any of the bounds are +/-oo
+                    return False
+                elif any(l.is_infinite is None for l in lim[1:]):
+                    # Maybe there are assumptions on the variable?
+                    if lim[0].is_infinite is None:
+                        ret_None = True
+            else:
+                if lim[0].is_infinite is None:
+                    ret_None = True
+
+        if ret_None:
+            return None
+        return True
+
+    @property
+    def has_reversed_limits(self):
+        """
+        Returns True if the limits are known to be in reversed order, either
+        by the explicit bounds, assumptions on the bounds, or assumptions on the
+        variables.  False if known to be in normal order, based on the bounds.
+        None if not enough information is available to determine.
+
+        Examples
+        ========
+
+        >>> from sympy import Sum, Integral, Product, oo, Symbol
+        >>> x = Symbol('x')
+        >>> Sum(x, (x, 8, 1)).has_reversed_limits
+        True
+
+        >>> Sum(x, (x, 1, oo)).has_reversed_limits
+        False
+
+        >>> M = Symbol('M')
+        >>> Integral(x, (x, 1, M)).has_reversed_limits
+
+        >>> N = Symbol('N', integer=True, positive=True)
+        >>> Sum(x, (x, 1, N)).has_reversed_limits
+        False
+
+        >>> Product(x, (x, 2, N)).has_reversed_limits
+
+        >>> Product(x, (x, 2, N)).subs(N, N + 2).has_reversed_limits
+        False
+
+        See Also
+        ========
+
+        ExprWithIntLimits.has_empty_sequence
+
+        """
+        ret_None = False
+        for lim in self.limits:
+            if len(lim) == 3:
+                var, a, b = lim
+                dif = b - a
+                if dif.is_extended_negative:
+                    return True
+                elif dif.is_extended_nonnegative:
+                    continue
+                else:
+                    ret_None = True
+            else:
+                return None
+        if ret_None:
+            return None
+        return False
+
 
 class AddWithLimits(ExprWithLimits):
     r"""Represents unevaluated oriented additions.
diff --git a/sympy/concrete/products.py b/sympy/concrete/products.py
index f31fbfdac5..a3bd61b50f 100644
--- a/sympy/concrete/products.py
+++ b/sympy/concrete/products.py
@@ -201,8 +201,47 @@ def term(self):
     function = term
 
     def _eval_is_zero(self):
-        # a Product is zero only if its term is zero.
-        return self.term.is_zero
+        if self.has_empty_sequence:
+            return False
+
+        z = self.term.is_zero
+        if z is True:
+            return True
+        if self.has_finite_limits:
+            # A Product is zero only if its term is zero assuming finite limits.
+            return z
+
+    def _eval_is_extended_real(self):
+        if self.has_empty_sequence:
+            return True
+
+        return self.function.is_extended_real
+
+    def _eval_is_positive(self):
+        if self.has_empty_sequence:
+            return True
+        if self.function.is_positive and self.has_finite_limits:
+            return True
+
+    def _eval_is_nonnegative(self):
+        if self.has_empty_sequence:
+            return True
+        if self.function.is_nonnegative and self.has_finite_limits:
+            return True
+
+    def _eval_is_extended_nonnegative(self):
+        if self.has_empty_sequence:
+            return True
+        if self.function.is_extended_nonnegative:
+            return True
+
+    def _eval_is_extended_nonpositive(self):
+        if self.has_empty_sequence:
+            return True
+
+    def _eval_is_finite(self):
+        if self.has_finite_limits and self.function.is_finite:
+            return True
 
     def doit(self, **hints):
         # first make sure any definite limits have product
diff --git a/sympy/concrete/summations.py b/sympy/concrete/summations.py
index ac2d5c26bd..19327f3d02 100644
--- a/sympy/concrete/summations.py
+++ b/sympy/concrete/summations.py
@@ -171,7 +171,24 @@ def _eval_is_zero(self):
         # cancel out. This only answers whether the summand is zero; if
         # not then None is returned since we don't analyze whether all
         # terms cancel out.
-        if self.function.is_zero:
+        if self.function.is_zero or self.has_empty_sequence:
+            return True
+
+    def _eval_is_extended_real(self):
+        if self.has_empty_sequence:
+            return True
+        return self.function.is_extended_real
+
+    def _eval_is_positive(self):
+        if self.has_finite_limits and self.has_reversed_limits is False:
+            return self.function.is_positive
+
+    def _eval_is_negative(self):
+        if self.has_finite_limits and self.has_reversed_limits is False:
+            return self.function.is_negative
+
+    def _eval_is_finite(self):
+        if self.has_finite_limits and self.function.is_finite:
             return True
 
     def doit(self, **hints):
@@ -208,6 +225,9 @@ def doit(self, **hints):
         for n, limit in enumerate(self.limits):
             i, a, b = limit
             dif = b - a
+            if dif == -1:
+                # Any summation over an empty set is zero
+                return S.Zero
             if dif.is_integer and dif.is_negative:
                 a, b = b + 1, a - 1
                 f = -f


Test patch:
diff --git a/sympy/concrete/tests/test_sums_products.py b/sympy/concrete/tests/test_sums_products.py
index 8440129e56..5d8b31d62e 100644
--- a/sympy/concrete/tests/test_sums_products.py
+++ b/sympy/concrete/tests/test_sums_products.py
@@ -676,6 +676,9 @@ def test_is_zero():
         assert func(0, (x, 1, 1)).is_zero is True
         assert func(x, (x, 1, 1)).is_zero is None
 
+    assert Sum(0, (x, 1, 0)).is_zero is True
+    assert Product(0, (x, 1, 0)).is_zero is False
+
 
 def test_is_number():
     # is number should not rely on evaluation or assumptions,
@@ -1165,6 +1168,140 @@ def test_exceptions():
     raises(NotImplementedError, lambda: S.is_convergent())
 
 
+def test_sumproducts_assumptions():
+    M = Symbol('M', integer=True, positive=True)
+
+    m = Symbol('m', integer=True)
+    for func in [Sum, Product]:
+        assert func(m, (m, -M, M)).is_positive is None
+        assert func(m, (m, -M, M)).is_nonpositive is None
+        assert func(m, (m, -M, M)).is_negative is None
+        assert func(m, (m, -M, M)).is_nonnegative is None
+        assert func(m, (m, -M, M)).is_finite is True
+
+    m = Symbol('m', integer=True, nonnegative=True)
+    for func in [Sum, Product]:
+        assert func(m, (m, 0, M)).is_positive is None
+        assert func(m, (m, 0, M)).is_nonpositive is None
+        assert func(m, (m, 0, M)).is_negative is False
+        assert func(m, (m, 0, M)).is_nonnegative is True
+        assert func(m, (m, 0, M)).is_finite is True
+
+    m = Symbol('m', integer=True, positive=True)
+    for func in [Sum, Product]:
+        assert func(m, (m, 1, M)).is_positive is True
+        assert func(m, (m, 1, M)).is_nonpositive is False
+        assert func(m, (m, 1, M)).is_negative is False
+        assert func(m, (m, 1, M)).is_nonnegative is True
+        assert func(m, (m, 1, M)).is_finite is True
+
+    m = Symbol('m', integer=True, negative=True)
+    assert Sum(m, (m, -M, -1)).is_positive is False
+    assert Sum(m, (m, -M, -1)).is_nonpositive is True
+    assert Sum(m, (m, -M, -1)).is_negative is True
+    assert Sum(m, (m, -M, -1)).is_nonnegative is False
+    assert Sum(m, (m, -M, -1)).is_finite is True
+    assert Product(m, (m, -M, -1)).is_positive is None
+    assert Product(m, (m, -M, -1)).is_nonpositive is None
+    assert Product(m, (m, -M, -1)).is_negative is None
+    assert Product(m, (m, -M, -1)).is_nonnegative is None
+    assert Product(m, (m, -M, -1)).is_finite is True
+
+    m = Symbol('m', integer=True, nonpositive=True)
+    assert Sum(m, (m, -M, 0)).is_positive is False
+    assert Sum(m, (m, -M, 0)).is_nonpositive is True
+    assert Sum(m, (m, -M, 0)).is_negative is None
+    assert Sum(m, (m, -M, 0)).is_nonnegative is None
+    assert Sum(m, (m, -M, 0)).is_finite is True
+    assert Product(m, (m, -M, 0)).is_positive is None
+    assert Product(m, (m, -M, 0)).is_nonpositive is None
+    assert Product(m, (m, -M, 0)).is_negative is None
+    assert Product(m, (m, -M, 0)).is_nonnegative is None
+    assert Product(m, (m, -M, 0)).is_finite is True
+
+    m = Symbol('m', integer=True)
+    assert Sum(2, (m, 0, oo)).is_positive is None
+    assert Sum(2, (m, 0, oo)).is_nonpositive is None
+    assert Sum(2, (m, 0, oo)).is_negative is None
+    assert Sum(2, (m, 0, oo)).is_nonnegative is None
+    assert Sum(2, (m, 0, oo)).is_finite is None
+
+    assert Product(2, (m, 0, oo)).is_positive is None
+    assert Product(2, (m, 0, oo)).is_nonpositive is None
+    assert Product(2, (m, 0, oo)).is_negative is False
+    assert Product(2, (m, 0, oo)).is_nonnegative is None
+    assert Product(2, (m, 0, oo)).is_finite is None
+
+    assert Product(0, (x, M, M-1)).is_positive is True
+    assert Product(0, (x, M, M-1)).is_finite is True
+
+
+def test_expand_with_assumptions():
+    M = Symbol('M', integer=True, positive=True)
+    x = Symbol('x', positive=True)
+    m = Symbol('m', nonnegative=True)
+    assert log(Product(x**m, (m, 0, M))).expand() == Sum(m*log(x), (m, 0, M))
+    assert log(Product(exp(x**m), (m, 0, M))).expand() == Sum(x**m, (m, 0, M))
+    assert log(Product(x**m, (m, 0, M))).rewrite(Sum).expand() == Sum(m*log(x), (m, 0, M))
+    assert log(Product(exp(x**m), (m, 0, M))).rewrite(Sum).expand() == Sum(x**m, (m, 0, M))
+
+    n = Symbol('n', nonnegative=True)
+    i, j = symbols('i,j', positive=True, integer=True)
+    x, y = symbols('x,y', positive=True)
+    assert log(Product(x**i*y**j, (i, 1, n), (j, 1, m))).expand() \
+        == Sum(i*log(x) + j*log(y), (i, 1, n), (j, 1, m))
+
+
+def test_has_finite_limits():
+    x = Symbol('x')
+    assert Sum(1, (x, 1, 9)).has_finite_limits is True
+    assert Sum(1, (x, 1, oo)).has_finite_limits is False
+    M = Symbol('M')
+    assert Sum(1, (x, 1, M)).has_finite_limits is None
+    M = Symbol('M', positive=True)
+    assert Sum(1, (x, 1, M)).has_finite_limits is True
+    x = Symbol('x', positive=True)
+    M = Symbol('M')
+    assert Sum(1, (x, 1, M)).has_finite_limits is True
+
+    assert Sum(1, (x, 1, M), (y, -oo, oo)).has_finite_limits is False
+
+def test_has_reversed_limits():
+    assert Sum(1, (x, 1, 1)).has_reversed_limits is False
+    assert Sum(1, (x, 1, 9)).has_reversed_limits is False
+    assert Sum(1, (x, 1, -9)).has_reversed_limits is True
+    assert Sum(1, (x, 1, 0)).has_reversed_limits is True
+    assert Sum(1, (x, 1, oo)).has_reversed_limits is False
+    M = Symbol('M')
+    assert Sum(1, (x, 1, M)).has_reversed_limits is None
+    M = Symbol('M', positive=True, integer=True)
+    assert Sum(1, (x, 1, M)).has_reversed_limits is False
+    assert Sum(1, (x, 1, M), (y, -oo, oo)).has_reversed_limits is False
+    M = Symbol('M', negative=True)
+    assert Sum(1, (x, 1, M)).has_reversed_limits is True
+
+    assert Sum(1, (x, 1, M), (y, -oo, oo)).has_reversed_limits is True
+    assert Sum(1, (x, oo, oo)).has_reversed_limits is None
+
+
+def test_has_empty_sequence():
+    assert Sum(1, (x, 1, 1)).has_empty_sequence is False
+    assert Sum(1, (x, 1, 9)).has_empty_sequence is False
+    assert Sum(1, (x, 1, -9)).has_empty_sequence is False
+    assert Sum(1, (x, 1, 0)).has_empty_sequence is True
+    assert Sum(1, (x, y, y - 1)).has_empty_sequence is True
+    assert Sum(1, (x, 3, 2), (y, -oo, oo)).has_empty_sequence is True
+    assert Sum(1, (y, -oo, oo), (x, 3, 2)).has_empty_sequence is True
+    assert Sum(1, (x, oo, oo)).has_empty_sequence is False
+
+
+def test_empty_sequence():
+    assert Product(x*y, (x, -oo, oo), (y, 1, 0)).doit() == 1
+    assert Product(x*y, (y, 1, 0), (x, -oo, oo)).doit() == 1
+    assert Sum(x, (x, -oo, oo), (y, 1, 0)).doit() == 0
+    assert Sum(x, (y, 1, 0), (x, -oo, oo)).doit() == 0
+
+
 def test_issue_8016():
     k = Symbol('k', integer=True)
     n, m = symbols('n, m', integer=True, positive=True)