Skip to content

Conversation

TimButters
Copy link
Contributor

The minimize function crashes if xtol is set to 0. This is because the initial value of a loop variable is 2 * xtol, which is intended to ensure the loop runs at least once. However, if xtol = 0 then the loop never runs, a variable isn't instantiated that is used outside of the loop, and an UnboundLocalError is raised.

To fix this, the initial value of the loop variable is set to the max float value.

Reference issue

Closes #20214

What does this implement/fix?

UnboundLocalError is thrown if xtol is set to 0 for the newton-cg method in minimize.

Additional information

The minimize function crashes if `xtol` is set to `0`. This is
because the initial value of a loop variable is `2 * xtol`, which
is intended to ensure the loop runs at least once. However, if
`xtol = 0` then the loop never runs, a variable isn't instantiated
that is used outside of the loop, and an `UnboundLocalError` is
raised.

To fix this, the initial value of the loop variable is set to the
max float value.
@TimButters TimButters requested a review from andyfaff as a code owner March 20, 2024 23:45
@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 Mar 20, 2024
Copy link
Member

@lucascolley lucascolley left a comment

Choose a reason for hiding this comment

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

LGTM!

@lucascolley lucascolley requested review from dschmitz89 and removed request for andyfaff March 21, 2024 00:17
@tylerjereddy tylerjereddy added this to the 1.14.0 milestone Mar 21, 2024
Copy link
Contributor

@tylerjereddy tylerjereddy left a comment

Choose a reason for hiding this comment

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

I'll let the optimize regulars chime in on whether a regression test is sensible to add, but this slightly modified version of the original reproducer fails before/passes after and seems fast:

--- a/scipy/optimize/tests/test_optimize.py
+++ b/scipy/optimize/tests/test_optimize.py
@@ -3120,3 +3120,19 @@ def test_sparse_hessian(method, sparse_type):
     assert res_dense.nfev == res_sparse.nfev
     assert res_dense.njev == res_sparse.njev
     assert res_dense.nhev == res_sparse.nhev
+
+
+def test_gh_20214():
+    def cosine(x):
+        f = np.cos(x[0])
+        return f
+
+    def jac(x):
+        ret = -np.sin(x)
+        return ret
+
+    result = optimize.minimize(cosine,
+                               x0=[0.1],
+                               jac=jac,
+                               method="newton-cg",
+                               options=dict(xtol=0, maxiter=2))

@tylerjereddy tylerjereddy added the backport-candidate This fix should be ported by a maintainer to previous SciPy versions. label Mar 21, 2024
Copy link
Member

@h-vetinari h-vetinari left a comment

Choose a reason for hiding this comment

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

At least one error from #20214 occurs elsewhere, namely update is undefined in this branch:

else:
if np.isnan(old_fval) or np.isnan(update).any():
return terminate(3, _status_message['nan'])

Looking at c8c78d7, this commit failed to rename that instance of update to update_l1norm. CC @lorentzenchr

We need a test that exercises this else-branch in any case. I haven't checked if @tylerjereddy's case does that, but to reach the else: clause of the while: loop, we must achieve update_l1norm <= xtol without having triggered any of the early return statements in the while body.

Corrected error from previous rework when switching to
`np.linalg.norm` that introduced the possibility of the unbound
variable error.
Regression test for scipygh-20214 which addressed an issue in the
Newton CG method which would raise an UnboundLocalVariable error
if `xtol` was set to zero. It also ensures the method finds the
solution in this case.
Copy link
Member

@j-bowhay j-bowhay left a comment

Choose a reason for hiding this comment

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

Thanks @TimButters lgtm but I will wait to see if others are happy before merging

@TimButters TimButters requested a review from h-vetinari March 22, 2024 06:25
Copy link
Member

@h-vetinari h-vetinari left a comment

Choose a reason for hiding this comment

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

I'm still unsure about the right initial value for update_l1norm, though I can see the argument that it's being updated at the end of the loop. Still, the interplay with the shape of x is a mystery to me. The code seems very brittle w.r.t. to putting in x0=[1, 2] or something like that.

