Repository: sympy/sympy
Pull request: Inverse trigonometric evaluation for compatible arguments
#### References to other Issues or PRs
Fixes #14320 

#### Brief description of what is fixed or changed
Added support for evaluation of inverse trigonometric non-symbolic expressions with real arguments, taking into account the range of inverse trigonometric functions.
This has the advantage of directly evaluating expressions of `sin/cos` arguments in `asin/acos`, `tan/cot` arguments in `atan/acot` and `sec/csc` arguments in `asec/acsc`.
This can even complicated expressions (of compatible functions).
```python
>> pprint(asin(sin(2)))
-2 + π

>> pprint(atan(cot(7)))
     5⋅π
-7 + ───
      2 

>> pprint(asin(sin(acos(cos(20)) + acos(cos(30)))))
-3⋅π + 10
```
@normalhuman @jksuom @asmeurer Please review.


Problem statement:
Improper results with inverse trigonometric functions
```python
>>> pprint(asin(sin(2)))
asin(sin(2))

>>> pprint(asin(sin(2)).simplify())
2

>>> pprint(asin(sin(2)).trigsimp())
asin(sin(2))
```
Using simplify, does not give the correct value as range of `asin` is `[-pi/2,pi/2]`. 
The other statements are not able to solve the same expression.

Having the ability to solve non-symbolic expressions involving compatible trigonometric functions would be useful.

