Repository: sympy/sympy
Pull request: Added assumption methods to Sum and Product
<!-- 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. -->

Fixes #17424

#### 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`.
   * Empty summations leads to zero result.
<!-- END RELEASE NOTES -->


Problem statement:
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?
￼

Hints from issue discussion:

Commit message: Added assumption methods to Sum and Product

Patch to review:
diff --git a/sympy/concrete/products.py b/sympy/concrete/products.py
index aebd2b71a204..6e2f1cfb5b67 100644
--- a/sympy/concrete/products.py
+++ b/sympy/concrete/products.py
@@ -204,6 +204,17 @@ def _eval_is_zero(self):
         # a Product is zero only if its term is zero.
         return self.term.is_zero
 
+    def _eval_is_extended_real(self):
+        return self.function.is_extended_real
+
+    def _eval_is_positive(self):
+        if self.function.is_positive:
+            return True
+
+    def _eval_is_nonnegative(self):
+        if self.function.is_nonnegative:
+            return True
+
     def doit(self, **hints):
         f = self.function
         for index, limit in enumerate(self.limits):
diff --git a/sympy/concrete/summations.py b/sympy/concrete/summations.py
index 705486560ea8..d65fe6509124 100644
--- a/sympy/concrete/summations.py
+++ b/sympy/concrete/summations.py
@@ -174,6 +174,21 @@ def _eval_is_zero(self):
         if self.function.is_zero:
             return True
 
+    def _eval_is_extended_real(self):
+        return self.function.is_extended_real
+
+    def _eval_is_positive(self):
+        return self.function.is_positive
+
+    def _eval_is_nonnegative(self):
+        return self.function.is_nonnegative
+
+    def _eval_is_negative(self):
+        return self.function.is_negative
+
+    def _eval_is_nonpositive(self):
+        return self.function.is_nonpositive
+
     def doit(self, **hints):
         if hints.get('deep', True):
             f = self.function.doit(**hints)
diff --git a/sympy/concrete/tests/test_sums_products.py b/sympy/concrete/tests/test_sums_products.py
index c24d659f9006..432563410084 100644
--- a/sympy/concrete/tests/test_sums_products.py
+++ b/sympy/concrete/tests/test_sums_products.py
@@ -1131,3 +1131,66 @@ def test_exceptions():
     raises(ReorderError, lambda: S.reorder_limit(0, 1))
     S = Sum(x*y, (x, a, b), (y, 1, 4))
     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
+
+    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
+
+    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
+
+    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 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
+
+    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 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
+
+
+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)
+    # Here, it must be rewritten as the assumption system cannot figure out
+    # that x**i*y**j is positive.
+    assert log(Product(x**i*y**j, (i, 1, n), (j, 1, m))).rewrite(Sum).expand() \
+        == Sum(i*log(x), (i, 1, n), (j, 1, m)) + Sum(j*log(y), (i, 1, n), (j, 1, m))


Code review comments:
diff_hunk: @@ -204,6 +204,17 @@ def _eval_is_zero(self):
         # a Product is zero only if its term is zero.
         return self.term.is_zero
 
+    def _eval_is_extended_real(self):
+        return self.function.is_extended_real
+
+    def _eval_is_positive(self):
+        if self.function.is_positive:
+            return True
path: sympy/concrete/products.py
text: @user1:
If this is an infinite product then can it not be zero or infinite even if the function is positive e.g.:
```julia
In [2]: Product(S(1)/2, (x, 1, oo)).doit()                                                                                        
Out[2]: 0

In [3]: Product(2, (x, 1, oo)).doit()                                                                                             
Out[3]: ∞
```
I think that a positive function can imply that the product is extended_nonnegative in the infinite case though.

@author:
Makes sense. However if the limits are finite it shouldn't happen, right?

@user1:
It won't happen if the limits are finite. I think it would be good to add an `is_finite_product` property that can be used conveniently in places like this.

@author:
I think `has_finite_limits` is my preferred name. Now, if there are no limits (as for Integrals, it makes sense to add this to `ExprWithLimits`), should one go for the assumptions on the variable or just return `None` if there are no explicit limits?

Btw, something similar happens for sums: `Sum(1/2, (x, 1, oo)).doit()` is `oo`.

@user1:
I think that `has_finite_limits` makes sense. In the context of an integral without limits it should be None I think. That probably makes the most sense since in most contexts you would use the property the None case would lead to an inconclusive result. (It probably also makes sense for Integrals to have something like an is_definite property if they don't)

@author:
However, if you have a integration variable with is real, it should differ from extended_real. So right now, real leads to finite limits.

diff_hunk: @@ -174,6 +174,21 @@ def _eval_is_zero(self):
         if self.function.is_zero:
             return True
 
+    def _eval_is_extended_real(self):
+        return self.function.is_extended_real
+
+    def _eval_is_positive(self):
+        return self.function.is_positive
path: sympy/concrete/summations.py
text: @user1:
Similar comments about infinite sums. Also we have to consider the empty sum here. I just went to show an example and got these:
```julia
In [8]: Sum(1, (x, 0, 0)).doit()                                                                                                  
Out[8]: 1

In [9]: Sum(1, (x, 0, -1)).doit()                                                                                                 
Out[9]: 0

In [10]: Sum(1, (x, 0, -5)).doit()                                                                                                
Out[10]: -4
```
I don't know if I think those are correct though...


@author:
Inverted limits are clearly worse... I was under the impression that these were inverted, but I see it is in doit, not on initialization.

@author:
Should still be some cases where it is possible to determine that it works.

@user1:
I still understand the convention for Sum limits well enough to answer this: how do I define an empty Sum?

@author:
I do not think that it is feasible, unless it is the same as having a negative difference of one.