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

ENH: vectorize scalar zero-search-functions #8357

Merged
merged 57 commits into from Jun 25, 2018
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
c7c5cb8
fixes #8354 and addresses #7242
mikofski Feb 2, 2018
98672d0
remove extra whitespace
mikofski Feb 2, 2018
39581f4
convert all to ndarray using asarray()
mikofski Feb 3, 2018
f24a559
add test for halley's with arrays
mikofski Feb 3, 2018
70edc74
remove commented code for legacy halley's parabolic variant
mikofski Feb 3, 2018
5b90676
allow arrays in newton for secant
mikofski Feb 3, 2018
44bf2eb
use secant test case without runtime warning
mikofski Feb 4, 2018
fcca324
don't test sqrt(16) with x0=4
mikofski Feb 4, 2018
5f181cb
fix sqrt(15) and sqrt(16) values in assertion
mikofski Feb 4, 2018
cef5de3
fix bugs x0 might be seq, div by zero not where tol reached
mikofski Feb 4, 2018
c4d6c9f
Merge branch 'master' of github.com:scipy/scipy into vectorize_newton
mikofski Feb 25, 2018
fe3f1cd
import numpy as np
mikofski Feb 25, 2018
8b7d30e
avoid domain specific function names and variabl
mikofski Feb 25, 2018
2f2b3df
Merge branch 'master' into vectorize_newton
mikofski Mar 10, 2018
9831250
ENH: combine rtol and xtol, set TOL constant once
mikofski Mar 10, 2018
395b046
check if initial guess isscalar, only run vectorized when false
mikofski Mar 10, 2018
0b8fe21
update tests newton array to use x0 array
mikofski Mar 15, 2018
25f2f14
WIP: ENH: continue iterating, handle halting conditions
mikofski Mar 17, 2018
4f0bb08
Merge branch 'master' into vectorize_newton
mikofski Mar 21, 2018
397da36
TST: add benchmarks for array_newton and newton looped
mikofski Mar 21, 2018
0866363
TST: WIP: ENH: fix array newton benchmarks
mikofski Mar 23, 2018
ee034e1
Merge branch 'master' of github.com:scipy/scipy into vectorize_newton
mikofski Mar 23, 2018
3e5d3c9
incorporate changes by Jaime Fernandez
mikofski Mar 23, 2018
e0cdc03
apply Jaime's recommendations to Secant
mikofski Mar 24, 2018
5c87dcb
suppress RuntimeWarning for zero-der elements of array newton
mikofski Mar 24, 2018
ae9e627
DOC: ENH: WIP: update docstring
mikofski Mar 24, 2018
d219ded
ENH: do not suppress RuntimeWarning
mikofski Mar 24, 2018
1155126
WIP: ENC: do not use kwargs as function argument
mikofski Mar 24, 2018
5d3de87
WIP: ENH: x0 to float first, use inplace assign, stop div-by-zero warn
mikofski Mar 25, 2018
acbc75a
WIP: ENH: fix convert x0 to float even if complex
mikofski Mar 25, 2018
5873bb9
WIP: ENH: fix don't index failures, don't reset secant zero-der guess
mikofski Mar 25, 2018
8953401
WIP: ENH: return named tuple
mikofski Mar 25, 2018
5166a7f
ENH: use modern str.format(), add more tests
mikofski Mar 25, 2018
ffb15bd
ENC: replace ~zero_der with nz_der
mikofski Mar 25, 2018
4f5ac8a
ENH: fix test_complex_halley for array
mikofski Mar 25, 2018
cfd7571
Merge branch 'master' into vectorize_newton
mikofski Apr 9, 2018
4373615
BUG: fix secant active_zero_der
mikofski Apr 9, 2018
5fa936f
DOC: update docstring if flag is true, then output is namedtuple
mikofski Apr 9, 2018
cd415e7
ENH: make failures same size as roots and zero_der
mikofski Apr 9, 2018
95290fb
Merge branch 'master' into vectorize_newton
mikofski Apr 25, 2018
dc7250f
Merge upstream 'master' into vectorize_newton
mikofski Apr 25, 2018
043ba2e
Merge #8803 from master into vectorize_newton
mikofski May 17, 2018
2792162
Merge branch 'master' into vectorize_newton
mikofski May 30, 2018
86df16a
Merge upstream 'master' into vectorize_newton
mikofski Jun 1, 2018
51b870c
TST: test secant slope is zero conditions
mikofski Jun 3, 2018
2343ea8
add test for gh8904
mikofski Jun 5, 2018
faf14e1
Merge branch 'fix_gh8904_zeroder_at_root_newton_fails' into array_new…
mikofski Jun 5, 2018
9c242a3
test arrays duh
mikofski Jun 5, 2018
3746e28
fix #8904 change "failures" to "converged"
mikofski Jun 5, 2018
c144b11
Merge branch 'master' into array_newton_with_gh8904
mikofski Jun 17, 2018
658dc63
remove extra line between docstring in root results
mikofski Jun 17, 2018
a5af44a
Merge branch 'array_newton_with_gh8904' into vectorize_newton
mikofski Jun 17, 2018
6e083f9
TST: ENH: test fval before fder and terminate if all roots found
mikofski Jun 17, 2018
0cfcd3d
TST: fix check zero derivatives in array newton
mikofski Jun 17, 2018
45e7b6a
TST: fix gh8904 test for array neweton
mikofski Jun 17, 2018
1f2748c
ENH: initialize nz_der as True, if fval is zero set failures to False
mikofski Jun 17, 2018
c05a476
DOC: add example of good usage case for vectorized
mikofski Jun 24, 2018
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
54 changes: 48 additions & 6 deletions scipy/optimize/tests/test_zeros.py
Expand Up @@ -5,7 +5,7 @@
from numpy.testing import (assert_warns, assert_,
assert_allclose,
assert_equal)
from numpy import finfo
import numpy as np

