Repository: sympy/sympy
Pull request: Modified is_positive for cosh function
<!-- 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. -->
Closes #11924 
Fixes #11721

#### Brief description of what is fixed or changed


#### 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 -->
NO ENTRY
<!-- END RELEASE NOTES -->


Problem statement:
abs(cosh(x)) should simplify to cosh(x) for real x
Sympy 1.0 only simplifies in a limited set of cases:

``` py
>>> x = sympy.var('x', real=True)
>>> abs(sympy.cosh(x)).simplify()
Abs(cosh(x)) # bad
>>> (sympy.cosh(x) - abs(sympy.cosh(x))).simplify()
0 # good
>>> (sympy.cosh(x) + abs(sympy.cosh(x))).simplify()
cosh(x) + Abs(cosh(x)) # bad
```

Hints from issue discussion:
I think to make it work in general `cosh(x).is_positive` should be True. It's strange that simplify works. It must be attempting some rewrites that cause it to reduce (like `rewrite(exp)`). 

These examples all work fine now:
```julia
In [169]: x = Symbol('x', real=True)                                                                                                                          

In [170]: abs(cosh(x))                                                                                                                                        
Out[170]: cosh(x)

In [171]: cosh(x) - abs(cosh(x))                                                                                                                              
Out[171]: 0

In [172]: cosh(x) + abs(cosh(x))                                                                                                                              
Out[172]: 2⋅cosh(x)
```
Not sure when that was fixed...

Looks like this was fixed in #13678

Commit message: increased coverage

Patch to review:
diff --git a/sympy/functions/elementary/hyperbolic.py b/sympy/functions/elementary/hyperbolic.py
index be58291b9b69..595750b8d244 100644
--- a/sympy/functions/elementary/hyperbolic.py
+++ b/sympy/functions/elementary/hyperbolic.py
@@ -8,6 +8,10 @@
 from sympy.functions.elementary.miscellaneous import sqrt
 from sympy.functions.elementary.integers import floor
 
+from sympy import pi, Eq
+from sympy.logic import Or, And
+
+
 
 def _rewrite_hyperbolics_as_exp(expr):
     expr = sympify(expr)
@@ -273,6 +277,48 @@ class cosh(HyperbolicFunction):
     sinh, tanh, acosh
     """
 
+    def _eval_is_positive(self):
+        arg = self.args[0]
+
+        if arg.is_real:
+            return True
+
+        re, im = arg.as_real_imag()
+        im_mod = im % (2*pi)
+
+        if im_mod == 0:
+            return True
+
+        if re == 0:
+            if im_mod < pi/2 or im_mod > 3*pi/2:
+                return True
+            elif im_mod >= pi/2 or im_mod <= 3*pi/2:
+                return False
+
+        return Or(And(Eq(re, 0), Or(im_mod < pi/2, im_mod > 3*pi/2)), Eq(im_mod, 0))
+
+
+    def _eval_is_nonnegative(self):
+        arg = self.args[0]
+
+        if arg.is_real:
+            return True
+
+        re, im = arg.as_real_imag()
+        im_mod = im % (2*pi)
+
+        if im_mod == 0:
+            return True
+
+        if re == 0:
+            if im_mod <= pi/2 or im_mod >= 3*pi/2:
+                return True
+            elif im_mod > pi/2 or im_mod < 3*pi/2:
+                return False
+
+        return Or(And(Eq(re, 0), Or(im_mod <= pi/2, im_mod >= 3*pi/2)), Eq(im_mod, 0))
+
+
     def fdiff(self, argindex=1):
         if argindex == 1:
             return sinh(self.args[0])
@@ -420,10 +466,6 @@ def _eval_is_real(self):
         re, im = arg.as_real_imag()
         return (im%pi).is_zero
 
-    def _eval_is_positive(self):
-        if self.args[0].is_extended_real:
-            return True
-
     def _eval_is_finite(self):
         arg = self.args[0]
         return arg.is_finite
diff --git a/sympy/functions/elementary/tests/test_hyperbolic.py b/sympy/functions/elementary/tests/test_hyperbolic.py
index d1071c0ee5eb..36e8e059d511 100644
--- a/sympy/functions/elementary/tests/test_hyperbolic.py
+++ b/sympy/functions/elementary/tests/test_hyperbolic.py
@@ -1097,6 +1097,29 @@ def test_cosh_expansion():
     assert cosh(3*x).expand(trig=True).expand() == \
         3*sinh(x)**2*cosh(x) + cosh(x)**3
 
+def test_cosh_positive():
+    # See issue 11721
+    # cosh(x) is positive for real values of x
+    x = symbols('x')
+    k = symbols('k', real=True)
+    n = symbols('n', integer=True)
+
+    assert cosh(k).is_positive == True
+    assert cosh(k + 2*n*pi*I).is_positive == True
+    assert cosh(I*pi/4).is_positive == True
+    assert cosh(3*I*pi/4).is_positive == False
+
+def test_cosh_nonnegative():
+    x = symbols('x')
+    k = symbols('k', real=True)
+    n = symbols('n', integer=True)
+
+    assert cosh(k).is_nonnegative == True
+    assert cosh(k + 2*n*pi*I).is_nonnegative == True
+    assert cosh(I*pi/4).is_nonnegative == True
+    assert cosh(3*I*pi/4).is_nonnegative == False
+    assert cosh(S.Zero).is_nonnegative == True
+
 def test_real_assumptions():
     z = Symbol('z', real=False)
     assert sinh(z).is_real is None


Code review comments:
diff_hunk: @@ -273,6 +277,48 @@ class cosh(HyperbolicFunction):
     sinh, tanh, acosh
     """
 
+    def _eval_is_positive(self):
+        arg = self.args[0]
+
+        if arg.is_real:
+            return True
+
+        re, im = arg.as_real_imag()
+        im_mod = im % (2*pi)
+
+        if im_mod == 0:
+            return True
+
+        if re == 0:
+            if im_mod < pi/2 or im_mod > 3*pi/2:
+                return True
+            elif im_mod >= pi/2 or im_mod <= 3*pi/2:
+                return False
+
+        return Or(And(Eq(re, 0), Or(im_mod < pi/2, im_mod > 3*pi/2)), Eq(im_mod, 0))
path: sympy/functions/elementary/hyperbolic.py
text: @user1:
The `_eval_is` routines should should return non-symbolic fuzzy bool (True/False/None) rather than symbolic Boolean (S.True/S.False/Boolean).

We could replace the logic above with fuzzy_or, fuzzy_and, re.is_zero etc. At the end if no determination is possible we should always return None (although fuzzy_or can do that for you).

diff_hunk: @@ -1097,6 +1097,29 @@ def test_cosh_expansion():
     assert cosh(3*x).expand(trig=True).expand() == \
         3*sinh(x)**2*cosh(x) + cosh(x)**3
 
+def test_cosh_positive():
+    # See issue 11721
+    # cosh(x) is positive for real values of x
+    x = symbols('x')
+    k = symbols('k', real=True)
+    n = symbols('n', integer=True)
+
+    assert cosh(k).is_positive == True
path: sympy/functions/elementary/tests/test_hyperbolic.py
text: @user1:
Use `is` rather than `==` in these tests. This should definitely give the singleton True and not anything else that happens to be equal to it.