Repository: sympy/sympy
Pull request: Rewrite imaginary argument to acos as asin
<!-- 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. -->
Partly fixes #17141 

#### Brief description of what is fixed or changed
`acos` with a purely imaginary input argument is rewritten to `asin`. This avoids the recursion error as happened in #17141.

#### Other comments

I also improved the documentation of some trigonometric functions a bit.

#### 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 -->
* functions
    * `acos` with purely imaginary input is automatically rewritten to `asin`.
<!-- END RELEASE NOTES -->


Problem statement:
simplify(x**(1 / acos(I))) recursion error
```
>>> from sympy import *
>>> x = Symbol('x')
>>> e = x**(1 / acos(I))
>>> print(simplify(e))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/e/se/sympy/simplify/simplify.py", line 559, in simplify
    expr = bottom_up(expr, lambda w: getattr(w, 'normal', lambda: w)())
  File "/home/e/se/sympy/simplify/simplify.py", line 1077, in bottom_up
    rv = F(rv)
  File "/home/e/se/sympy/simplify/simplify.py", line 559, in <lambda>
    expr = bottom_up(expr, lambda w: getattr(w, 'normal', lambda: w)())
  File "/home/e/se/sympy/core/expr.py", line 2141, in normal
    n, d = self.as_numer_denom()
  File "/home/e/se/sympy/core/power.py", line 1322, in as_numer_denom
    return self.func(n, exp), self.func(d, exp)
  File "/home/e/se/sympy/core/cache.py", line 94, in wrapper
    retval = cfunc(*args, **kwargs)
  File "/home/e/se/sympy/core/power.py", line 283, in __new__
    if abs(e).is_infinite:
  File "/home/e/se/sympy/core/expr.py", line 167, in __abs__
    return Abs(self)
  File "/home/e/se/sympy/core/cache.py", line 94, in wrapper
    retval = cfunc(*args, **kwargs)
  File "/home/e/se/sympy/core/function.py", line 462, in __new__
    result = super(Function, cls).__new__(cls, *args, **options)
  File "/home/e/se/sympy/core/cache.py", line 94, in wrapper
    retval = cfunc(*args, **kwargs)
  File "/home/e/se/sympy/core/function.py", line 277, in __new__
    evaluated = cls.eval(*args)
  File "/home/e/se/sympy/functions/elementary/complexes.py", line 510, in eval
    return exp(re(exponent*z))
  File "/home/e/se/sympy/core/cache.py", line 94, in wrapper
    retval = cfunc(*args, **kwargs)
  File "/home/e/se/sympy/core/function.py", line 462, in __new__
    result = super(Function, cls).__new__(cls, *args, **options)
  File "/home/e/se/sympy/core/cache.py", line 94, in wrapper
    retval = cfunc(*args, **kwargs)
  File "/home/e/se/sympy/core/function.py", line 277, in __new__
    evaluated = cls.eval(*args)
  File "/home/e/se/sympy/functions/elementary/complexes.py", line 82, in eval
    real_imag = term.as_real_imag(ignore=arg)
  File "/home/e/se/sympy/core/mul.py", line 783, in as_real_imag
    r, i = a.as_real_imag()
  File "/home/e/se/sympy/functions/elementary/exponential.py", line 693, in as_real_imag
    abs = Abs(self.args[0].expand(deep, **hints))
  File "/home/e/se/sympy/core/cache.py", line 94, in wrapper
    retval = cfunc(*args, **kwargs)
  File "/home/e/se/sympy/core/function.py", line 462, in __new__
    result = super(Function, cls).__new__(cls, *args, **options)
  File "/home/e/se/sympy/core/cache.py", line 94, in wrapper
    retval = cfunc(*args, **kwargs)
  File "/home/e/se/sympy/core/function.py", line 277, in __new__
    evaluated = cls.eval(*args)
  File "/home/e/se/sympy/functions/elementary/complexes.py", line 510, in eval
    return exp(re(exponent*z))
  File "/home/e/se/sympy/core/cache.py", line 94, in wrapper
    retval = cfunc(*args, **kwargs)
  File "/home/e/se/sympy/core/function.py", line 462, in __new__
    result = super(Function, cls).__new__(cls, *args, **options)
  File "/home/e/se/sympy/core/cache.py", line 94, in wrapper
    retval = cfunc(*args, **kwargs)
  File "/home/e/se/sympy/core/function.py", line 277, in __new__
    evaluated = cls.eval(*args)

...

    retval = cfunc(*args, **kwargs)
RecursionError: maximum recursion depth exceeded while calling a Python object
```