from scipy.optimize import zeros as cc
from scipy.optimize import zeros
Expand All @@ -18,8 +18,8 @@ class TestBasic(object):
def run_check(self, method, name):
a = .5
b = sqrt(3)
xtol = 4*finfo(float).eps
rtol = 4*finfo(float).eps
xtol = 4*np.finfo(float).eps
rtol = 4*np.finfo(float).eps
for function, fname in zip(functions, fstrings):
zero, r = method(function, a, b, xtol=xtol, rtol=rtol,
full_output=True)
Expand Down Expand Up @@ -56,6 +56,48 @@ def test_newton(self):
x = zeros.newton(f, 3, fprime=f_1, fprime2=f_2, tol=1e-6)
assert_allclose(f(x), 0, atol=1e-6)

def test_newton_array(self):
"""test newton with array"""

def f1(x, *a):
b = a[0] + x * a[3]
return a[1] - a[2] * (np.exp(b / a[5]) - 1.0) - b / a[4] - x

def f1_1(x, *a):
b = a[3] / a[5]
return -a[2] * np.exp(a[0] / a[5] + x * b) * b - a[3] / a[4] - 1

def f1_2(x, *a):
b = a[3] / a[5]
return -a[2] * np.exp(a[0] / a[5] + x * b) * b ** 2

a0 = np.array([
5.32725221, 5.48673747, 5.49539973,
5.36387202, 4.80237316, 1.43764452,
5.23063958, 5.46094772, 5.50512718,
5.42046290
])
a1 = (np.sin(range(10)) + 1.0) * 7.0
args = (a0, a1, 1e-09, 0.004, 10, 0.27456)
x0 = 7.0
x = zeros.newton(f1, x0, f1_1, args)
x_expected = (
6.17264965, 11.7702805, 12.2219954,
7.11017681, 1.18151293, 0.143707955,
4.31928228, 10.5419107, 12.7552490,
8.91225749
)
assert_allclose(x, x_expected)
# test halley's
x = zeros.newton(f1, x0, f1_1, args, fprime2=f1_2)
assert_allclose(x, x_expected)
# test secant
x = zeros.newton(lambda y, z: z - y ** 2, 4.0, args=([15.0, 17.0],))
assert_allclose(x, (3.872983346207417, 4.123105625617661))
# test derivative zero warning
assert_warns(RuntimeWarning, zeros.newton,
lambda y: y ** 2, [0., 0.], lambda y: 2 * y)

