Changes to improve exception handling #287
Open
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Addresses issue #286
The Problem
Currently, there are many situations in which cyipopt produces predictable but cryptic errors in response to user input errors. Some examples:
If some of the indices returned by jacobianstructure are negative, one would expect an error message such as
Instead, Ipopt halts with status code -100 (Unrecoverable Exception), without any indication that this issue is an error in the indices.
If the hessian callback raises an exception, one would expect the exception to be raised eventually for the user to see. That doesn't happen, and instead the hessian is not calculated, and typically the Ipopt iteration will never converge. This bug is really insidious, because the user won't know that there's an exception in their code.
If a user specifies hessian indices that are not lower triangular, Ipopt exits with status code -12, Invalid Option, because it concludes that no hessian is provided, and expects the
hessian_approximation
option to be "limited-memory". It would be much better of course to get an error message:The Causes
There are multiple issues:
The jacobianstructure and hessianstructure user callbacks are called at problem instantiation only to get the length of the index arrays, with no error checking. Error checking is deferred until the Ipopt solver is called, and the error checking is done in the callbacks. That's a particularly problematic design choice, because an exception should not be raised in the callbacks. When an error is detected, the callbacks return
False
, which in turn leads Ipopt to return a failure status code. A quick fix would be to raise an exception in the callbacks, which would be caught in the try-except block. That's awkward, but actually works on MacOS, but causes a segfault on Linux, because it leaves the index arrays uninitialized.There are bugs in the
hessian_cb
function. For one, theuser_data
pointer is cast asself = <object>user_data
instead ofself = <Problem>user_data
. That changes how Cython name mangles variables, and the result is that theself.__exception = sys.exc_info()
doesn't actually pass the exception back to the class, and so an exception in the user hessian callback never gets re-raised. Further, there's a missingreturn True
that prevents the intermediate callback from stopping Ipopt execution.Proposed Solution
My proposed solution moves all the index checking into the
Problem.__init__
method, so that all errors are caught at problem instantiation. This effectively moves all the code fromjacobian_struct_cb
andhessian_struct_cb
into the constructor, and also simplifiesjacobian_cb
andhessian_cb
. All errors in the indices raiseValueError
exceptions, with richer messages than the current log messages to provide better feedback to the user.I've also fixed the logic error that silenced all exceptions in the user hessian callback.
In order to allow unit testing of these changes, I added a feature that should be useful to the user. I've added a
Problem.info
attribute, which holds the info dict return in aproblem.solve(x0)
, so that if an exception is re-raised after the call to the Ipopt solver, it can be caught by the unit tests (or the user!), and both the return status and the state of the solver can be examined. AddingProblem.info
does not break the current API, and could actually be viewed as a positive feature to help users debug their code. If not desired as a feature, it could be changed toProblem.__info
.I've also fixed a few typos here and there.
Readiness of the PR
This PR is not to master but to a feature branch, improve-exception-handling, so that if the PR is accepted, some polishing can be done before merging to main. I have some additional tests that aren't quite ready, and minor changes to the docs would be required as well. If you'd prefer a different approach, let me know.
The current PR does pass all the CI tests I believe. I've modifed the
test_deriv_errors.py
test to reflect my changes. An added bonus is that because all the indices are properly tested and an exception is raised if there's an error before the call the Ipopt solver, it's no longer necessary to distinguish pre and post Ipopt v3.14.13, and that is reflected in the tests.