-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Enable automatic rewrite for cot, sec, csc, and related inverse and hyperbolic functions #22993
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
Conversation
✅ Hi, I am the SymPy bot (v167). I'm here to help you write a release notes entry. Please read the guide on how to write release notes. Your release notes are in good order. Here is what the release notes will look like:
This will be added to https://github.com/sympy/sympy/wiki/Release-Notes-for-1.11. Click here to see the pull request description that was parsed.
Update The release notes on the wiki have been updated. |
@@ -69,6 +69,7 @@ class CodePrinter(StrPrinter): | |||
# may be supported | |||
# function_to_rewrite : (function_to_rewrite_to, iterable_with_other_functions_required) | |||
_rewriteable_functions = { | |||
'cot': ('tan', []), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Presumably this should also be done for sec, csc, coth, sech, csch as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And all the inverses asec, acsc etc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good observation! I'll add those as well (but maybe not tests for all combinations etc...).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is worth testing all combinations. Some of them won't work and some of the rewrite rules seem to be wrong or broken e.g.:
In [2]: sech(x).rewrite(cosh)
Out[2]: sech(x)
In [3]: sech(x).rewrite(sinh)
Out[3]:
1
───────
cosh(x)
In [4]: csch(x).rewrite(cosh)
Out[4]:
1
───────
sinh(x)
In [5]: csch(x).rewrite(sinh)
Out[5]: csch(x)
Also if any rewrite rule is missing then lambdify
goes into infinite recursion so that would happen for asech
here:
In [9]: asec(x).rewrite(acos)
Out[9]:
⎛1⎞
acos⎜─⎟
⎝x⎠
In [10]: asech(x).rewrite(acosh)
Out[10]: asech(x)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK! I'll update the current one then. I found that the a*h
could not be rewritten, but not the others.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some are simply not supported, while others are getting rewritten as part of automatic evaluation. For example:
In [3]: sech(x).rewrite(sinh)
Out[3]:
1
───────
cosh(x)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be a sech._eval_rewrite_as_cosh
method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see you've added it now
Benchmark results from GitHub Actions Lower numbers are good, higher numbers are bad. A ratio less than 1 Significantly changed benchmark results (PR vs master) before after ratio
[bb7275c1] [8ac511a8]
+ 30.2±0.09μs 49.2±0.04μs 1.63 core.expand.TimeExpand.time_expand_log
Significantly changed benchmark results (master vs previous release) before after ratio
[907895ac] [bb7275c1]
- 260±1ms 157±0.9ms 0.60 large_exprs.TimeLargeExpressionOperations.time_subs
- 17.7±0.04ms 11.8±0.03ms 0.67 matrices.TimeMatrixExpression.time_MatAdd_doit
- 270±0.7μs 130±0.1μs 0.48 matrices.TimeMatrixExpression.time_MatMul
- 17.0±0.02ms 9.05±0.04ms 0.53 matrices.TimeMatrixExpression.time_MatMul_doit
- 4.99±0s 371±2ms 0.07 polygon.PolygonArbitraryPoint.time_bench01
+ 4.03±0.02ms 6.75±0.02ms 1.67 solve.TimeMatrixOperations.time_det(4, 2)
+ 4.03±0.02ms 6.78±0.01ms 1.68 solve.TimeMatrixOperations.time_det_bareiss(4, 2)
+ 44.9±0.2ms 79.9±0.5ms 1.78 solve.TimeMatrixSolvePyDySlow.time_linsolve(1)
+ 45.4±0.5ms 79.3±0.4ms 1.75 solve.TimeMatrixSolvePyDySlow.time_solve(1)
Full benchmark results can be found as artifacts in GitHub Actions |
# Standard math | ||
F = lambdify([a, t], expr) | ||
|
||
assert abs(expr.subs(point) - F(*point.values())) <= 1e-10 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there any choices of point
that would deviate substantially between cot
and tan**-1
due to numerical implementation? I'd argue that this test would imply to users that this accuracy holds for any point, but that might not actually be the case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't have thought that there would be a substantial deviation. Rounding error from division is generally minimal.
For inverse functions there could be issues with branch cuts and signed zeros (#21751)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess one can probably find cases where it is better to evaluate the original function as opposed to a rewrite, but to some extent it is choice where to get the errors... And in most cases, I assume that the rewrites are OK enough and not worse that just evaluating a large symbolic argument to cot
like sqrt(sqrt(2) + sin(pi/7))
or so, which we do not really promise are accurate enough.
2f368ce
to
6313eda
Compare
6313eda
to
1104bf2
Compare
This currently is enough (and a bit more) to support code generation for all the (a)cot/sec/csc(h) variants, but for each, it is in general possible to rewrite it to almost any other form (including the sin/cos/tan ones), so typically 12 variants (plus log and meijerg etc) for each (> 144 in total). Hence, this is quite a bit of work. I am currently happy for now. While it is of course possible to add all, I am not sure if it is worth it. Some of them are automatically evaluated back and others have quite massive expressions for the general case. I cannot even imagine adding tests for all the cases. Maybe the way forward is to add an issue for adding the remaining one. It is pretty straightforward, although tedious, and it is easy to show by an example what code and test to add (although, evaluation can make it a bit challenging to figure out a good test case). |
assert asinh(x).rewrite(atanh) == atanh(x/sqrt(1 + x**2)) | ||
assert asinh(x).rewrite(asin) == asinh(x) | ||
assert asinh(x*(1 + I)).rewrite(asin) == -I*asin(I*x*(1+I)) | ||
assert asinh(x).rewrite(acos) == I*(-I*asinh(x) + pi/2) - I*pi/2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this odd: no acos
, just a rewriting in terms of original function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And when expanded, it is just the original.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is automatic evaluation:
In [2]: acos(I * x)
Out[2]:
π
-ⅈ⋅asinh(x) + ─
2
@@ -637,6 +641,8 @@ def test_acosh(): | |||
def test_acosh_rewrite(): | |||
x = Symbol('x') | |||
assert acosh(x).rewrite(log) == log(x + sqrt(x - 1)*sqrt(x + 1)) | |||
assert acosh(x).rewrite(asinh) == Abs(asinh(sqrt(x**2 - 1))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At x=0
these are not the same.
@@ -637,6 +641,8 @@ def test_acosh(): | |||
def test_acosh_rewrite(): | |||
x = Symbol('x') | |||
assert acosh(x).rewrite(log) == log(x + sqrt(x - 1)*sqrt(x + 1)) | |||
assert acosh(x).rewrite(asinh) == Abs(asinh(sqrt(x**2 - 1))) | |||
assert acosh(x).rewrite(atanh) == Abs(atanh(sqrt(x**2 - 1)/x)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are not the same at 1.
@@ -721,6 +727,9 @@ def test_asech_series(): | |||
def test_asech_rewrite(): | |||
x = Symbol('x') | |||
assert asech(x).rewrite(log) == log(1/x + sqrt(1/x - 1) * sqrt(1/x + 1)) | |||
assert asech(x).rewrite(acosh) == acosh(1/x) | |||
assert asech(x).rewrite(asinh) == Abs(asinh(sqrt(1/x**2 - 1))) | |||
assert asech(x).rewrite(atanh) == Abs(atanh(x*sqrt(1/x**2 - 1))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Issues with branch, again, when x > 1
-- do these need to be Piecewise?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe some of these rewrites that aren't needed for lambdify should be removed so that the main issue can be fixed.
If a few of the rewrite methods are removed then I think this is good to go. |
1104bf2
to
cdeb436
Compare
I've updated the rewrites. In some cases I used a Piecewise, for others, I went for the most general expressions available. I'm a bit confused about
as Sympy will evaluate |
I tested all combinations like this: from sympy import *
x = symbols('x')
funcs = [cot , csc , sec , acot , acsc , asec,
coth, csch, sech, acoth, acsch, asech]
points = [
0, 1, -1, I, -I, 1+I, 1-I, -1+I, -1-I,
]
for f1 in funcs:
for f2 in funcs:
expr1 = f1(x)
expr2 = expr1.rewrite(f2)
for p in points:
if expr1.subs(x, p).n() != expr2.subs(x, p).n():
print()
print(f1, f2, p)
print(expr1, ' --> ', expr2)
print(expr1.subs(x, p), expr2.subs(x, p))
print(expr1.subs(x, p).n(5), expr2.subs(x, p).n(5)) There are a bunch of cases that don't work out e.g.: In [32]: sec(0)
Out[32]: 1
In [33]: sec(x).rewrite(cot).subs(x, 0)
Out[33]: nan The full output is csc cot 0
csc(x) --> (cot(x/2)**2 + 1)/(2*cot(x/2))
zoo nan
zoo nan
sec cot 0
sec(x) --> (cot(x/2)**2 + 1)/(cot(x/2)**2 - 1)
1 nan
1.0000 nan
acot acsc 0
acot(x) --> x*(-acsc(sqrt((x**2 + 1)/x**2)) + pi/2)*sqrt(x**(-2))
pi/2 nan
1.5708 nan
acot acsc I
acot(x) --> x*(-acsc(sqrt((x**2 + 1)/x**2)) + pi/2)*sqrt(x**(-2))
-oo*I zoo
-oo*I zoo
acot acsc -I
acot(x) --> x*(-acsc(sqrt((x**2 + 1)/x**2)) + pi/2)*sqrt(x**(-2))
oo*I zoo
oo*I zoo
acot acsc -1 + I
acot(x) --> x*(-acsc(sqrt((x**2 + 1)/x**2)) + pi/2)*sqrt(x**(-2))
-acot(1 - I) (-1 - I)*(-1 + I)*(pi/2 - acsc(sqrt((1 + (-1 + I)**2)/(-1 + I)**2)))/2
-0.55357 - 0.40236*I 0.55357 + 0.40236*I
acot acsc -1 - I
acot(x) --> x*(-acsc(sqrt((x**2 + 1)/x**2)) + pi/2)*sqrt(x**(-2))
-acot(1 + I) (-1 - I)*(-1 + I)*(pi/2 - acsc(sqrt((1 + (-1 - I)**2)/(-1 - I)**2)))/2
-0.55357 + 0.40236*I 0.55357 - 0.40236*I
acot asec 0
acot(x) --> x*sqrt(x**(-2))*asec(sqrt((x**2 + 1)/x**2))
pi/2 nan
1.5708 nan
acot asec I
acot(x) --> x*sqrt(x**(-2))*asec(sqrt((x**2 + 1)/x**2))
-oo*I zoo
-oo*I zoo
acot asec -I
acot(x) --> x*sqrt(x**(-2))*asec(sqrt((x**2 + 1)/x**2))
oo*I zoo
oo*I zoo
acot asec -1 + I
acot(x) --> x*sqrt(x**(-2))*asec(sqrt((x**2 + 1)/x**2))
-acot(1 - I) (-1 - I)*(-1 + I)*asec(sqrt((1 + (-1 + I)**2)/(-1 + I)**2))/2
-0.55357 - 0.40236*I 0.55357 + 0.40236*I
acot asec -1 - I
acot(x) --> x*sqrt(x**(-2))*asec(sqrt((x**2 + 1)/x**2))
-acot(1 + I) (-1 - I)*(-1 + I)*asec(sqrt((1 + (-1 - I)**2)/(-1 - I)**2))/2
-0.55357 + 0.40236*I 0.55357 - 0.40236*I
acsc acot 0
acsc(x) --> (-acot(1/sqrt(x**2 - 1)) + pi/2)*sqrt(x**2)/x
zoo nan
zoo nan
acsc acot I
acsc(x) --> (-acot(1/sqrt(x**2 - 1)) + pi/2)*sqrt(x**2)/x
acsc(I) pi/2 - I*acoth(sqrt(2)/2)
-0.88137*I 0.e-10 - 0.88137*I
acsc acot -I
acsc(x) --> (-acot(1/sqrt(x**2 - 1)) + pi/2)*sqrt(x**2)/x
-acsc(I) -pi/2 + I*acoth(sqrt(2)/2)
0.88137*I -0.e-10 + 0.88137*I
acsc acot -1 + I
acsc(x) --> (-acot(1/sqrt(x**2 - 1)) + pi/2)*sqrt(x**2)/x
-acsc(1 - I) (-1 - I)*(-1 + I)*(pi/2 - acot(1/sqrt(-1 + (-1 + I)**2)))/2
-0.45228 - 0.53064*I 0.45228 + 0.53064*I
acsc acot -1 - I
acsc(x) --> (-acot(1/sqrt(x**2 - 1)) + pi/2)*sqrt(x**2)/x
-acsc(1 + I) (-1 - I)*(-1 + I)*(pi/2 - acot(1/sqrt(-1 + (-1 - I)**2)))/2
-0.45228 + 0.53064*I 0.45228 - 0.53064*I
acsc asec I
acsc(x) --> -asec(x) + pi/2
acsc(I) pi/2 - asec(I)
-0.88137*I 0.e-10 - 0.88137*I
acsc asec -I
acsc(x) --> -asec(x) + pi/2
-acsc(I) pi/2 - asec(-I)
0.88137*I 0.e-10 + 0.88137*I
asec acot 0
asec(x) --> (2*acot(x - sqrt(x**2 - 1)) - pi/2)*sqrt(x**2)/x
zoo nan
zoo nan
asec acot -1 + I
asec(x) --> (2*acot(x - sqrt(x**2 - 1)) - pi/2)*sqrt(x**2)/x
asec(-1 + I) (-1 - I)*(-1 + I)*(-pi/2 - 2*acot(1 + sqrt(-1 + (-1 + I)**2) - I))/2
2.0231 + 0.53064*I -2.0231 - 0.53064*I
asec acot -1 - I
asec(x) --> (2*acot(x - sqrt(x**2 - 1)) - pi/2)*sqrt(x**2)/x
asec(-1 - I) (-1 - I)*(-1 + I)*(-pi/2 - 2*acot(1 + I + sqrt(-1 + (-1 - I)**2)))/2
2.0231 - 0.53064*I -2.0231 + 0.53064*I
csch coth 0
csch(x) --> (coth(x/2)**2 - 1)/(2*coth(x/2))
zoo nan
zoo nan
sech coth 0
sech(x) --> (coth(x/2)**2 - 1)/(coth(x/2)**2 + 1)
1 nan
1.0000 nan
asech acsch 0
asech(x) --> sqrt(-1 + 1/x)*(-I*acsch(I*x) + pi/2)/sqrt(1 - 1/x)
oo nan
oo nan
asech acsch 1
asech(x) --> sqrt(-1 + 1/x)*(-I*acsch(I*x) + pi/2)/sqrt(1 - 1/x)
0 nan
0 nan I don't think any of these are rewrites that are needed for lambdify though. Many of the examples fail on master as well. |
Well, I have only added rewrite equations for the hyperbolic functions, so cannot really say anything about the trig-rewrites. (Some of those seems to be fundamentally incorrect.) For the hyperbolic functions, I assume that the equations (given at Wolframs function site) do assume certain things, such as 1/0 = oo (rather than NaN), so the limit will be correct but as Sympy has made the choice to do that, the results will be a bit messed up. But it is of course possible to return a Piecewise for those cases. |
There are so many rewrites that are incorrect in the existing code that it is not really possible to fix it in this PR. I mean, not even this holds: I'll push the latest version that fixes the introduced issues pointed out in your test and also introduces some support for +/-I*oo. |
cdeb436
to
622b997
Compare
Here is an extended test:
|
I wasn't suggesting that they all needed to be fixed here. |
Okay, there's a lot of fails (on master)... |
I think all failing tests of your original test are not caused by my addition, even the ones for hyperbolic functions. Some of the basic hyperbolic rewrites are just using the reciprocal of the argument or function, which then of course fails for 0 inputs. |
Yes, most of the hyperbolic I would say. Also, it would reduce the risk of actually not typing the correct Piecewise, I think (at least) one of them is not fully correct yet. I'm slowly trying to get rid of the last bugs and converting the previous rewrites to correct ones (evaluating with 0 or pi kills quite a few of the basic rewrites, like |
Ideally we should avoid using Piecewise wherever possible (it is very awkward both symbolically and numerically e.g. in lambdify). So if we are using a Piecewise in place of a potential non-Piecewise expression just to work around some bug then it would be good to have a comment clearly showing what the non-Piecewise version of the formula would be and why it isn't being used. I want to have a go at fixing the sqrt but but I'm not sure how easy it is to do or how disruptive it might potentially be (maybe this is too late in the release cycle). If we had more time then it would make sense to fix that bug first and then get all the formulae here perfected with minimial use of Piecewise. If we want to get this PR in for 1.11 though then it's probably best not to wait for that. We should make sure though that we can easily fix all the formulae later if/when the bug does get fixed. |
Actually it seems to be quite easy to do (#23653) |
Now that #23653 is merged it shouldn't be necessary to work around the sqrt bug using unnecessary Piecewise. |
It would be good to get this in for 1.11 |
def _eval_rewrite_as_asinh(self, x, **kwargs): | ||
return Piecewise((S.Zero, Eq(x, 1)), | ||
(sqrt(x - 1)/sqrt(1 - x)*(pi/2 + | ||
S.ImaginaryUnit*asinh(S.ImaginaryUnit*x)), True)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is acosh(x).rewrite(asinh)
. I think it can be done without Piecewise:
In [25]: 2*asinh(sqrt((x - 1)/2))
Out[25]:
⎛ _______⎞
⎜ ╱ x 1 ⎟
2⋅asinh⎜ ╱ ─ - ─ ⎟
⎝╲╱ 2 2 ⎠
def _eval_rewrite_as_asinh(self, x, **kwargs): | ||
from sympy.functions.elementary.complexes import arg, Abs | ||
return Piecewise((S.Half*asinh(2*x/(1 - x**2)) - pi*x/2*sqrt(-1/x**2), | ||
Abs(x) < 1), | ||
(-S.Half*asinh(2*x/(1 - x**2)), Abs(x) > 1), | ||
(S.Infinity*arg(x), True)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is acoth(x).rewrite(asinh)
. The final case in the Piecewise seems incorrect for x=I
:
In [1]: acoth(x).subs(x, I)
Out[1]:
-ⅈ⋅π
─────
4
In [2]: acoth(x).rewrite(asinh).subs(x, I)
Out[2]: ∞
This is the failing test:
It has been changed to
which is correct for |
I'll try to get some time to retype the general expressions. |
c9ca06c
to
cbb767c
Compare
This is now updated. (But will be updated again in a few minutes as I realized that some Piecewises that should not be there was left.) The possible controversial thing is that e.g |
2032945
to
94ab406
Compare
sorry I missed this; glad to see it was fixed |
94ab406
to
74c43ce
Compare
…yperbolic functions
Okay, let's get this in. There are some followups to be done from the discussion above but we can defer those to a future issue. |
There is some mistake due lambdifing expressions with |
I am not sure it is caused by this. Entering |
Ahh, I see the problem now. However, it is not clear how to get around this (at least from the changes made in this PR). Maybe one can return an unevaluated result in this case though. |
It looks correct to me:
|
References to other Issues or PRs
Closes #22992
Brief description of what is fixed or changed
cot
is automatically rewritten totan
to support lambdifying.Other comments
Release Notes
cot
,sec
,csc
and related inverse and hyperbolic functions are automatically rewritten if they cannot be printed. Enableslambdify
for these expressions.