8000 BUG: minimize: fix for powell method by irideselby · Pull Request #21092 · scipy/scipy · GitHub
[go: up one dir, main page]

Skip to content

BUG: minimize: fix for powell method #21092

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open

Conversation

irideselby
Copy link

Hello all,

I encountered an issue in the minimize function while working with differential evolution. For some members of the population, the direc1 value of the extrapolated point became zero without being caught by the check in _linesearch_powell, meaning the optimisation could not progress further.

It turns out this problem arises during the iteration of successive points in _minimize_powell: In each step, the current point is stored for comparison with the subsequent point, from which the direc1 value for the next iteration is calculated. While the f(x)-value of each point is correctly stored, the corresponding x-value is not stored at all if the condition fx > fx2 is fulfilled. That means the current point will be stored with the correct f(x)-value but with the wrong (previous) x-value.

In some circumstances it can now happen that in one iteration _linesearch_powell results in a point with 2.0 * (fx - fval) > bnd, meaning the function doesn't break out, but with x - x1 = direc1 = 0, meaning the optimisation cannot progress further.

This pull request fixes the storing of x-values, such that all points are stored correctly for comparison with subsequent points of the iteration. Additionally, a unit test is implemented to check the correct behaviour.

@irideselby irideselby requested a review from andyfaff as a code owner July 1, 2024 12:07
@github-actions github-actions bot added scipy.optimize defect A clear bug or issue that prevents SciPy from being installed or used as expected labels Jul 1, 2024
@andyfaff
Copy link
Contributor
andyfaff commented Jul 1, 2024

This PR doesn't seem to make a lot of sense and is a little suspicious.

@irideselby
Copy link
Author

Can you elaborate on what doesn't make a lot of sense to you?

@andyfaff
Copy link
Contributor
andyfaff commented Jul 2, 2024

while working with differential evolution. For some members of the population, the direc1 value of the extrapolated point became zero without being caught by the check in _linesearch_powell, meaning the optimisation could not progress further.

Please forgive me, I was hasty to judge. I was confused because method=powell has nothing to do with differential_evolution.

The following example on the current main (i.e. without the fix applied) reveals no circumstances where test_func(intermediate_result.x) != intermediate_result.fun:

import numpy as np
from scipy.optimize import minimize, rosen

class CallbackMinimize:

    def __init__(self):
        self.f_vals = []
        self.x_vals = []

    def __call__(self, intermediate_result):
        self.f_vals.append(intermediate_result.fun)
        self.x_vals.append(intermediate_result.x)

def test_func(x):
    return np.sum(x ** 2)

call_fun = CallbackMinimize()
x_min, x_max = 0, 10
bnds = [(x_min, x_max)]
res = minimize(
    test_func,
    x0=[9],
    bounds=bnds,
    method='Powell',
    tol=1e-6,
    callback=call_fun
)

for i in range(len(call_fun.f_vals)):
    np.testing.assert_equal(call_fun.f_vals[i], test_func(call_fun.x_vals[i]))

np.testing.assert_equal(test_func(res.x), res.fun)
  • I can't see any location in the loop where fval and x aren't kept synced.
  • I can see how x1 and fx could get out of sync, because fval is updated after x1 is copied, and at the start of the while loop fx=fval. It's unclear what effect this has on minimisation, I don't see how all entries in direc1 would be zero, even if x1 was stale.

@irideselby
Copy link
Author
irideselby commented Jul 2, 2024

Sorry for the confusion! I just wanted to give context about where my problem arose, which was because I minimised a lot of functions (that stemmed from a large population I generated with differential_evolution); the problem itself has nothing to do with the differential_evolution.

Concerning the unit test you showed, the problem is not that test_func(intermediate_result.x) != intermediate_result.fun doesn't happen; actually the unit test you reference will indeed not give any error without the fix, because it checks the output of intermediate_result, which is an OptimizeResult object and this object is only created here, and in this line x and fval are always in sync.
However, as you said referring to this block, _linesearch_powell is called and fval and x are calculated, but only fx=fval is updated in the while loop, while x1 is not updated. Then with this updated fx and not-updated x1, the next _linesearch_powell is called, from which we get a new fval and x, which are again in sync. Now that we are again in sync, the next OptimizeResult object is created, meaning the instance where fval and x were out of sync is not caught by the above-mentioned unit test that only tests the intermediate_result.

