Skip to content

Add an unopy interface#483

Open
robfalck wants to merge 15 commits intomdolab:mainfrom
robfalck:unopy
Open

Add an unopy interface#483
robfalck wants to merge 15 commits intomdolab:mainfrom
robfalck:unopy

Conversation

@robfalck
Copy link
Copy Markdown
Contributor

Purpose

This pull request adds an interface to Uno to pyoptsparse.
Uno is a C++ package for nonlinear constrained optimization, with a python interface (unopy).
Uno provides another option that provides both SQP and interior-point capability depending on the settings.

Other notes about the Uno interface

  • Supports sparse jacobians
  • Supports user termination
  • Currently output is only sent to standard output, with verbosity set by the 'logger' option.

This implementation requires unopy 0.4.0 or later.

Expected time until merged

A few weeks (not urgent).

Type of change

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (non-backwards-compatible fix or feature)
  • Code style update (formatting, renaming)
  • Refactoring (no functional changes, no API changes)
  • Documentation update
  • Maintenance update
  • Other (please describe)

Testing

Running the test suite will test Uno against several existing test problems.

Checklist

  • I have run ruff check and ruff format to make sure the Python code adheres to PEP-8 and is consistently formatted
  • I have formatted the Fortran code with fprettify or C/C++ code with clang-format as applicable
  • I have run unit and regression tests which pass locally with my changes
  • I have added new tests that prove my fix is effective or that my feature works
  • I have added necessary documentation

Resolves cvanaret/Uno#318

  Implements a pyoptsparse wrapper for the UNO (Unified Nonlinear Optimizer)
  using the unopy package. Follows the pyIPOPT pattern: COO sparse Jacobian
  format, constraint reordering via getOrdering, and MPI rank-0/waitLoop split.

  unopy callbacks receive x as unopy.Vector and output arrays as
  unopy.PointerToDouble, neither of which expose the buffer protocol.
  x is converted via np.fromiter using the callback's nv count parameter.
  Output arrays are mapped to writable numpy views via a two-level ctypes
  dereference through pybind11's simple_value_holder layout, avoiding
  per-element Python/C++ round-trips for bulk array writes.
@robfalck robfalck requested a review from marcomangano as a code owner March 20, 2026 01:13
@robfalck
Copy link
Copy Markdown
Contributor Author

@cvanaret I would appreciate your review again. Theres a skipped test of problem 109 (TP109) from the Schittkowski test that I wasn't able to get to converge with Uno.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.99%. Comparing base (92cd835) to head (f75e2de).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #483   +/-   ##
=======================================
  Coverage   82.99%   82.99%           
=======================================
  Files           1        1           
  Lines         147      147           
=======================================
  Hits          122      122           
  Misses         25       25           

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

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@@ -205,11 +196,13 @@ def test_uno_informs(self):
optOptions={"time_limit": 1e-15, "logger": "INFO"})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is "time_limit": 1e-15 correct?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That was a test to make sure the solution inform indicates that the time limit was reached.

@cvanaret
Copy link
Copy Markdown

@robfalck thanks for the PR!

I have an AMPL model of hs109 and Uno indeed doesn't converge with hessian_model="LBFGS" + the default quasi_newton_memory_size=6. It looks like the Hessian approximation is too positive definite and the solver takes tiny steps.
It does converge for quasi_newton_memory_size=15 but that's just lucky.

I think I should monitor progress and flush the limited memory if the solver stalls. Note that I'm also planning to implement interior points with L-BFGS in the next few days.

Will the exact Hessian callback be available in the near future?

@robfalck
Copy link
Copy Markdown
Contributor Author

Thanks for checking. That last question is up to the mdolab folks since it's a significant change.

@cvanaret
Copy link
Copy Markdown

cvanaret commented Mar 25, 2026

unopy v0.4.1 is out with:

  • L-BFGS for interior-point methods:
uno_solver.set_preset("ipopt")
# either set the Hessian model explicitly
uno_solver.set_option("hessian_model", "LBFGS")
# or let Uno default to L-BFGS if no Hessian is provided
  • a function set_logger_stream:
uno_solver.set_logger_stream(sys.stdout)
uno_solver.optimize(model)
sys.stdout.flush()

# or

with open("log.txt", "w") as f:
    uno_solver.set_logger_stream(f)
    uno_solver.optimize(model)
    f.flush()

@marcomangano
Copy link
Copy Markdown
Collaborator

Will the exact Hessian callback be available in the near future?

It would be a very cool capability to add since most gradient-based optimizers should support that, but I don't believe that the lab (pinging @A-CGray @eirikurj ) is interested in adding this capability in the near term. Most of the (high-fidelity) codes used for engineering MDAO do not provide second derivatives.

I haven't thought this through yet but I think it would require a change in API of the _masterFunc and _masterFunc2 callback function wrappers and some non-naive way to store sparsity information, so it would not be a trivial implementation. I also know that @ewu63 has been considering some major code refactoring at some point, so this change could fall under that bigger effort.

@A-CGray
Copy link
Copy Markdown
Member

A-CGray commented Mar 26, 2026

Will the exact Hessian callback be available in the near future?

It would be a very cool capability to add since most gradient-based optimizers should support that, but I don't believe that the lab (pinging @A-CGray @eirikurj ) is interested in adding this capability in the near term. Most of the (high-fidelity) codes used for engineering MDAO do not provide second derivatives.

I wouldn't complain if somebody else implemented it, but we have no plans to. We are rarely (if ever) solving problems where the Hessian is computable in any practical sense. If somebody was going to implement this, we may as well implement the ability to use jacobian-vector products as well.

Thanks for doing this work @robfalck , I have been meaning to try out Uno for a long time, now I might actually get around to it!

@marcomangano
Copy link
Copy Markdown
Collaborator

Hi all, I am working through some testing environment issues on my side and I am currently running stuff on docker to prepare the other PR. Anyway, I kept bumping into this weird error that makes all the Uno tests fail when running testflo:

TypeError: protected_actual_reduction_macheps_coefficient is not of type int triggered on this line

Even if I make sure that the option defined in this line is actually an int, the error persists. Only if I remove the option altogether the tests actually converge. @robfalck do you have any issues like that on your side?

@cvanaret
Copy link
Copy Markdown

Hi @marcomangano, can you try 10. instead of 10?
The option should be a float but it looks like the wrong unopy overload is called.

@marcomangano
Copy link
Copy Markdown
Collaborator

marcomangano commented Mar 31, 2026

Hi @marcomangano, can you try 10. instead of 10? The option should be a float but it looks like the wrong unopy overload is called.

Ha, that fixes it, good catch! I am not very fluent in cpp, is it because 10 is read as an int by the cpp layer here? Anyway, pushing the quick fix here for now. I cannot commit directly to Rob's branch, but that is an easy fix for later.

@cvanaret
Copy link
Copy Markdown

Ha, that fixes it, good catch! I am not very fluent in cpp, is it because 10 is read as an int by the cpp layer here?

Great!
Yes, it picks the right function given the type of the argument, so with 10 it will naturally pick the integer one. A bit stringent, but this avoids implicit conversions between types.

@robfalck
Copy link
Copy Markdown
Contributor Author

robfalck commented Apr 1, 2026

Don't have access to a computer but hopefully my update today via GitHub mobile fixed it.

@cvanaret
Copy link
Copy Markdown

cvanaret commented Apr 1, 2026

unopy v0.4.2 is out. The ipopt preset with L-BFGS Hessian should be able to solve hs109.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Integrate Uno into pyOptSparse

4 participants