Hints from issue discussion:
The recursion error is manifested when we try to call `as_real_imag()` on `log(acos(-I)*acos(I)/sqrt(acos(-I)**2*acos(I)**2))/2`.

Or for a simpler example, `acos(-I)**2*acos(I)**2`.
Even simpler example: `simplify(2**acos(I)**2)`

It seems like `asin(I)` evaluates to `I*log(1 + sqrt(2))` but `acos(I)` is kept unevaluated. One possibility would be to evaluate it to `-I*log(I+I*sqrt(2))` (see e.g. https://www.mathworks.com/help/matlab/ref/acos.html ). Not sure why it is done for `asin` and not `acos`.
These lines are in `asin`, but nothing corresponding for `acos`:

https://github.com/sympy/sympy/blob/d2308e8eb55ed6acb24bcd8dbb8c410c9e5e9d5c/sympy/functions/elementary/trigonometric.py#L1972-L1974
#17154 will fix these cases, but not: `simplify(2**acos(I+1)**2)`

It is worth noting that a RecursionError also happens for `(2**acos(I+1)**2).rewrite('log').expand()`. For this case, the minimum example I've found is `(2**(1 - log(sqrt(I) + 1)**2)).expand()`. Removing either the sqrt or any of the 1s leads to that it returns properly. Also changing - to + removes the RecursionError. 
The problem for the last example seems to be in computing: `atan2(0, atan(sqrt(2)/(2*(sqrt(2)/2 + 1)))**2 + log(sqrt(2)/2 - sqrt(2)*I/2 - sqrt(2)*sqrt(I)*I/2 + sqrt(2)*sqrt(I)/2+sqrt(I))**2)` which leads to a RecursionError.

A smaller example for this is: `atan2(0, 1 + log(-sqrt(2)*I/2 - sqrt(2)*sqrt(I)*I/2 + sqrt(2)*sqrt(I)/2+sqrt(I))**2)`

Removing any of the sqrt/I terms removes the error, so it seems to require a certain combination of terms here.

The bad circle is atan2 => `return -S.ImaginaryUnit*log((x + S.ImaginaryUnit*y)/sqrt(x**2 + y**2))`
sqrt => x**S.Half
Pow => `s = exp(2*S.Pi*S.ImaginaryUnit*other*floor(S.Half - e*arg(b)/(2*S.Pi)))`
arg => atan2 with same arguments.

(Removing the 1 in front makes SymPy "hang" (or at least take a very long time to compute).)

Commit message: Corrected LaTeX

Patch to review:
diff --git a/sympy/functions/elementary/tests/test_trigonometric.py b/sympy/functions/elementary/tests/test_trigonometric.py
index 1edb173848e4..51f08d52f433 100644
--- a/sympy/functions/elementary/tests/test_trigonometric.py
+++ b/sympy/functions/elementary/tests/test_trigonometric.py
@@ -855,6 +855,8 @@ def test_acos():
     assert acos(sqrt(2)/2) == pi/4
     assert acos(-sqrt(2)/2) == (3*pi)/4
 
+    assert acos(2*I) == pi/2 - asin(2*I)
+
     assert acos(x).diff(x) == -1/sqrt(1 - x**2)
 
     assert acos(0.2).is_real is True
diff --git a/sympy/functions/elementary/trigonometric.py b/sympy/functions/elementary/trigonometric.py
index 0ff7c05402d3..a68ee1615fdb 100644
--- a/sympy/functions/elementary/trigonometric.py
+++ b/sympy/functions/elementary/trigonometric.py
@@ -107,6 +107,7 @@ def _peeloff_pi(arg):
     (x, pi/2)
     >>> peel(x + 2*pi/3 + pi*y)
     (x + pi*y + pi/6, pi/2)
+
     """
     for a in Add.make_args(arg):
         if a is S.Pi:
@@ -159,6 +160,7 @@ def _pi_coeff(arg, cycles=1):
     2
     >>> coeff(2*Dummy(even=True)*pi)
     0
+
     """
     arg = sympify(arg)
     if arg is S.Pi:
@@ -241,6 +243,7 @@ class sin(TrigonometricFunction):
     .. [2] http://dlmf.nist.gov/4.14
     .. [3] http://functions.wolfram.com/ElementaryFunctions/Sin
     .. [4] http://mathworld.wolfram.com/TrigonometryAngles.html
+
     """
 
     def period(self, symbol=None):
@@ -515,6 +518,7 @@ class cos(TrigonometricFunction):
     .. [1] https://en.wikipedia.org/wiki/Trigonometric_functions
     .. [2] http://dlmf.nist.gov/4.14
     .. [3] http://functions.wolfram.com/ElementaryFunctions/Cos
+
     """
 
     def period(self, symbol=None):
@@ -936,6 +940,7 @@ class tan(TrigonometricFunction):
     .. [1] https://en.wikipedia.org/wiki/Trigonometric_functions
     .. [2] http://dlmf.nist.gov/4.14
     .. [3] http://functions.wolfram.com/ElementaryFunctions/Tan
+
     """
 
     def period(self, symbol=None):
@@ -1239,6 +1244,7 @@ class cot(TrigonometricFunction):
     .. [1] https://en.wikipedia.org/wiki/Trigonometric_functions
     .. [2] http://dlmf.nist.gov/4.14
     .. [3] http://functions.wolfram.com/ElementaryFunctions/Cot
+
     """
 
     def period(self, symbol=None):
@@ -1644,6 +1650,7 @@ class sec(ReciprocalTrigonometricFunction):
     .. [1] https://en.wikipedia.org/wiki/Trigonometric_functions
     .. [2] http://dlmf.nist.gov/4.14
     .. [3] http://functions.wolfram.com/ElementaryFunctions/Sec
+
     """
 
     _reciprocal_of = cos
@@ -1724,6 +1731,7 @@ class csc(ReciprocalTrigonometricFunction):
     .. [1] https://en.wikipedia.org/wiki/Trigonometric_functions
     .. [2] http://dlmf.nist.gov/4.14
     .. [3] http://functions.wolfram.com/ElementaryFunctions/Csc
+
     """
 
     _reciprocal_of = sin
@@ -1773,7 +1781,16 @@ def taylor_term(n, x, *previous_terms):
 
 
 class sinc(Function):
-    r"""Represents unnormalized sinc function
+    r"""
+    Represents an unnormalized sinc function:
+
+    .. math::
+
+        \operatorname{sinc}(x) =
+        \begin{cases}
+          \frac{\sin x}{x} & \qquad x \neq 0 \\
+          1 & \qquad x = 0
+        \end{cases}
 
     Examples
     ========
@@ -1875,9 +1892,11 @@ class asin(InverseTrigonometricFunction):
     Notes
     =====
 
-    asin(x) will evaluate automatically in the cases oo, -oo, 0, 1,
-    -1 and for some instances when the result is a rational multiple
-    of pi (see the eval class method).
+    ``asin(x)`` will evaluate automatically in the cases ``oo``, ``-oo``,
+    ``0``, ``1``, ``-1`` and for some instances when the result is a rational
+    multiple of pi (see the eval class method).
+
+    A purely imaginary argument will lead to an asinh expression.
 
     Examples
     ========
@@ -1900,6 +1919,7 @@ class asin(InverseTrigonometricFunction):
     .. [1] https://en.wikipedia.org/wiki/Inverse_trigonometric_functions
     .. [2] http://dlmf.nist.gov/4.23
     .. [3] http://functions.wolfram.com/ElementaryFunctions/ArcSin
+
     """
 
     def fdiff(self, argindex=1):
@@ -2057,11 +2077,14 @@ class acos(InverseTrigonometricFunction):
     =====
 
     ``acos(x)`` will evaluate automatically in the cases
-    ``oo``, ``-oo``, ``0``, ``1``, ``-1``.
+    ``oo``, ``-oo``, ``0``, ``1``, ``-1`` and for some instances when
+    the result is a rational multiple of pi (see the eval class method).
 
     ``acos(zoo)`` evaluates to ``zoo``
     (see note in :py:class`sympy.functions.elementary.trigonometric.asec`)
 
+    A purely imaginary argument will be rewritten to asinh.
+
     Examples
     ========
 
@@ -2085,6 +2108,7 @@ class acos(InverseTrigonometricFunction):
     .. [1] https://en.wikipedia.org/wiki/Inverse_trigonometric_functions
     .. [2] http://dlmf.nist.gov/4.23
     .. [3] http://functions.wolfram.com/ElementaryFunctions/ArcCos
+
     """
 
     def fdiff(self, argindex=1):
@@ -2135,6 +2159,10 @@ def eval(cls, arg):
             if arg in cst_table:
                 return cst_table[arg]
 
+        i_coeff = arg.as_coefficient(S.ImaginaryUnit)
+        if i_coeff is not None:
+            return pi/2 - asin(arg)
+
         if isinstance(arg, cos):
             ang = arg.args[0]
             if ang.is_comparable:
@@ -2229,8 +2257,9 @@ class atan(InverseTrigonometricFunction):
     Notes
     =====
 
-    atan(x) will evaluate automatically in the cases
-    oo, -oo, 0, 1, -1.
+    ``atan(x)`` will evaluate automatically in the cases
+    ``oo``, ``-oo``, ``0``, ``1``, ``-1`` and for some instances when the
+    result is a rational multiple of pi (see the eval class method).
 
     Examples
     ========
@@ -2255,6 +2284,7 @@ class atan(InverseTrigonometricFunction):
     .. [1] https://en.wikipedia.org/wiki/Inverse_trigonometric_functions
     .. [2] http://dlmf.nist.gov/4.23
     .. [3] http://functions.wolfram.com/ElementaryFunctions/ArcTan
+
     """
 
     def fdiff(self, argindex=1):
@@ -2418,6 +2448,7 @@ class acot(InverseTrigonometricFunction):
     .. [1] https://en.wikipedia.org/wiki/Inverse_trigonometric_functions
     .. [2] http://dlmf.nist.gov/4.23
     .. [3] http://functions.wolfram.com/ElementaryFunctions/ArcCot
+
     """
 
     def fdiff(self, argindex=1):
@@ -2579,16 +2610,16 @@ class asec(InverseTrigonometricFunction):
     it can be defined [4]_ as
 
     .. math::
-        sec^{-1}(z) = -i*(log(\sqrt{1 - z^2} + 1) / z)
+        \operatorname{sec^{-1}}(z) = -i\frac{\log\left(\sqrt{1 - z^2} + 1\right)}{z}
 
     At ``x = 0``, for positive branch cut, the limit evaluates to ``zoo``. For
     negative branch cut, the limit
 
     .. math::
-        \lim_{z \to 0}-i*(log(-\sqrt{1 - z^2} + 1) / z)
+        \lim_{z \to 0}-i\frac{\log\left(-\sqrt{1 - z^2} + 1\right)}{z}
 
-    simplifies to :math:`-i*log(z/2 + O(z^3))` which ultimately evaluates to
-    ``zoo``.
+    simplifies to :math:`-i\log\left(z/2 + O\left(z^3\right)\right)` which
+    ultimately evaluates to ``zoo``.
 
     As ``asex(x)`` = ``asec(1/x)``, a similar argument can be given for
     ``acos(x)``.
@@ -2615,6 +2646,7 @@ class asec(InverseTrigonometricFunction):
     .. [2] http://dlmf.nist.gov/4.23
     .. [3] http://functions.wolfram.com/ElementaryFunctions/ArcSec
     .. [4] http://reference.wolfram.com/language/ref/ArcSec.html
+
     """
 
     @classmethod
@@ -2723,6 +2755,7 @@ class acsc(InverseTrigonometricFunction):
     .. [1] https://en.wikipedia.org/wiki/Inverse_trigonometric_functions
     .. [2] http://dlmf.nist.gov/4.23
     .. [3] http://functions.wolfram.com/ElementaryFunctions/ArcCsc
+
     """
 
     @classmethod
@@ -2820,6 +2853,13 @@ class atan2(InverseTrigonometricFunction):
     Attention: Note the role reversal of both arguments. The `y`-coordinate
     is the first argument and the `x`-coordinate the second.
 
+    If either `x` or `y` is complex:
+
+    .. math::
+
+        \operatorname{atan2}(y, x) =
+            -i\log\left(\frac{x + iy}{\sqrt{x**2 + y**2}}\right)
+
     Examples
     ========
 
@@ -2891,6 +2931,7 @@ class atan2(InverseTrigonometricFunction):
     .. [1] https://en.wikipedia.org/wiki/Inverse_trigonometric_functions
     .. [2] https://en.wikipedia.org/wiki/Atan2
     .. [3] http://functions.wolfram.com/ElementaryFunctions/ArcTan2
+
     """
 
     @classmethod
diff --git a/sympy/simplify/tests/test_simplify.py b/sympy/simplify/tests/test_simplify.py
index 23d8620ef41a..34a550aa30ae 100644
--- a/sympy/simplify/tests/test_simplify.py
+++ b/sympy/simplify/tests/test_simplify.py
@@ -11,8 +11,8 @@
 from sympy.core.mul import _keep_coeff
 from sympy.core.expr import unchanged
 from sympy.simplify.simplify import nthroot, inversecombine
-from sympy.utilities.pytest import XFAIL, slow
-from sympy.core.compatibility import range
+from sympy.utilities.pytest import XFAIL, slow, raises
+from sympy.core.compatibility import range, PY3
 
 from sympy.abc import x, y, z, t, a, b, c, d, e, f, g, h, i, k
 
@@ -820,3 +820,19 @@ def test_issue_7971():
     z = Integral(x, (x, 1, 1))
     assert z != 0
     assert simplify(z) is S.Zero
+
+
+def test_issue_17141():
+    # Check that there is no RecursionError
+    assert simplify(x**(1 / acos(I))) == x**(2/(pi - 2*I*log(1 + sqrt(2))))
+    assert simplify(acos(-I)**2*acos(I)**2) == \
+           log(1 + sqrt(2))**4 + pi**2*log(1 + sqrt(2))**2/2 + pi**4/16
+    assert simplify(2**acos(I)**2) == 2**((pi - 2*I*log(1 + sqrt(2)))**2/4)
+
+    # However, for a complex number it still happens
+    if PY3:
+        raises(RecursionError, lambda: simplify(2**acos(I+1)**2))
+        raises(RecursionError, lambda: simplify((2**acos(I+1)**2).rewrite('log')))
+    else:
+        raises(RuntimeError, lambda: simplify(2**acos(I+1)**2))
+        raises(RuntimeError, lambda: simplify((2**acos(I+1)**2).rewrite('log')))


Code review comments:
diff_hunk: @@ -2616,10 +2616,10 @@ class asec(InverseTrigonometricFunction):
     negative branch cut, the limit
 
     .. math::
-        \lim_{z \to 0}-i\frac{\log\left(-\sqrt{1 - z^2} + 1}{z}
+        \lim_{z \to 0}-i\frac{\log\left(-\sqrt{1 - z^2} + 1\right)}{z}
 
-    simplifies to :math:`-i*\log(z/2 + O(z^3))` which ultimately evaluates to
-    ``zoo``.
+    simplifies to :math:`-i\log\left(z/2 + O\left(z^3\right)\right)` which
+    ultimately evaluates to ``zoo``.
 
     As ``asex(x)`` = ``asec(1/x)``, a similar argument can be given for
path: sympy/functions/elementary/trigonometric.py
text: @user1:
asex?

@author:
Well spotted. Will change it! (The trig documentation could need a major overhaul...)