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

Support user-provided CyIpopt callbacks with 13 arguments #3289

Merged
merged 14 commits into from
Aug 20, 2024

Conversation

Robbybp
Copy link
Contributor

@Robbybp Robbybp commented Jun 14, 2024

Summary/Motivation:

Currently, users are unable to access the get_current_iterate and get_current_violations methods during an intermediate callback as they don't have access to the cyipopt.Problem object. We currently expect a user-provided callback to be a function with 12 arguments: the 11 arguments that Ipopt gives us, plus the NLP object that cyipopt is using. This PR adds support for a new callback API, where the user's callback accepts 13 arguments. The new argument is the cyipopt.Problem instance (our CyIpoptNLP), from which the user can access the get_current* methods.

This is the first step towards implementing something like #2860. With this PR, users can implement their own callbacks to track primal-dual iterates and infeasibilities. Eventually, I'd like to commit a basic debugging callback that automatically tracks useful information.

Changes proposed in this PR:

  • Update CyIpopt interface to support user-provided callbacks with 13 arguments

Legal Acknowledgement

By contributing to this software project, I have read the contribution guide and agree to the following terms and conditions for my contribution:

  1. I agree my contributions are submitted under the BSD license.
  2. I represent I am authorized to make the contributions and grant the license. If my employer has rights to intellectual property that includes these contributions, I represent that I have received permission to make contributions and grant the required license on behalf of that employer.

@blnicho blnicho self-requested a review June 25, 2024 19:02
mrmundt
mrmundt previously requested changes Jul 9, 2024
pyomo/contrib/pynumero/interfaces/cyipopt_interface.py Outdated Show resolved Hide resolved
pyomo/contrib/pynumero/interfaces/cyipopt_interface.py Outdated Show resolved Hide resolved
pyomo/contrib/pynumero/interfaces/cyipopt_interface.py Outdated Show resolved Hide resolved
@blnicho blnicho requested a review from mrmundt July 10, 2024 23:35
Copy link
Member

@blnicho blnicho left a comment

Choose a reason for hiding this comment

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

I have one minor edit suggestion but otherwise this looks fine.

pyomo/contrib/pynumero/interfaces/cyipopt_interface.py Outdated Show resolved Hide resolved
@blnicho
Copy link
Member

blnicho commented Aug 6, 2024

Looks like the Jenkins test failure is real. Here is the stack-trace:

15:25:17 =================================== FAILURES ===================================
15:25:17 _________________ TestCyIpoptSolver.test_solve_13arg_callback __________________
15:25:17 
15:25:17 self = <pyomo.contrib.pynumero.algorithms.solvers.tests.test_cyipopt_solver.TestCyIpoptSolver testMethod=test_solve_13arg_callback>
15:25:17 
15:25:17     @unittest.skipIf(
15:25:17         not cyipopt_available or not cyipopt_ge_1_3, "cyipopt version < 1.3.0"
15:25:17     )
15:25:17     def test_solve_13arg_callback(self):
15:25:17         m = create_model1()
15:25:17     
15:25:17         iterate_data = []
15:25:17     
15:25:17         def intermediate(
15:25:17             nlp,
15:25:17             problem,
15:25:17             alg_mod,
15:25:17             iter_count,
15:25:17             obj_value,
15:25:17             inf_pr,
15:25:17             inf_du,
15:25:17             mu,
15:25:17             d_norm,
15:25:17             regularization_size,
15:25:17             alpha_du,
15:25:17             alpha_pr,
15:25:17             ls_trials,
15:25:17         ):
15:25:17             iterate = problem.get_current_iterate()
15:25:17             x = iterate["x"]
15:25:17             y = iterate["mult_g"]
15:25:17             iterate_data.append((x, y))
15:25:17     
15:25:17         x_sol = np.array([3.85958688, 4.67936007, 3.10358931])
15:25:17         y_sol = np.array([-1.0, 53.90357665])
15:25:17     
15:25:17         solver = pyo.SolverFactory("cyipopt", intermediate_callback=intermediate)
15:25:17         res = solver.solve(m, tee=True)
15:25:17 >       pyo.assert_optimal_termination(res)
15:25:17 
15:25:17 pyomo/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py:406: 
15:25:17 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
15:25:17 
15:25:17 results = {'Problem': [{'Name': 'unknown', 'Lower bound': -inf, 'Upper bound': -504.0, 'Number of objectives': 1, 'Number of con... termination of the optimization.', 'Wallclock time': 0.003202378051355481, 'Termination condition': 'userInterrupt'}]}
15:25:17 
15:25:17     def assert_optimal_termination(results):
15:25:17         """
15:25:17         This function checks if the termination condition for the solver
15:25:17         is 'optimal', 'locallyOptimal', or 'globallyOptimal', and the status is 'ok'
15:25:17         and it raises a RuntimeError exception if this is not true.
15:25:17     
15:25:17         Parameters
15:25:17         ----------
15:25:17         results : Pyomo results object returned from solver.solve
15:25:17         """
15:25:17         if not check_optimal_termination(results):
15:25:17             msg = (
15:25:17                 'Solver failed to return an optimal solution. '
15:25:17                 'Solver status: {}, Termination condition: {}'.format(
15:25:17                     results.solver.status, results.solver.termination_condition
15:25:17                 )
15:25:17             )
15:25:17 >           raise RuntimeError(msg)
15:25:17 E           RuntimeError: Solver failed to return an optimal solution. Solver status: aborted, Termination condition: userInterrupt
15:25:17 
15:25:17 pyomo/pyomo/opt/results/solver.py:178: RuntimeError
15:25:17 ----------------------------- Captured stdout call -----------------------------
15:25:17 This is Ipopt version 3.13.2, running with linear solver ma27.
15:25:17 
15:25:17 Number of nonzeros in equality constraint Jacobian...:        2
15:25:17 Number of nonzeros in inequality constraint Jacobian.:        2
15:25:17 Number of nonzeros in Lagrangian Hessian.............:        4
15:25:17 
15:25:17 Total number of variables............................:        3
15:25:17                      variables with only lower bounds:        2
15:25:17                 variables with lower and upper bounds:        0
15:25:17                      variables with only upper bounds:        0
15:25:17 Total number of equality constraints.................:        1
15:25:17 Total number of inequality constraints...............:        1
15:25:17         inequality constraints with only lower bounds:        0
15:25:17    inequality constraints with lower and upper bounds:        0
15:25:17         inequality constraints with only upper bounds:        1
15:25:17 
15:25:17 iter    objective    inf_pr   inf_du lg(mu)  ||d||  lg(rg) alpha_du alpha_pr  ls
15:25:17    0 -5.0400000e+02 5.00e+00 2.17e+01  -1.0 0.00e+00    -  0.00e+00 0.00e+00   0
15:25:17 
15:25:17 Number of Iterations....: 0
15:25:17 
15:25:17                                    (scaled)                 (unscaled)
15:25:17 Objective...............:  -8.7500000000000000e+01   -5.0400000000000000e+02
15:25:17 Dual infeasibility......:   2.1710422009792488e+01    1.2505203077640472e+02
15:25:17 Constraint violation....:   5.0000000000000000e+00    5.0000000000000000e+00
15:25:17 Complementarity.........:   4.0000000099999999e+00    2.3040000057600000e+01
15:25:17 Overall NLP error.......:   2.1710422009792488e+01    1.2505203077640472e+02
15:25:17 
15:25:17 
15:25:17 Number of objective function evaluations             = 1
15:25:17 Number of objective gradient evaluations             = 1
15:25:17 Number of equality constraint evaluations            = 1
15:25:17 Number of inequality constraint evaluations          = 1
15:25:17 Number of equality constraint Jacobian evaluations   = 1
15:25:17 Number of inequality constraint Jacobian evaluations = 1
15:25:17 Number of Lagrangian Hessian evaluations             = 0
15:25:17 Total CPU secs in IPOPT (w/o function evaluations)   =      0.002
15:25:17 Total CPU secs in NLP function evaluations           =      0.000
15:25:17 
15:25:17 EXIT: Stopping optimization at current point as requested by user.
15:25:17 ----------------------------- Captured stderr call -----------------------------
15:25:17 Exceptions caught in Qt event loop:
15:25:17 ________________________________________________________________________________
15:25:17 Traceback (most recent call last):
15:25:17   File ".../python310/pyomo/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py", line 500, in intermediate
15:25:17     return self._intermediate_callback(
15:25:17   File ".../python310/pyomo/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py", line 396, in intermediate
15:25:17     iterate = problem.get_current_iterate()
15:25:17   File "cyipopt/cython/ipopt_wrapper.pyx", line 701, in ipopt_wrapper.Problem.get_current_iterate
15:25:17 RuntimeError: get_current_iterate only supports Ipopt version >=3.14.0 CyIpopt is compiled with version 3.13.2
15:25:17 =========================== short test summary info ============================
15:25:17 FAILED pyomo/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py::TestCyIpoptSolver::test_solve_13arg_callback - RuntimeError: Solver failed to return an optimal solution. Solver status: aborted, Termination condition: userInterrupt

Copy link

codecov bot commented Aug 13, 2024

Codecov Report

Attention: Patch coverage is 93.33333% with 1 line in your changes missing coverage. Please review.

Project coverage is 88.54%. Comparing base (404fd6d) to head (671d8c6).
Report is 603 commits behind head on main.

Files Patch % Lines
...o/contrib/pynumero/interfaces/cyipopt_interface.py 93.33% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3289   +/-   ##
=======================================
  Coverage   88.53%   88.54%           
=======================================
  Files         868      868           
  Lines       98495    98509   +14     
=======================================
+ Hits        87206    87221   +15     
+ Misses      11289    11288    -1     
Flag Coverage Δ
linux 86.05% <93.33%> (+<0.01%) ⬆️
osx 75.62% <6.66%> (-0.01%) ⬇️
other 86.55% <93.33%> (+<0.01%) ⬆️
win 83.86% <93.33%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Member

@jsiirola jsiirola left a comment

Choose a reason for hiding this comment

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

One comment, but not something that I will hold up the PR for.

Comment on lines +318 to +320
# callback to infer whether they want this argument. To preserve backwards
# compatibility if the user asked for variable-length *args, we do not pass
# the Problem object as an argument in this case.
Copy link
Member

Choose a reason for hiding this comment

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

I am a big fan of backwards compatibility, but in this case, I think I disagree: if the user defined the callback using *args, then it is their responsibility to track any changes to our callback API.

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 counterpoint is that they may reasonably expect the callback API to be stable. Personally, I would rather we didn't support *args at all. Maybe we should raise a deprecation warning if a 12-arg callback or *args is provided?

Copy link
Member

Choose a reason for hiding this comment

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

The counter-counterpoint is this is in contrib, so this is where we make the weakest (i.e., no) guarantee on backwards compatibility.

With so many arguments, I like the model where we pass everything by name, and deprecate all use of positional arguments. We could be clever and even allow callbacks with subsets of named arguments (which would support backwards compatibility), future-proof us to passing new arguments, and remove the current reliance on a large set of ordered arguments.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

While CyIpoptInterface is in contrib, a user can provide a callback via pyo.SolverFactory("cyipopt", intermediate_callback=callback). I've always had the impression that solvers accessible via SolverFactory (without some prefix e.g. "contrib.cyipopt"), should remain stable. To me, it's a gray area. That said, I do like the idea of only supporting named arguments.

Copy link
Member

Choose a reason for hiding this comment

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

@Robbybp how do you want to proceed with this? Should we merge it as-is to get it into the August release and open an issue to deprecate positional arguments in favor of named arguments? Or wait and modify this PR directly?

Copy link
Member

Choose a reason for hiding this comment

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

Let's merge this as-is to get it in for the release. I created issue #3354 to track this discussion.

@blnicho blnicho merged commit dc6eccd into Pyomo:main Aug 20, 2024
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
No open projects
Development

Successfully merging this pull request may close these issues.

5 participants