-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
ENH: Add vectorized scalar minimization bracket finder #19757
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
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 flying now, so I'd like to play with it a bit when I get to my desktop, but it looks good!
This has been placed in
scipy.optimize._zeros_py.py
to avoid a circular import, since that is where most of the machinery used here lives.
Shall I move the machinery before we merge this?
I think I finally get the details of how this machinery works.
Cool! In that case, would you review gh-19545? It relaxes the requirement that the shape of the function output must be the shape of the input. This is much more convenient in some cases (especially _tanhsinh
, in which case users want to express callables as vector-valued).
Here's a pretty useful smoke test. I adjusted import numpy as np
from scipy import stats
rng = np.random.default_rng(2549824598234528)
from scipy.optimize._zeros_py import _bracket_minima
from scipy.stats._distr_params import distcont
distcont = dict(distcont)
n_trials = 0
n_success = 0
for distname, params in distcont.items():
family = getattr(stats, distname)
dist = family(*params)
def f(x):
return -dist.pdf(x)
try:
res = _bracket_minima(f, a=dist.rvs(size=10),
min=dist.support()[0], max=dist.support()[1])
print(distname, np.all(res.success))
n_trials += 10
n_success += np.sum(res.success)
except Exception as e:
print(distname, str(e))
print(f'success rate: {n_success/n_trials}') Obviously there's no reason that all of these should achieve success, but it does perfectly aside from the cases which got an invalid input error. The three distributions which did not always see success are Code to investigate a particular distribution:
Update: When I change to |
Thanks @mdhaber. Good points all. I have a clear idea of how to proceed for everything except how to handle the arguments for left, middle and right initial points. I don't have a strong preference yet, but will think about it some more and then we can discuss. |
@mdhaber. I think I've made all of the code changes asked for. I changed the signature to include the midpoint as a positional arg, and made the left and right endpoints optional and keyword only. I wrote a scheme for the defaults to ensure the initial bracket won't go beyond a bound when the relevant endpoint isn't specified. I also changed how the updates work to match I haven't updated the tests yet. There's still a question of standardizing variable names for local variables in the functions, and for the inputs. As a proposal, I've changed ...
max = [1.0, 2.0, 3.0]
result = bracket_minimum(f, xm, max=max, args=args) and run into problems I also changed |
docs build will be fixed upon merging main |
scipy/optimize/_zeros_py.py
Outdated
work.step[~work.limited] *= work.factor[~work.limited] | ||
work.step[work.limited] /= work.factor[work.limited] | ||
x = np.empty_like(work.xr) | ||
x[~work.limited] = work.xr[~work.limited] + work.step[~work.limited] | ||
x[work.limited] = work.limit[work.limited] - work.step[work.limited] |
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.
Thanks, this is more similar to the _bracket_root
logic now. It makes me realize, though, that I think we could redefine factor at the top like
work.factor[work.limited] = 1/work.factor[work.limited]
and then we don't have do fancy indexing every iteration.
work.step *= work.factor
Could do it here in both functions or in a separate PR if it sounds good.
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've changed it for bracket_minimum
. I think a separate PR for the other function would be better. I had to change
factor = np.broadcast_to(factor, shape).astype(dtype, copy=False).ravel()
to
factor = np.broadcast_to(factor, shape).copy().astype(dtype, copy=False).ravel()
because np.broadcast_to
produces a read-only view. I guess the copy=False
might not be needed anymore, but I left it in.
0dbd52b
to
d206a4d
Compare
OK. I've tested this thoroughly and actually uncovered a couple bugs. 1) dtypes changing to np.float64 in I like |
OK. I think I've successfully resolved the merge conflicts. I think the main thing to settle down now is consistent argument names for For the former we currently have For the later we have I prefer the convention I chose for
|
The reason I didn't use |
Oh, good point. I do think of them like initial guesses, so I don't think that aspect is an issue. It kind of bugs me that I can't help reading them as "x lo", "x mo", "x ro". |
Would prefer to avoid the underscores, and it's |
Done |
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 looked at _bracket.py
and found some minor things. I'll take a look at the tests soon!
if not np.all(factor > 1): | ||
raise ValueError('All elements of `factor` must be greater than 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.
I seem to remember thinking of cases in which factor < 1
could be desirable for _bracket_root
. Anyway, something to think about, but this is the primary use 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.
Looks like the test match those for TestBracketRoot
for the most part, and I'm not thinking of other tests that would be needed here (that aren't already). The main comments are that the vectorization test looks like it could be stronger and I'd like to know more about the magic numbers in tests from test_scalar_no_limits
through test_minima_at_boundary_point
.
OK, I think those will be my only comments. When they are resolved, I think it's good to go. When it's done, would you do a quick PR to address #19757 (review) for |
Thanks @mdhaber! I batched up and committed all of your suggestions and should have time to fix the remaining things on Thursday or Friday. |
OK. When that's done, maybe squash commits before and after the name changes so that the name change commit is separate; we'll merge three commits. |
I think I've addressed everything.
I've also fixed a place where I accidentally changed |
Re: |
7ef8cd4
to
ed2f735
Compare
Well, that attempt at git surgery was less than successful. Sorry everyone who got notified. |
@mdhaber, I'm just going to close this and make a new PR with the three commits. |
Co-authored-by: Matt Haberland <mhaberla@calpoly.edu> Only update xl0/xr0 to feasible values when not supplied Fix plural when should be singular Combine xl0, xr0 in docstring Fix false positive variable renaming Change vectorization test to match TestBracketRoot Add comments explaining magic constants in tests Add another check to initial bracket valid test Mention that `xl0`, `xr0` must be broadcastable with `xm0`
ed2f735
to
4776858
Compare
Here's the backup of my branch so you can check the diff https://github.com/steppi/scipy/tree/scalar-minimize-bracket-finder-backup |
Reference issue
I don't think there's an issue for this.
What does this implement/fix?
This PR adds a private vectorized$f$ is a triple of points $x_l$ , $x_m$ , $x_r$ , with $x_l < x_m < x_r$ , such that
scalar_minimization
bracket finder, similar to the vectorized root bracket finder from gh-18348. The intent of this function is the same as scipy.optimize.bracket. A bracket for the minima of a unimodal univariate functionwhere at least one of these two inequalities is strict. Unlike
scipy.optimize.bracket
, this function is able to accept upperand lower bounds on the endpoints of brackets. The algorithm was suggested to me by @mdhaber, who convinced me it is a sound approach. Quoting from the Notes section of the Docstring
Given a
Additional information
This has been placed in
scipy.optimize._zeros_py.py
to avoid a circular import, since that is where most of the machinery used here lives. A more rightful home would bescipy.optimize._optimize.py
wherescipy.optimize.bracket
lives. Since this is private, I don't think this is important just yet, but we should keep it in mind for the future. @mdhaber, I think I finally get the details of how this machinery works.