Hints from issue discussion:
This was recently discussed on gitter. The attribute `inverse` does not differentiate between right inverse and left inverse. Therefore [this line](https://github.com/sympy/sympy/blob/master/sympy/simplify/simplify.py#L531) may lead to a wrong result.
> The attribute inverse does not differentiate between right inverse and left inverse.

Yes, as you mentioned the line that leads to wrong result is integral to simplify. 
Reduction by inverse trigonometric functions themselves, would be useful in such cases.

Commit message: Added is_comparable check

Patch to review:
diff --git a/sympy/functions/elementary/tests/test_trigonometric.py b/sympy/functions/elementary/tests/test_trigonometric.py
index c2eaf6668062..895fd5e92061 100644
--- a/sympy/functions/elementary/tests/test_trigonometric.py
+++ b/sympy/functions/elementary/tests/test_trigonometric.py
@@ -1562,3 +1562,20 @@ def test_real_assumptions():
     assert atan(n).is_negative is True
     assert acot(p).is_positive is True
     assert acot(n).is_negative is True
+
+def test_issue_14320():
+    assert asin(sin(2)) == -2 + pi and (-pi/2 <= -2 + pi <= pi/2) and sin(2) == sin(-2 + pi)
+    assert asin(cos(2)) == -2 + pi/2 and (-pi/2 <= -2 + pi/2 <= pi/2) and cos(2) == sin(-2 + pi/2)
+    assert acos(sin(2)) == -pi/2 + 2 and (0 <= -pi/2 + 2 <= pi) and sin(2) == cos(-pi/2 + 2)
+    assert acos(cos(20)) == -6*pi + 20 and (0 <= -6*pi + 20 <= pi) and cos(20) == cos(-6*pi + 20)
+    assert acos(cos(30)) == -30 + 10*pi and (0 <= -30 + 10*pi <= pi) and cos(30) == cos(-30 + 10*pi)
+
+    assert atan(tan(17)) == -5*pi + 17 and (-pi/2 < -5*pi + 17 < pi/2) and tan(17) == tan(-5*pi + 17)
+    assert atan(cot(12)) == -12 + 7*pi/2 and (-pi/2 < -12 + 7*pi/2 < pi/2) and cot(12) == tan(-12 + 7*pi/2)
+    assert acot(cot(15)) == -4*pi + 15 and (0 < -4*pi + 15 < pi) and cot(15) == cot(-4*pi + 15)
+    assert acot(tan(19)) == -19 + 13*pi/2 and (0 < -19 + 13*pi/2 < pi) and tan(19) == cot(-19 + 13*pi/2)
+
+    assert asec(sec(11)) == -11 + 4*pi and (0 <= -11 + 4*pi <= pi) and cos(11) == cos(-11 + 4*pi)
+    assert asec(csc(13)) == -13 + 9*pi/2 and (0 <= -13 + 9*pi/2 <= pi) and sin(13) == cos(-13 + 9*pi/2)
+    assert acsc(csc(14)) == -4*pi + 14 and (-pi/2 <= -4*pi + 14 <= pi/2) and sin(14) == sin(-4*pi + 14)
+    assert acsc(sec(10)) == -7*pi/2 + 10 and (-pi/2 <= -7*pi/2 + 10 <= pi/2) and cos(10) == sin(-7*pi/2 + 10)
diff --git a/sympy/functions/elementary/trigonometric.py b/sympy/functions/elementary/trigonometric.py
index eefd577930d3..5ccb347c845f 100644
--- a/sympy/functions/elementary/trigonometric.py
+++ b/sympy/functions/elementary/trigonometric.py
@@ -1948,6 +1948,26 @@ def eval(cls, arg):
         if i_coeff is not None:
             return S.ImaginaryUnit * asinh(i_coeff)
 
+        if isinstance(arg, sin):
+            ang = arg.args[0]
+            if ang.is_comparable:
+                ang %= 2*pi # restrict to [0,2*pi)
+                if ang > pi: # restrict to (-pi,pi]
+                    ang = pi - ang
+
+                # restrict to [-pi/2,pi/2]
+                if ang > pi/2:
+                    ang = pi - ang
+                if ang < -pi/2:
+                    ang = -pi - ang
+
+                return ang
+
+        if isinstance(arg, cos): # acos(x) + asin(x) = pi/2
+            ang = arg.args[0]
+            if ang.is_comparable:
+                return pi/2 - acos(arg)
+
     @staticmethod
     @cacheit
     def taylor_term(n, x, *previous_terms):
@@ -2090,6 +2110,20 @@ def eval(cls, arg):
             if arg in cst_table:
                 return cst_table[arg]
 
+        if isinstance(arg, cos):
+            ang = arg.args[0]
+            if ang.is_comparable:
+                ang %= 2*pi # restrict to [0,2*pi)
+                if ang > pi: # restrict to [0,pi]
+                    ang = 2*pi - ang
+
+                return ang
+
+        if isinstance(arg, sin): # acos(x) + asin(x) = pi/2
+            ang = arg.args[0]
+            if ang.is_comparable:
+                return pi/2 - asin(arg)
+
     @staticmethod
     @cacheit
     def taylor_term(n, x, *previous_terms):
@@ -2261,6 +2295,20 @@ def eval(cls, arg):
         if i_coeff is not None:
             return S.ImaginaryUnit * atanh(i_coeff)
 
+        if isinstance(arg, tan):
+            ang = arg.args[0]
+            if ang.is_comparable:
+                ang %= pi # restrict to [0,pi)
+                if ang > pi/2: # restrict to [-pi/2,pi/2]
+                    ang = pi - ang
+
+                return ang
+
+        if isinstance(arg, cot): # atan(x) + acot(x) = pi/2
+            ang = arg.args[0]
+            if ang.is_comparable:
+                return pi/2 - acot(arg)
+
     @staticmethod
     @cacheit
     def taylor_term(n, x, *previous_terms):
@@ -2405,6 +2453,16 @@ def eval(cls, arg):
         if i_coeff is not None:
             return -S.ImaginaryUnit * acoth(i_coeff)
 
+        if isinstance(arg, cot):
+            ang = arg.args[0]
+            if ang.is_comparable:
+                return ang % pi # restrict to [0,pi)
+
+        if isinstance(arg, tan): # atan(x) + acot(x) = pi/2
+            ang = arg.args[0]
+            if ang.is_comparable:
+                return pi/2 - atan(arg)
+
     @staticmethod
     @cacheit
     def taylor_term(n, x, *previous_terms):
@@ -2531,6 +2589,20 @@ def eval(cls, arg):
         if arg in [S.Infinity, S.NegativeInfinity, S.ComplexInfinity]:
             return S.Pi/2
 
+        if isinstance(arg, sec):
+            ang = arg.args[0]
+            if ang.is_comparable:
+                ang %= 2*pi # restrict to [0,2*pi)
+                if ang > pi: # restrict to [0,pi]
+                    ang = 2*pi - ang
+
+                return ang
+
+        if isinstance(arg, csc): # asec(x) + acsc(x) = pi/2
+            ang = arg.args[0]
+            if ang.is_comparable:
+                return pi/2 - acsc(arg)
+
     def fdiff(self, argindex=1):
         if argindex == 1:
             return 1/(self.args[0]**2*sqrt(1 - 1/self.args[0]**2))
@@ -2623,6 +2695,26 @@ def eval(cls, arg):
         if arg in [S.Infinity, S.NegativeInfinity, S.ComplexInfinity]:
             return S.Zero
 
+        if isinstance(arg, csc):
+            ang = arg.args[0]
+            if ang.is_comparable:
+                ang %= 2*pi # restrict to [0,2*pi)
+                if ang > pi: # restrict to (-pi,pi]
+                    ang = pi - ang
+
+                # restrict to [-pi/2,pi/2]
+                if ang > pi/2:
+                    ang = pi - ang
+                if ang < -pi/2:
+                    ang = -pi - ang
+
+                return ang
+
+        if isinstance(arg, sec): # asec(x) + acsc(x) = pi/2
+            ang = arg.args[0]
+            if ang.is_comparable:
+                return pi/2 - asec(arg)
+
     def fdiff(self, argindex=1):
         if argindex == 1:
             return -1/(self.args[0]**2*sqrt(1 - 1/self.args[0]**2))


Code review comments:
diff_hunk: @@ -2405,6 +2453,16 @@ def eval(cls, arg):
         if i_coeff is not None:
             return -S.ImaginaryUnit * acoth(i_coeff)
 
+        if isinstance(arg, cot):
+            ang = arg.args[0]
+            if ang.is_comparable:
+                return ang % pi # restrict to [0,pi)
path: sympy/functions/elementary/trigonometric.py
text: @user1:
This part also needs to be revised, considering that acot takes values in (-pi/2, pi/2] instead of [0, pi).

@author:
Okay, so basically the revision in `acot/cot` would be exactly the same as `atan/tan`.
And, this will need `atan/cot` and `acot/tan` to be revised for them to lie in range `(-pi/2,pi/2]` and `(-pi/2,pi/2)` respectively.

diff_hunk: @@ -2261,6 +2295,20 @@ def eval(cls, arg):
         if i_coeff is not None:
             return S.ImaginaryUnit * atanh(i_coeff)
 
+        if isinstance(arg, tan):
+            ang = arg.args[0]
+            if ang.is_comparable:
+                ang %= pi # restrict to [0,pi)
+                if ang > pi/2: # restrict to [-pi/2,pi/2]
+                    ang = pi - ang
path: sympy/functions/elementary/trigonometric.py
text: @user1:
This line should be `ang = ang - pi`, as tan is pi-periodic. `tan(pi - ang)` is not tan(ang).   For example, 
```
>>> atan(tan(15))
-15 + 5*pi
```
is wrong. (And this example should be added to the tests). 



@author:
Yes, good catch. Thanks, I will add it.