As far as I see, all of this only becomes a real problem in the following case: starting with some initial x1 = xa and fx = fa, we go through the iteration and reach this block, here we get some fx = fb, but we keep x1 = xa because it is not updated. Then in the next iteration step, we calculate some new point with fval = fc and x = xc (x is again updated). From my experience it is now possible that during the iteration xc = xa and fc = fa, because we step in one direction and then back from where we came. If we then check the condition if 2.0 * (fx - fval) = 2.0 * (fc - fb) <= bnd:, this will not necessarily go below the bnd, so we don't break out of the function. Then we calculate direc1 here, and since we didn't update x1 at the top of the iteration, that means x = xc and x1 = xa (NOT xb as it "should" be). If now xa = xc as I assumed above, then direc1 = 0 (only this one entry, not all entries), but as soon as this is the case even for one entry, the minimisation cannot continue when calling _line_for_search; this is even referred to in its documentation, that direc1 cannot even become zero because of a check implemented in a different function.

I know this is a special case, but when I minimised a large amount of functions, this direc1 = 0 happened not often but regularly, which is why I tried to find out what is happening. I think it all boils down to actually assigning x1 = x whenever we also assign fx = fval, so that the pairs are always in sync. My unit test doesn't compare f(x) to fval, but it compares what I above named xa to xc as well as fa to fc.

I hope this clears everything up a bit.

@andyfaff
Copy link
Contributor
andyfaff commented Jul 2, 2024

Now that we are again in sync, the next OptimizeResult object is created, meaning the instance where fval and x were out of sync is not caught by the above-mentioned unit test that only tests the intermediate_result.

fval and x are never out of sync, it's only fx and x1 that can get out of sync.

I think I can just about follow the logic in your post, but it's not easy. I think it's ok to introduce the x1 = x.copy() at the top of the loop, but I'm 50:50 on whether we should introduce new entries into the intermediate_result (is it useful for the end user, apart from our check here).

@irideselby
Copy link
Author

fval and x are never out of sync, it's only fx and x1 that can get out of sync.

You're right, that's what I meant to say.

Originally I wasn't even planning on changing the entries of intermediate_result, then when constructing the unit test I just didn't see another way of testing this behaviour. Ideally one would find a unit test where direc1 = 0 happens, but I found this difficult to reconstruct in a simple manner that's suited for a unit test.
I don't think changing the entries helps in any way other than for this particular test, and I understand that one shouldn't adjust the functions to suit the unit test, but rather the other way around.

From my side it would be okay to take out the unit test, leave intermediate_result alone and just move the x1 = x.copy().

@irideselby
Copy link
Author

@andyfaff :)

@irideselby
Copy link
Author

Since I couldn't come up with a better unit test for the behaviour, I deleted the unit test and reverted the corresponding change in OptimizeResult.

@j-bowhay j-bowhay added this to the 1.15.0 milestone Nov 12, 2024
@tylerjereddy
Copy link
Contributor

@andyfaff @irideselby where does this stand? we're likely a few days from branching for 1.15.0 now

@andyfaff
Copy link
Contributor
andyfaff commented Dec 5, 2024

I've lost currency with the changeset here. I don't think I'll be able to revisit before branching

@tylerjereddy
< 8000 span class="Button-content"> Copy link
Contributor

Alright, I'll bump this one for now--this is definitely a strange MR with 1-line change!

@tylerjereddy tylerjereddy modified the milestones: 1.15.0, 1.16.0 Dec 7, 2024
@tylerjereddy
Copy link
Contributor

I went through the comments in this PR again today because I try to avoid blindly bumping PRs just before a release, especially for first time contributors.

I sat down with @j-bowhay for a little bit and I think we agreed that we're just not comfortable enough on the justification for the change here to merge at this time.

It seems a bit tricky to justify without a robust reproducer that could be used as a unit test. I'm not trying to discourage pursuing the fix though, and it does sounds like the developer here really is trying to communicate a complex problem, but it is still a bit hard for me to understand at this time. So, I will bump the milestone again--let us know what we can do to help. I think a simple reproducer/unit test is the clearest route to getting this across the line.

@tylerjereddy tylerjereddy modified the milestones: 1.16.0, 1.17.0 May 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
defect A clear bug or issue that prevents SciPy from being installed or used as expected scipy.optimize
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants
0