def test_deriv_zero_warning(self):
func = lambda x: x**2
dfunc = lambda x: 2*x
Expand All @@ -69,8 +111,8 @@ def f(x):
return x - root

methods = [cc.bisect, cc.ridder]
xtol = 4*finfo(float).eps
rtol = 4*finfo(float).eps
xtol = 4*np.finfo(float).eps
rtol = 4*np.finfo(float).eps
Copy link
Member

Choose a reason for hiding this comment

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

these two lines could be

xtol = rtol = 4*np.finfo(float).eps

for method in methods:
res = method(f, -1e8, 1e7, xtol=xtol, rtol=rtol)
assert_allclose(root, res, atol=xtol, rtol=rtol,
Expand All @@ -94,7 +136,7 @@ def f(x):
return x - 0.6

atol = 0.51
rtol = 4*finfo(float).eps
rtol = 4*np.finfo(float).eps
methods = [cc.brentq, cc.brenth]
for method in methods:
res = method(f, 0, 1, xtol=atol, rtol=rtol)
Expand Down
54 changes: 25 additions & 29 deletions scipy/optimize/zeros.py
Expand Up @@ -3,11 +3,11 @@
import warnings

from . import _zeros
from numpy import finfo, sign, sqrt
import numpy as np

_iter = 100
_xtol = 2e-12
_rtol = 4*finfo(float).eps
_rtol = 4*np.finfo(float).eps

__all__ = ['newton', 'bisect', 'ridder', 'brentq', 'brenth']

Expand Down Expand Up @@ -163,54 +163,50 @@ def newton(func, x0, fprime=None, args=(), tol=1.48e-8, maxiter=50,
# Newton-Rapheson method
# 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
fder2 = 0
p0 = 1.0 * np.asarray(x0) # convert to ndarray
for iter in range(maxiter):
myargs = (p0,) + args
fder = fprime(*myargs)
if fder == 0:
fder = np.asarray(fprime(*myargs)) # convert to ndarray
if (fder == 0).any():
Copy link

Choose a reason for hiding this comment

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

wouldn't you need to keep iterating the other elements?

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 could, but IMO you really don't need to. I think it requires indexing which IMO makes the code a little messy. Do you feel very strongly that it should keep iterating the other cases? I'm open to changes.

Copy link

Choose a reason for hiding this comment

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

you'd exit early upon hitting a problematic termination condition for any element, even if remaining elements are far from convergence. but I'd defer to a scipy maintainer's opinion here, since non-scalar usage would be new functionality (right? does the released code error on arrays?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. See the traceback in #8354.

msg = "derivative was zero."
warnings.warn(msg, RuntimeWarning)
return p0
fval = func(*myargs)
if fprime2 is not None:
fder2 = fprime2(*myargs)
if fder2 == 0:
fval = np.asarray(func(*myargs)) # convert to ndarray
if fprime2 is None:
# Newton step
p = p0 - fval / fder
else:
# Parabolic Halley's method
discr = fder ** 2 - 2 * fval * fder2
if discr < 0:
p = p0 - fder / fder2
else:
p = p0 - 2*fval / (fder + sign(fder) * sqrt(discr))
if abs(p - p0) < tol:
fder2 = np.asarray(fprime2(*myargs)) # convert to ndarray
# Halley's method
# https://en.wikipedia.org/wiki/Halley%27s_method
p = p0 - 2 * fval * fder / (2 * fder ** 2 - fval * fder2)
Copy link

Choose a reason for hiding this comment

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

is removing the switch changing the scalar behavior here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, this switch is particular to the "parabolic Halley's method" a variant, see link in #5922. I removed it because branches require indexing which IMO makes the code a little messy. Using the traditional Halley's method, see Wikipedia link in code, removed the need for branching, and IMHO makes the code cleaner. Does this answer your question?

Copy link

Choose a reason for hiding this comment

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

Gotcha. Guess I'd avoid coupling behavior changes for the scalar case to the feature addition of vectorization (but again my opinion here matters less than the maintainers'). The two changes could be done separately, one but not the other, or both, though vectorizing the modified version is simpler as you say.

Copy link
Member

Choose a reason for hiding this comment

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

Guess I'd avoid coupling behavior changes for the scalar case to the feature addition of vectorization

I agree 100% with this. Let's stick to one issue at a time. (The modification looks more prone to overflow because of the fder**2, so it's not clear the changes are a win.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The current version (as of scipy-1.0.0) with the "parabolic" variant of Halley's already has fder**2 on L183, so if it is likely to overflow, then it would have already.

I agree it would be more explicit to future maintainers to understand if we separated the switch from the parabolic variant of Halley's to the more widely accepted form into a separate PR. Although it's a requirement for this PR, because the more common version of Halley's doesn't require any branching which I'm really hoping to avoid here. So should I open another PR to propose switching out the parabolic variant of Halley's?

if np.abs(p - p0).max() < tol:
return p
p0 = p
else:
# Secant method
p0 = x0
if x0 >= 0:
p1 = x0*(1 + 1e-4) + 1e-4
else:
p1 = x0*(1 + 1e-4) - 1e-4
q0 = func(*((p0,) + args))
q1 = func(*((p1,) + args))
p0 = np.asarray(x0)
dx = np.finfo(float).eps**0.33
dp = np.where(p0 >= 0, dx, -dx)
p1 = p0 * (1 + dx) + dp
q0 = np.asarray(func(*((p0,) + args)))
Copy link
Member

Choose a reason for hiding this comment

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

This is the same as the much more readable func(p0, *args), you use the same convoluted construct two more times.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you so much for your thorough and thoughtful feedback!

I agree 100% - this snippet is from the original scalar newton code - I was trying to make the minimum amount of changes possible, but I agree this snippet could be improved to be more readable. I've made a new issue #8589 to address readability in both the original newton method and the proposed array newton method.

q1 = np.asarray(func(*((p1,) + args)))
for iter in range(maxiter):
if q1 == q0:
if p1 != p0:
msg = "Tolerance of %s reached" % (p1 - p0)
divide_by_zero = (q1 == q0)
if divide_by_zero.any():
Copy link

Choose a reason for hiding this comment

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

here as well, wouldn't it be better to continue for all other elements?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See my previous answer in the newton branch. My ethic was to make the smallest changes possible to add some new features. If users demand more features, then I think we should discuss the trade offs.

Copy link
Member

Choose a reason for hiding this comment

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

The halting conditions are the biggest thing to think about here. This way is simple, but I'm sure that someone is going to yell at us in the future about evaluating their callback function too many times. Might be worth seeing how messy the indexing would look.

tolerance_reached = (p1 != p0)
if (divide_by_zero & tolerance_reached).any():
msg = "Tolerance of %s reached" % np.sqrt(sum((p1 - p0)**2))
warnings.warn(msg, RuntimeWarning)
return (p1 + p0)/2.0
else:
p = p1 - q1*(p1 - p0)/(q1 - q0)
if abs(p - p1) < tol:
if np.abs(p - p1).max() < tol:
return p
p0 = p1
q0 = q1
p1 = p
q1 = func(*((p1,) + args))
q1 = np.asarray(func(*((p1,) + args)))
msg = "Failed to converge after %d iterations, value is %s" % (maxiter, p)
raise RuntimeError(msg)

Expand Down