-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
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
Better eval
and is_zero
checking for a number of functions.
#17510
Conversation
✅ Hi, I am the SymPy bot (v149). 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.5. Note: This comment will be updated with the latest check if you edit the pull request. You need to reload the page to see it. Click here to see the pull request description that was parsed.
Update The release notes on the wiki have been updated. |
449e411
to
04e512a
Compare
sympy/core/function.py
Outdated
def _eval_is_zero(self): | ||
f = self.func(*self.args, evaluate=True) | ||
if f.func != self.func: | ||
return f.is_zero |
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'm not sure about this. I have generally approached this from the perspective that if the user doesn't want to evaluate then the assumptions system shouldn't do the evaluation. We should generally assume that the user had a good reason for not evaluating.
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 whole idea is really that I think all checks should be done as .is_zero
rather than == 0
. In the longer run, the functions can catch up to return better results.
Note that Add(1, -1, evaluate=False).is_zero
is currently True
.
Part of the motivation comes from #17454 and #17482 where the polynomial has a form of x**2 + x*expr
but in a later stage it turned out that expr
was 0. Hence, checking for == 0
or is S.Zero
is not really viable, but with a good is_zero
things would be easier.
Also, consider Sum(0, (x, M, N)). is_zero
, sin(Symbol('x', zero=True))
, #17414, and the Zen of SymPy: "Unevaluated is better than evaluated.". If one would like to know if the expression has evaluated to 0
or S.Zero
check against that. If one would like to know if the expression (for any reason) will evaluate or has evaluated to zero, check is_zero
. As the assumptions can figure out things that the numerical evaluation cannot, it makes sense to primarily use that when possible in most code.
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.
Somehow, it is a question if the numerical comparisons should fallback to the assumptions or the assumptions should fallback to the numerical comparisons.
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.
Hence, I think that e.g. sin
should have lines, somewhere, in eval
that goes like:
if arg.is_zero:
return S.Zero
as it right now doesn't benefit from the assumptions system in the evaluation. Maybe that is the way to solve/improve #17511 btw.
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.
So, basically, if you want to check that if an expression (unevaluated or not) is identical to S.Zero
, there are plenty of ways to do that without the assumption system.
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 whole idea is really that I think all checks should be done as
.is_zero
rather than== 0
Given that in a parallel thread you are talking about mechanically changing the code to things that are faster I should point out the main difference here which is that is_zero can take an arbitrary amount of time. It should generally be quicker than doit (where doit might result in something more obviously zero) but it can be a lot slower than == 0
.
In [17]: x = Symbol('x', positive=True)
In [18]: random_poly(x, 100, -10, 10) == 0
Out[18]: False
In [19]: random_poly(x, 100, -10, 10).is_zero # Try this a few times because it can be fast or slow
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'm not sure about this.
I agree. I think is_zero
is mainly there to give None/False values for expressions which are not identically zero. Add(x, -x, evaluate=False).is_zero
gives None and it should since x - x
might be nan
if x
is not finite. equals
should try to evaluate expressions, but assumptions should not. If the first check of foo.is_zero
is if foo is S.Zero: return True
then you can use is_zero
instead of is S.Zero
everywhere. But I would advise, with @oscarbenjamin , against trying to make the assumptions use evaluation.
Turns out that evaluating huge factorials isn't really feasible... So I'll have to rethink this a bit... |
This is what I meant when I said that the user probably has a good reason for using evaluate=False :). As a general principle I think that the assumptions system should lie underneath the evaluation system. Evaluation should use the assumptions system to make decisions but the assumptions system should not use evaluation to answer queries. |
If this is the case then how should a query about whether some complicated expression is positive be handled when |
e290d34
to
4433586
Compare
I can see the point of not using the evaluation system in
I think that in the code base one can see all types of ways to solve this and I think that one should be the preferred. Btw, this PR has now changed focus to implementing better
As you see, in this PR I (now) first check |
eval
and is_zero
checking for a number of functions.
Btw, I'll add tests and revert the |
Can you show an example? There are cases where a subexpression is created for example |
Search for Seems like
But the maybe most worrying is
Now, I'm not sure what the purpose of copy is, but I can imagine that if someone uses it, there may be unexpected consequences. |
This should be removed I think.
There shouldn't be any need for Basic.copy since Basic is immutable. Either way though it is difficult at that level to use an evaluate kwarg because many Basic subclasses don't support it #17280 #17372. |
Codecov Report
@@ Coverage Diff @@
## master #17510 +/- ##
============================================
- Coverage 74.76% 74.703% -0.058%
============================================
Files 634 634
Lines 165333 165530 +197
Branches 38857 38937 +80
============================================
+ Hits 123604 123656 +52
- Misses 36309 36391 +82
- Partials 5420 5483 +63 |
|
||
if fuzzy_not(k.is_zero): | ||
if x is S.Zero: | ||
if x is S.Zero or x.is_zero: |
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.
Is it beneficial to so if x is S.Zero or x.is_zero
rather than just if x.is_zero
? In the case that x is S.Zero
, both essentially boil down to an attribute lookup:
In [5]: x = S.Zero
In [6]: %timeit bool(x is S.Zero or x.is_zero)
212 ns ± 1.97 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [7]: %timeit bool(x.is_zero)
184 ns ± 5.24 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
In any other case the x is S.Zero
check will be False and we still end up calling x.is_zero
.
Even if this was faster I would be unsure about this change: it would be better to optimise S.Zero.is_zero
than pepper things like this around the codebase.
def _eval_is_zero(self): | ||
x = self.args[0] | ||
if len(self.args) == 1: | ||
k = S.Zero |
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.
As a general principle I think it would be better to use properties like x and k than to manipulate args directly. We should try to limit the number of places that depend on the exact layout of args.
@@ -1117,6 +1117,8 @@ def eval(cls, arg): | |||
return S.Zero | |||
elif arg is S.Zero: | |||
return S.One / (3**Rational(2, 3) * gamma(Rational(2, 3))) | |||
if arg.is_zero: | |||
return S.One / (3**Rational(2, 3) * gamma(Rational(2, 3))) |
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 the elif above isn't needed
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.
Same below
if a == 0: | ||
if m == 0 and b == 0: | ||
if a is S.Zero: | ||
if m is S.Zero and b is S.Zero: |
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 see why you were asking about sympification of arguments to eval
. I think it's fine to treat eval
as an internal routine so that sympification is the user's responsibility.
@@ -647,6 +664,10 @@ def eval(cls, x, y): | |||
if isinstance(y, erf2inv) and y.args[0] == x: | |||
return y.args[1] | |||
|
|||
if x.is_zero or y.is_zero or x.is_extended_real and x.is_infinite or \ | |||
y.is_extended_real and y.is_infinite: | |||
return erf(y) - erf(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 is a similar check a few lines above. Maybe this can replace that
@@ -846,6 +874,9 @@ def eval(cls, z): | |||
elif z == 2: | |||
return S.NegativeInfinity | |||
|
|||
if z.is_zero: | |||
return S.Infinity |
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.
We are also checking z is S.Zero
above. Maybe this can replace that
if x.is_zero: | ||
if y.is_zero: | ||
return S.Zero | ||
else: |
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 checks are also duplicated above.
@@ -1028,6 +1071,9 @@ def eval(cls, z): | |||
elif z is S.NegativeInfinity: | |||
return S.Zero | |||
|
|||
if z.is_zero: | |||
return S.NegativeInfinity |
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 also duplicated above
@@ -285,7 +284,7 @@ def eval(cls, a, x): | |||
# of both s and x), i.e. | |||
# lowergamma(s, exp(2*I*pi*n)*x) = exp(2*pi*I*n*a)*lowergamma(a, x) | |||
from sympy import unpolarify, I | |||
if x == 0: | |||
if x is S.Zero: |
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 this could be .is_zero
instead of also checking that below
@@ -676,16 +689,18 @@ def eval(cls, n, z): | |||
return S.Infinity | |||
else: | |||
return S.Zero | |||
if n.is_zero: | |||
return S.Infinity |
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 use this above instead of checking is S.Zero
as well
return S.EulerGamma | ||
|
||
if n.is_integer == False: | ||
return S.ComplexInfinity |
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 you can use is False
here.
return S.ComplexInfinity | ||
|
||
if n.is_zero and a in [None, 1]: | ||
return S.EulerGamma |
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'd use a tuple for [None, 1]
. I believe that in CPython it can be embedded as a "compile time constant" in the pyc so a new list isn't created each time the function is called.
This generally looks good although I left comments. There are lots of places where we now have something like: def eval(cls, arg):
if arg is S.Zero:
# Some stuff
# Some other stuff
if arg.is_zero:
# Same stuff as before I think it would be better to combine those. I presume that you do this because it will possibly be quicker but I don't think it will be because in most cases where another condition applies the |
Having looked at this again I want to clarify that my comments above are just subjective comments: I think the changes here are fine to merge. It does look like a lot of these changes are untested though. Codecov is showing 45% diff coverage which doesn't seem right but a quick scan of the diff shows some untested things like |
@oscargus are you still working on this? If not I think this should be merged. |
…appelf1 and hyper
f706a0f
to
0904b18
Compare
I will merge this soon if no one objects |
…appelf1 and hyper
References to other Issues or PRs
Fixes #17511
Brief description of what is fixed or changed
Many functions now use
is_zero
ineval
(typically towards the end ofeval
for performance reasons). This will lead to smaller expressions in general.Many functions now have a dedicated
_eval_is_zero
function.Also, added support for
evaluate
-keywords tohyper
andappellf1
.hyper
never evaluates, but I think it makes sense to support it anyway, if nothing else for theis_zero
andis_rational
-support.Other comments
Release Notes
hyper
andappellf1
supportsevaluate
keyword.is_zero
to evaluate which leads to smaller expressions.is_zero
check for many functions.beta
evaluates expressions if one of the arguments is1
.