Skip to content

Add an unopy interface#483

Merged
A-CGray merged 42 commits into
mdolab:mainfrom
robfalck:unopy
May 22, 2026
Merged

Add an unopy interface#483
A-CGray merged 42 commits into
mdolab:mainfrom
robfalck:unopy

Conversation

@robfalck
Copy link
Copy Markdown
Contributor

@robfalck robfalck commented Mar 20, 2026

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
  • Output can be sent to a stream specified by the user using 'logger_stream' option, with verbosity set by the 'logger' option.

This implementation requires unopy 0.4.7 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 (769adbc) to head (0d02fc9).

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.

Comment thread tests/test_tp109.py Outdated
@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.

@cvanaret
Copy link
Copy Markdown

The callback is available in unopy 0.4.7, see the example file.

@A-CGray
Copy link
Copy Markdown
Member

A-CGray commented May 14, 2026

Thanks @cvanaret , I've been working on getting that major iteration tracking working with that callback. I have it working but there seems to have been some change between unopy v0.4.6 and 0.4.7 that means Uno can no longer converge the rosenbrock test problem. With 0.4.6 it converges in 47 major iterations, whereas with 0.4.7 I killed the process after about 1500 major iterations without convergence. I've attached two log files from the two versions, where the 0.4.7 run was limited to 100 major iterations, I don't see any obvious differences in the algorithm settings.

Uno 046-rosenbrockOutput.log
Uno 047-rosenbrockOutput.log

@ewu63
Copy link
Copy Markdown
Collaborator

ewu63 commented May 15, 2026

I think we discussed making a conda-recipe for uno/unopy to make it a more easily-installed depdendency. @ewu63 have you had a chance to start that?

I have been away for a bit but will get back to this next week. I had started something which compiled fine locally but had trouble running it within the conda-forge docker container ecosystem, where some of the variables were not being passed in correctly for cmake to find library paths for blas etc.

@A-CGray
Copy link
Copy Markdown
Member

A-CGray commented May 15, 2026

Tests will fail until we merge the PR updating our docker images to unopy 0.4.7

@cvanaret
Copy link
Copy Markdown

Thanks @cvanaret , I've been working on getting that major iteration tracking working with that callback. I have it working but there seems to have been some change between unopy v0.4.6 and 0.4.7 that means Uno can no longer converge the rosenbrock test problem. With 0.4.6 it converges in 47 major iterations, whereas with 0.4.7 I killed the process after about 1500 major iterations without convergence. I've attached two log files from the two versions, where the 0.4.7 run was limited to 100 major iterations, I don't see any obvious differences in the algorithm settings.

Uno 046-rosenbrockOutput.log Uno 047-rosenbrockOutput.log

Ah, I changed the initialization of L-BFGS between 0.4.6 and 0.4.7. This is most likely the culprit (no wonder, it's got the number 666 😈 ). I will revert. I will also add a CI workflow that runs the pyOptSparse benchmark with the latest Uno changes, to make sure unopy doesn't regress.

@cvanaret
Copy link
Copy Markdown

Fixed in unopy v0.4.8!

@A-CGray
Copy link
Copy Markdown
Member

A-CGray commented May 20, 2026

I just merged a PR that bumps our docker images up to unopy 0.4.8, usually takes ~4 hours to run all the CI tests and push the new images, at which point the tests here should run successfully.

Comment thread tests/test_hs015.py Outdated
Comment thread tests/test_hs015.py Outdated
@ewu63
Copy link
Copy Markdown
Collaborator

ewu63 commented May 21, 2026

Just a heads up that I have managed to get unopy working in conda-forge after some back and forth. PR is here and there will be some adjustments + potential porting of patches upstream. We can coordinate the activities in that PR, and feel free to add yourselves as maintainers.

Removed convergence test that wasn't checking for proper application of settings.
Comment thread pyoptsparse/pyUno/pyUno.py
@robfalck robfalck requested a review from A-CGray May 22, 2026 12:51
A-CGray
A-CGray previously approved these changes May 22, 2026
Copy link
Copy Markdown
Member

@A-CGray A-CGray left a comment

Choose a reason for hiding this comment

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

Thanks for all your work on this @robfalck and @cvanaret , this is a great addition to pyOptSparse!

Copy link
Copy Markdown
Collaborator

@marcomangano marcomangano left a comment

Choose a reason for hiding this comment

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

Great work, thanks a lot!

@A-CGray A-CGray merged commit 812cad7 into mdolab:main May 22, 2026
14 checks passed
@robfalck
Copy link
Copy Markdown
Contributor Author

Thanks for the continued support for pyoptsparse. It really is a valuable tool. And @cvanaret, thanks for an open-source tool as capable as Uno.

It's been a pleasure collaborating with all of you on this.

@cvanaret
Copy link
Copy Markdown

Thank you all for your time! I can't wait to improve the existing Uno solvers and try out new ones.

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

5 participants