Repository: sympy/sympy
Pull request: Reduce exp() argument modulo 2*I*pi
#### References to other Issues or PRs
Fixes #17216

#### Brief description of what is fixed or changed
`exp()` arguments with an imaginary part that is a rational multiple of pi
will now be reduced mod `2*pi*I`. (In fact the principal value will be retained.)

Before:
```
In [8]: exp(9*I*pi/4) - exp(I*pi/4)
Out[8]: -exp(I*pi/4) + exp(9*I*pi/4)

In [9]: simplify(_)
Out[9]: -(-1)**(1/4) + exp(9*I*pi/4)
```

After:
```
In [4]: exp(9*I*pi/4) - exp(I*pi/4)
Out[4]: 0
```


#### Other comments


#### Release Notes


<!-- BEGIN RELEASE NOTES -->

* functions
  * `exp()` now reduces its argument mod `2*pi*I`.
<!-- END RELEASE NOTES -->


Problem statement:
exp doesn't simplify based on its periodicity
In current master, `exp` doesn't use its periodicity to automatically reduce its argument, not even for purely imaginary arguments:
```
>>> exp(9*I*pi/4)
 9⋅ⅈ⋅π
 ─────
   4
ℯ
>>> simplify(exp(9*I*pi/4))
 9⋅ⅈ⋅π
 ─────
   4
ℯ
>>> a = exp(9*I*pi/4) - exp(I*pi/4); a
   ⅈ⋅π    9⋅ⅈ⋅π
   ───    ─────
    4       4
- ℯ    + ℯ
>>> simplify(a)
            9⋅ⅈ⋅π
            ─────
  4 ____      4
- ╲╱ -1  + ℯ
>>> expand_complex(a)
0
```

Hints from issue discussion:

Commit message: Add tests for exp() periodicity handling

Patch to review:
diff --git a/sympy/functions/elementary/exponential.py b/sympy/functions/elementary/exponential.py
index 047e2d52ea08..3eb7567764c9 100644
--- a/sympy/functions/elementary/exponential.py
+++ b/sympy/functions/elementary/exponential.py
@@ -262,6 +262,12 @@ def eval(cls, arg):
                             return -S.ImaginaryUnit
                         elif ask(Q.odd(coeff + S.Half)):
                             return S.ImaginaryUnit
+                    elif coeff.is_Rational:
+                        ncoeff = coeff % 2 # restrict to [0, 2pi)
+                        if ncoeff > 1: # restrict to (-pi, pi]
+                            ncoeff -= 2
+                        if ncoeff != coeff:
+                            return cls(ncoeff*S.Pi*S.ImaginaryUnit)
 
             # Warning: code in risch.py will be very sensitive to changes
             # in this (see DifferentialExtension).
@@ -292,16 +298,21 @@ def eval(cls, arg):
         elif arg.is_Add:
             out = []
             add = []
+            argchanged = False
             for a in arg.args:
                 if a is S.One:
                     add.append(a)
                     continue
                 newa = cls(a)
                 if isinstance(newa, cls):
-                    add.append(a)
+                    if newa.args[0] != a:
+                        add.append(newa.args[0])
+                        argchanged = True
+                    else:
+                        add.append(a)
                 else:
                     out.append(newa)
-            if out:
+            if out or argchanged:
                 return Mul(*out)*cls(Add(*add), evaluate=False)
 
         elif isinstance(arg, MatrixBase):
diff --git a/sympy/functions/elementary/tests/test_exponential.py b/sympy/functions/elementary/tests/test_exponential.py
index 4a1fc7d60f3d..7a3a543d01d7 100644
--- a/sympy/functions/elementary/tests/test_exponential.py
+++ b/sympy/functions/elementary/tests/test_exponential.py
@@ -48,6 +48,18 @@ def test_exp_values():
     assert exp(oo, evaluate=False).is_finite is False
 
 
+def test_exp_period():
+    assert exp(9*I*pi/4) == exp(I*pi/4)
+    assert exp(46*I*pi/18) == exp(5*I*pi/9)
+    assert exp(25*I*pi/7) == exp(-3*I*pi/7)
+    assert exp(-19*I*pi/3) == exp(-I*pi/3)
+    assert exp(37*I*pi/8) - exp(-11*I*pi/8) == 0
+    assert exp(-5*I*pi/3) / exp(11*I*pi/5) * exp(148*I*pi/15) == 1
+
+    assert exp(2 - 17*I*pi/5) == exp(2 + 3*I*pi/5)
+    assert exp(log(3) + 29*I*pi/9) == 3 * exp(-7*I*pi/9)
+
+
 def test_exp_log():
     x = Symbol("x", real=True)
     assert log(exp(x)) == x


Code review comments:
diff_hunk: @@ -262,6 +262,12 @@ def eval(cls, arg):
                             return -S.ImaginaryUnit
                         elif ask(Q.odd(coeff + S.Half)):
                             return S.ImaginaryUnit
+                    elif coeff.is_Rational:
path: sympy/functions/elementary/exponential.py
text: @user1:
```suggestion
                    elif coeff.is_rational:
```

This will allow symbolic coefficients to pass.

@author:
Hi, thanks for looking through the changes! Wouldn't this proposed change entail that in case of a symbolic argument to exp(), another term (multiple of `2*pi*I`) would be added to the argument and hence it'd grow slightly longer and arguably more complex? Whereas for `is_Rational` the modulo-reduction would only be applied if it's an actual (albeit minor) simplification.
I may well be missing something though.

@user1:
I think what you have now will catch this case in the `(2*coeff).is_integer` block.