Unless we can figure this out quickly and comprehensively, I'd prefer an immediate fix for the unboundname error to go into 1.13, and the rest to be fixed for 1.14.

@lorentzenchr
Copy link
Contributor

Is there a test for the „nan-path“?

TimButters and others added 2 commits March 22, 2024 21:51
Comment added to explain the purpose of setting `update_l1norm` to the max float value - this is to ensure the preceding while loop executes at least once.

Co-authored-by: Christian Lorentzen <lorentzen.ch@gmail.com>
This is for the `xtol=0` test in the Newton CG minimize method.
@TimButters
Copy link
Contributor Author

Is there a test for the „nan-path“?

@lorentzenchr yes, there is an existing test test_nan_values that checks this branch.
Line 1494 in test_optimize.py.

@h-vetinari h-vetinari dismissed their stale review March 22, 2024 22:33

Not going to stand in the way here as I don't know the code well enough and I believe this is in good hands; as long as the UnboundNameError is fixed I'm happy. :)

@TimButters
Copy link
Contributor Author

@j-bowhay @lucascolley I think we have consensus now, would one of you be happy to merge this?

Copy link
Member

@lucascolley lucascolley left a comment

Choose a reason for hiding this comment

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

thanks @TimButters , I'll give it a few days for any more comments before merging.

As a note for the future, I'd recommend that you leave resolving conversations to maintainers - a thorough reviewer will probably want to check any conversations that were resolved by the author, so it is easier to leave them as is.

@TimButters
Copy link
Contributor Author

Thanks for the feedback @lucascolley, I thought I was doing the right thing there. I’ll leave them open in the future.

@lucascolley
Copy link
Member

I'll give it a few days for any more comments before merging.

Here we are, thanks @TimButters !

@lucascolley lucascolley merged commit d13ff2d into scipy:main Mar 26, 2024
@lucascolley lucascolley removed the request for review from dschmitz89 March 26, 2024 20:13
@j-bowhay
Copy link
Member

Thanks @TimButters a great couple of first contributions, keep them coming!

@TimButters TimButters deleted the newtoncg-xtol branch March 26, 2024 20:19
@lucascolley lucascolley modified the milestones: 1.14.0, 1.13.0 Mar 26, 2024
tylerjereddy pushed a commit to tylerjereddy/scipy that referenced this pull request Apr 1, 2024
* BUG: Optimize: NewtonCG min crashes with xtol=0

The minimize function crashes if `xtol` is set to `0`. This is
because the initial value of a loop variable is `2 * xtol`, which
is intended to ensure the loop runs at least once. However, if
`xtol = 0` then the loop never runs, a variable isn't instantiated
that is used outside of the loop, and an `UnboundLocalError` is
raised.

To fix this, the initial value of the loop variable is set to the
max float value.

* BUG: Optimize: UnboundLocalError for xtol=0 NCG

Corrected error from previous rework when switching to
`np.linalg.norm` that introduced the possibility of the unbound
variable error.

* TST: Optimize: NewtonCF Minimize when xtol=0

Regression test for scipygh-20214 which addressed an issue in the
Newton CG method which would raise an UnboundLocalVariable error
if `xtol` was set to zero. It also ensures the method finds the
solution in this case.

* MAINT: Add comment to explain initial value

Comment added to explain the purpose of setting `update_l1norm` to the max float value - this is to ensure the preceding while loop executes at least once.

Co-authored-by: Christian Lorentzen <lorentzen.ch@gmail.com>

* TST: Test solution value as well as success

This is for the `xtol=0` test in the Newton CG minimize method.

* TST: Explicitly extract val from array

---------

Co-authored-by: Christian Lorentzen <lorentzen.ch@gmail.com>
@tylerjereddy tylerjereddy removed the backport-candidate This fix should be ported by a maintainer to previous SciPy versions. label May 2, 2024
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.

BUG: minimize(method="newton-cg") crashes with UnboundLocalError if xtol=0
6 participants