-
-
Notifications
You must be signed in to change notification settings - Fork 5.1k
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
BUG: ENH: Check if guess for newton is already zero before checking derivative #8907
BUG: ENH: Check if guess for newton is already zero before checking derivative #8907
Conversation
scipy/optimize/zeros.py
Outdated
@@ -187,6 +187,9 @@ def newton(func, x0, fprime=None, args=(), tol=1.48e-8, maxiter=50, | |||
# Multiply by 1.0 to convert to floating point. We don't use float(x0) | |||
# so it still works if x0 is complex. | |||
p0 = 1.0 * x0 | |||
# check if initial guess is the solution | |||
if func(p0, *args) == 0: | |||
return _results_select(full_output, (p0, 0, 0, _ECONVERGED)) |
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 check outside the loop only handles the case of the starting point being a root. Its doesn't handle an intermediate iterate being a multiple root.
Move the test inside the loop, and move the evaluation of f
ahead of that of fprime
. Something like
for itr in range(maxiter):
# First evaluate f
fval = func(p0, *args)
funcalls += 1
# If fval is 0, a root has been found. Terminate.
if fval == 0:
return _results_select(full_output, (p0, funcalls, itr, _ECONVERGED))
# Now evaluate f'
fder = fprime(p0, *args)
funcalls += 1
# If fder is 0 and fval != 0, there is a problem. Warn/terminate
if fder == 0:
msg = "derivative was zero."
warnings.warn(msg, RuntimeWarning)
return _results_select(full_output, (p0, funcalls, itr + 1, _ECONVERR))
newton_step = fval / fder
...
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 agree, in that case shall I change the title of the PR to "check for convergence using abs of residual" or something like that?
And more importantly, what tolerance should we use to check for convergence in this way? I think a test for zero identically is too strict, perhaps something more like:
# If a root has been found within some tolerance, then terminate
if abs(fval) < tol:
return _results_select(full_output, (p0, funcalls, itr, _ECONVERGED))
Should we use the same tolerance as what's currently used for the Newton step or add a new argument for a separate one? Or should we just check if the guess is identically 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.
Testing for "exactly 0" fixes the situation of landing in a multiple root.
Testing for "approximately 0" would change existing results. It also isn't scale-invariant (replacing f
with const * f
leads to different outcomes when using "approximately 0".).
So "exactly 0" is the right criteria to apply.
|
* update test to check not just an initial guess at the root but also that if the root is found by checking the residual that it does not raise a RuntimeWarning and that the convergence flag is set * remove test of initial guess before individual methods and iteration loop so that we can check every iteration, don't worry about secant for now * evaluate fval before fder so we can check if it's zero or relative close, use same default residual convergence criteria as the other Zeros (brent, bisect, ridder) * return if residual shows convergence and don't increment iteration count, just function calls
* also change zero residual test to eps too
* use xtol and rtol from zeros
So @pvanmulbregt, is your review affirmative now? @rgommers I believe this fixes the bug #8904 and IMO is an improvement to the existing SciPy NR implementation. Let me know if you have any other comments. Thanks! |
scipy/optimize/tests/test_zeros.py
Outdated
@@ -169,3 +169,36 @@ def f_2(x, *a): | |||
y = zeros.newton(f, z, args=coeffs, fprime=f_1, fprime2=f_2, tol=1e-6) | |||
# (-0.75000000000000078+1.1989578808281789j) | |||
assert_allclose(f(y, *coeffs), 0, atol=1e-6) | |||
|
|||
|
|||
@pytest.mark.filterwarnings("error") |
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 line can be removed, this is the default behavior (see pytest.ini
line 7 in the repo root).
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.
Hi @rgommers, even though the error
warning filter is set by default, it didn't seem to work when I use python runtest.py -t scipy/optimize/tests/test_zeros.py
and more importantly I feel that it's more explicit to leave the decorator even if it is redundant.
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 want to control this globally. For one, we're switching this behavior between development versions and releases (we do want errors for the former, not for the latter).
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.
Okay
scipy/optimize/tests/test_zeros.py
Outdated
|
||
# a function that has a zero derivative at it's root | ||
def f_zeroder_root(x): | ||
return x ** 3 - x ** 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.
formatting: there should be no spaces around **
(here and below)
scipy/optimize/tests/test_zeros.py
Outdated
r = zeros.newton(f_zeroder_root, x0=0, fprime=fder) | ||
assert_allclose(r, 0, atol=zeros._xtol, rtol=zeros._rtol) | ||
r = zeros.newton(f_zeroder_root, x0=0, fprime=fder, | ||
fprime2=lambda x: 6 * x - 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.
formatting nitpick: 6*x - 2
Thanks @mikofski. I've made a few minor comments, will be happy to merge once those are addressed. |
* conforming to http://pep8.org/#other-recommendations which suggests separating operations with lower priority with spaces
Thanks for your comments @rgommers - hopefully all good now. |
Let's give this a go. Thanks @mikofski! |
RuntimeWarning
is raised for case wherefval(p0)
is already zero wherep0
is the initial guessnewton
iffval(p0)
is already zero, before iterating for any method or evaluating derivativesfprime
(orfprime2
) if given, and returns_ECONVERGED
instead of raising aRuntimeWarning
scipy/optimize/tests/test_zeros.py::TestBasic::test_deriv_zero_warning
since these two are in opposition.