Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix is_real for trigonometric and hyperbolic functions #13678

Merged
merged 2 commits into from
Dec 16, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
47 changes: 43 additions & 4 deletions sympy/functions/elementary/hyperbolic.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,16 @@ def _eval_as_leading_term(self, x):
return self.func(arg)

def _eval_is_real(self):
return self.args[0].is_real
if self.args[0].is_real:
return True

def _eval_is_positive(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In your branch:

>>> sinh(1 + I).is_positive
None

>>> sinh(1 + I).evalf().is_positive
False

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably want sinh(1 + I).is_real to be False instead of None (making is_positive False too), but I'm not sure it's worth it.

It would be easy to call self.as_real_imag() and then .is_zero, but even if we pass deep=False nested evaluation will lead to runtime exponential in the depth of the expression: 8 seconds for building (sinh^9)(1 + I) (^ in the sense of nested application) and 24 seconds for .is_real, instead of 3 milliseconds total with the current PR.

Slowing down to return True instead of None could have been worthwhile because proving something is real often leads to simplifications, but is it worth the slowdown to return False instead of None?

Food for thought: in the current implementation, (a+b).is_real only returns True when both a and b are real.

>>> (exp(2*I*pi/3) + exp(4*I*pi/3)).is_real
None

if self.args[0].is_real:
return self.args[0].is_positive

def _eval_is_negative(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly for this I guess.

if self.args[0].is_real:
return self.args[0].is_negative

def _eval_is_finite(self):
arg = self.args[0]
Expand Down Expand Up @@ -379,8 +388,9 @@ def _eval_as_leading_term(self, x):
else:
return self.func(arg)

def _eval_is_real(self):
return self.args[0].is_real
def _eval_is_positive(self):
if self.args[0].is_real:
return True

def _eval_is_finite(self):
arg = self.args[0]
Expand Down Expand Up @@ -526,7 +536,16 @@ def _eval_as_leading_term(self, x):
return self.func(arg)

def _eval_is_real(self):
return self.args[0].is_real
if self.args[0].is_real:
return True

def _eval_is_positive(self):
if self.args[0].is_real:
return self.args[0].is_positive

def _eval_is_negative(self):
if self.args[0].is_real:
return self.args[0].is_negative

def _eval_is_finite(self):
arg = self.args[0]
Expand Down Expand Up @@ -657,6 +676,14 @@ def _eval_rewrite_as_cosh(self, arg):
def _eval_rewrite_as_tanh(self, arg):
return 1/tanh(arg)

def _eval_is_positive(self):
if self.args[0].is_real:
return self.args[0].is_positive

def _eval_is_negative(self):
if self.args[0].is_real:
return self.args[0].is_negative

def _eval_as_leading_term(self, x):
from sympy import Order
arg = self.args[0].as_leading_term(x)
Expand Down Expand Up @@ -784,6 +811,14 @@ def taylor_term(n, x, *previous_terms):
def _eval_rewrite_as_cosh(self, arg):
return S.ImaginaryUnit / cosh(arg + S.ImaginaryUnit * S.Pi / 2)

def _eval_is_positive(self):
if self.args[0].is_real:
return self.args[0].is_positive

def _eval_is_negative(self):
if self.args[0].is_real:
return self.args[0].is_negative

def _sage_(self):
import sage.all as sage
return sage.csch(self.args[0]._sage_())
Expand Down Expand Up @@ -823,6 +858,10 @@ def taylor_term(n, x, *previous_terms):
def _eval_rewrite_as_sinh(self, arg):
return S.ImaginaryUnit / sinh(arg + S.ImaginaryUnit * S.Pi /2)

def _eval_is_positive(self):
if self.args[0].is_real:
return True

def _sage_(self):
import sage.all as sage
return sage.sech(self.args[0]._sage_())
Expand Down
25 changes: 25 additions & 0 deletions sympy/functions/elementary/tests/test_hyperbolic.py
Original file line number Diff line number Diff line change
Expand Up @@ -937,3 +937,28 @@ def test_cosh_expansion():
assert cosh(2*x).expand(trig=True) == cosh(x)**2 + sinh(x)**2
assert cosh(3*x).expand(trig=True).expand() == \
3*sinh(x)**2*cosh(x) + cosh(x)**3

def test_real_assumptions():
z = Symbol('z', real=False)
assert sinh(z).is_real is None
assert cosh(z).is_real is None
assert tanh(z).is_real is None
assert sech(z).is_real is None
assert csch(z).is_real is None
assert coth(z).is_real is None

def test_sign_assumptions():
p = Symbol('p', positive=True)
n = Symbol('n', negative=True)
assert sinh(n).is_negative is True
assert sinh(p).is_positive is True
assert cosh(n).is_positive is True
assert cosh(p).is_positive is True
assert tanh(n).is_negative is True
assert tanh(p).is_positive is True
assert csch(n).is_negative is True
assert csch(p).is_positive is True
assert sech(n).is_positive is True
assert sech(p).is_positive is True
assert coth(n).is_negative is True
assert coth(p).is_positive is True
23 changes: 22 additions & 1 deletion sympy/functions/elementary/tests/test_trigonometric.py
Original file line number Diff line number Diff line change
Expand Up @@ -940,7 +940,7 @@ def test_acot():
assert acot(I*pi) == -I*acoth(pi)
assert acot(-2*I) == I*acoth(2)
assert acot(x).is_positive is None
assert acot(r).is_positive is True
assert acot(n).is_positive is False
assert acot(p).is_positive is True
assert acot(I).is_positive is False

Expand Down Expand Up @@ -1541,3 +1541,24 @@ def test_issue_11864():
F = Piecewise((1, Eq(2*pi*k, 0)), (sin(pi*k)/(pi*k), True))
soln = Piecewise((1, Eq(2*pi*k, 0)), (sinc(pi*k), True))
assert F.rewrite(sinc) == soln

def test_real_assumptions():
z = Symbol('z', real=False)
assert sin(z).is_real is None
assert cos(z).is_real is None
assert tan(z).is_real is False
assert sec(z).is_real is None
assert csc(z).is_real is None
assert cot(z).is_real is False
assert asin(p).is_real is None
assert asin(n).is_real is None
assert asec(p).is_real is None
assert asec(n).is_real is None
assert acos(p).is_real is None
assert acos(n).is_real is None
assert acsc(p).is_real is None
assert acsc(n).is_real is None
assert atan(p).is_positive is True
assert atan(n).is_negative is True
assert acot(p).is_positive is True
assert acot(n).is_negative is True
31 changes: 19 additions & 12 deletions sympy/functions/elementary/trigonometric.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from sympy.core.numbers import igcdex, Rational, pi
from sympy.core.singleton import S
from sympy.core.symbol import Symbol, Wild
from sympy.core.logic import fuzzy_not
from sympy.core.logic import fuzzy_not, fuzzy_or
from sympy.functions.combinatorial.factorials import factorial, RisingFactorial
from sympy.functions.elementary.miscellaneous import sqrt, Min, Max
from sympy.functions.elementary.exponential import log, exp
Expand Down Expand Up @@ -460,7 +460,8 @@ def _eval_as_leading_term(self, x):
return self.func(arg)

def _eval_is_real(self):
return self.args[0].is_real
if self.args[0].is_real:
return True

def _eval_is_finite(self):
arg = self.args[0]
Expand Down Expand Up @@ -879,7 +880,8 @@ def _eval_as_leading_term(self, x):
return self.func(arg)

def _eval_is_real(self):
return self.args[0].is_real
if self.args[0].is_real:
return True

def _eval_is_finite(self):
arg = self.args[0]
Expand Down Expand Up @@ -1887,10 +1889,10 @@ def _eval_is_rational(self):
return s.is_rational

def _eval_is_positive(self):
if self.args[0].is_positive:
return (self.args[0] - 1).is_negative
if self.args[0].is_negative:
return not (self.args[0] + 1).is_positive
return self._eval_is_real() and self.args[0].is_positive

def _eval_is_negative(self):
return self._eval_is_real() and self.args[0].is_negative

@classmethod
def eval(cls, arg):
Expand Down Expand Up @@ -2048,10 +2050,6 @@ def _eval_is_rational(self):
else:
return s.is_rational

def _eval_is_positive(self):
x = self.args[0]
return (1 - abs(x)).is_nonnegative

@classmethod
def eval(cls, arg):
if arg.is_Number:
Expand Down Expand Up @@ -2117,6 +2115,9 @@ def _eval_is_real(self):
x = self.args[0]
return x.is_real and (1 - abs(x)).is_nonnegative

def _eval_is_nonnegative(self):
return self._eval_is_real()

def _eval_nseries(self, x, n, logx):
return self._eval_rewrite_as_log(self.args[0])._eval_nseries(x, n, logx)

Expand Down Expand Up @@ -2344,6 +2345,12 @@ def _eval_is_rational(self):
return s.is_rational

def _eval_is_positive(self):
return self.args[0].is_nonnegative

def _eval_is_negative(self):
return self.args[0].is_negative

def _eval_is_real(self):
return self.args[0].is_real

@classmethod
Expand Down Expand Up @@ -2542,7 +2549,7 @@ def _eval_is_real(self):
x = self.args[0]
if x.is_real is False:
return False
return (x - 1).is_nonnegative or (-x - 1).is_nonnegative
return fuzzy_or(((x - 1).is_nonnegative, (-x - 1).is_nonnegative))

def _eval_rewrite_as_log(self, arg):
return S.Pi/2 + S.ImaginaryUnit*log(S.ImaginaryUnit/arg + sqrt(1 - 1/arg**2))
Expand Down