POUNCE is a pure-Rust port of the Ipopt interior-point nonlinear programming solver. It solves problems of the form
min f(x)
s.t. g_L <= g(x) <= g_U
x_L <= x <= x_U
where f and g are twice-continuously-differentiable. The algorithm,
console output, and option semantics follow upstream Ipopt closely enough
that anyone used to reading ipopt logs can drop in pounce without
relearning where the numbers live.
The default build is pure Rust — no Fortran, no HSL, no system BLAS required.
The FERAL backend provides a sparse symmetric LDLᵀ
factorization that is also in pure Rust. The HSL MA57 backend is available
behind the optional ma57 feature for users who have libcoinhsl installed.
License: EPL-2.0 (same as upstream Ipopt).
Production-ready for the core IPM workflow. The algorithm-side core,
NLP interface, line search, filter, barrier update (monotone + Mehrotra
adaptive), KKT solve, restoration phase, AMPL .nl reader, the C ABI
(pounce-cinterface), the Python wrapper (pounce-solver), and the
CLI all solve a wide range of NLPs from the standard test suites
(Hock-Schittkowski, CUTEst, Mittelmann ampl-nlp, CHO parameter
estimation, gas/water network design). Sensitivity analysis (sIPOPT
port) and reduced-Hessian computation are wired end-to-end; the
pounce-presolve pass (auxiliary-equality elimination + FBBT +
bound-tightening) and the active-set SQP path (pounce-qp-backed)
are available behind option keys.
See benchmarks/ for the comparison harness against upstream Ipopt.
The full user guide lives in docs/ as an
mdbook — installation, the CLI,
solver options, the JSON solve report, sensitivity analysis, and the
Pyomo / Python integrations. Browse the Markdown sources directly on
GitHub, or render the book locally:
make book # builds docs/book/ (requires `cargo install mdbook`)| Crate | Purpose |
|---|---|
pounce-common |
Types, exceptions, journalist, options, tagged objects, cached results (Ipopt src/Common). |
pounce-linalg |
BLAS-1, dense/compound vectors and matrices, triplet storage, CSC conversion (Ipopt src/LinAlg). |
pounce-linsol |
Symmetric linear-solver trait layer — no FFI; backends plug in below. |
pounce-feral |
Pure-Rust sparse symmetric LDLᵀ backend. Default. |
pounce-hsl |
MA57 backend via libcoinhsl (optional, behind ma57 feature). |
pounce-nlp |
TNLP trait, TNLPAdapter, IpoptApplication entry point (Ipopt src/Interfaces). |
pounce-algorithm |
IteratesVector, IpoptData, calculated quantities, KKT, line search, mu update, conv check, main loop (Ipopt src/Algorithm). |
pounce-restoration |
Restoration phase (Ipopt Algorithm/Resto*). |
pounce-presolve |
NLP preprocessing — auxiliary-equality elimination, FBBT, bound tightening, redundant-row removal. |
pounce-l1penalty |
Thierry-Biegler ℓ₁-exact penalty-barrier wrapper for degenerate / MPCC problems. |
pounce-sensitivity |
Post-optimal sensitivity + reduced-Hessian (port of upstream sIPOPT). |
pounce-qp |
Sparse parametric active-set QP subproblem solver — drives the SQP path and the sensitivity corrector. |
pounce-solve-report |
pounce.solve-report/v1 JSON writer (shared by pounce-cli --json-output and IpoptWriteSolveReport). |
pounce-observability |
tracing subscriber install + per-iteration collector layer that feeds the iteration stream into the solve report. |
pounce-cinterface |
C ABI shim — CreateIpoptProblem / IpoptSolve / FreeIpoptProblem / IpoptWriteSolveReport. |
pounce-py |
PyO3 bindings — the cyipopt-compatible pounce Python package (the pounce-solver wheel). |
pounce-cli |
The pounce command-line driver. |
pounce-studio-core |
Solve-report / iter-dump parsers and diagnostic analysis (foundation for the pounce-studio GUI / MCP server). |
pounce-studio-pyo3 |
PyO3 _native extension exposing pounce-studio-core to the pounce-studio-mcp Python MCP server. |
Prerequisites: a stable Rust toolchain. Nothing else for the default build.
make # release build of the workspace
make test # run all tests
make clippy # lint
make doc # rustdocTo build with the HSL MA57 backend (requires libcoinhsl discoverable
by the linker):
cargo build -p pounce-cli --release --features ma57make install # installs to $HOME/.local
sudo make install PREFIX=/usr/local # or system-wideThis drops the pounce binary into $PREFIX/bin and the
libpounce_cinterface shared library into $PREFIX/lib. Make sure
$HOME/.local/bin is on your PATH.
Solve an AMPL .nl file:
pounce problem.nl
pounce problem.nl print_level=8 max_iter=500 tol=1e-10
pounce problem.nl linear_solver=ma57 # with --features ma57Trailing KEY=VALUE pairs follow the same syntax and semantics as the
upstream Ipopt CLI; they override values loaded from --options-file.
List available built-in test problems:
pounce --list-problems
pounce --problem rosenbrockFull help:
pounce --helpFollowing the AMPL solver convention, solving a positional .nl file
writes a sibling <stub>.sol next to it — pounce problem.nl
produces problem.sol. The file carries the primal x and dual
lambda blocks plus an objno line with the AMPL solve_result_num,
so AMPL (or any .sol reader) can pull the solution back:
pounce problem.nl # writes problem.sol
pounce problem.nl --sol-output out.sol # write to an explicit path
pounce problem.nl --no-sol # skip the .sol writeA .sol is written even when the solve fails, so the
solve_result_num is always recoverable. Built-in problems
(--problem …) have no .nl stub, so they only produce a .sol
when --sol-output is given explicitly.
.nl files that import functions from a shared library (declared in
F segments, called via f<id> expression tokens) are supported.
Set AMPLFUNC to a newline-separated list of library paths — the
same convention upstream Ipopt uses — and pounce loads each library
through the standard AMPL funcadd_ASL ABI:
AMPLFUNC=$HOME/.idaes/bin/general_helmholtz_external.dylib \
pounce helmholtz.nlMultiple libraries: AMPLFUNC=$(printf '%s\n%s\n' /path/lib1 /path/lib2) pounce ….
Without AMPLFUNC set, problems that need external functions fail
with a clear error naming the offending function.
Because pounce speaks the AMPL NL/SOL protocol, it drops into
Pyomo through the AMPL Solver Library
interface — exactly how Pyomo drives Ipopt. The
pyomo-pounce package registers pounce as a Pyomo
SolverFactory solver:
import pyomo_pounce # registers 'pounce'
from pyomo.environ import *
solver = SolverFactory('pounce')
solver.solve(model)To invoke pounce directly as an AMPL solver, pass -AMPL
(pounce problem.nl -AMPL); in that mode the termination is conveyed
through the .sol file rather than the process exit code.
Pass --json-output PATH to write a structured solve report alongside
the regular console output:
pounce problem.nl --json-output result.json
pounce problem.nl --json-output result.json --json-detail fullThe report is FAIR-aligned (Wilkinson et al. 2016, DOI
10.1038/sdata.2016.18) — every
field documented in docs/src/schema/solve-report-v1.md.
--json-detail summary (default) emits status, primal x, dual
lambda, and aggregate statistics; --json-detail full adds the
per-iteration trajectory (iter, objective, inf_pr, inf_du, mu,
||d||, alphas, line-search trials) for debugging.
The schema is versioned (pounce.solve-report/v1) so downstream
tooling can pin against a major version. Consumers should tolerate
unknown fields — additive changes don't bump the version.
POUNCE emits diagnostics through the tracing
ecosystem. Logs go to stderr; program output (the iteration table,
summary, --dump) stays on stdout, so the two never collide when
redirected. The per-iteration table is colored when stdout is a terminal.
| Variable | Effect |
|---|---|
RUST_LOG |
Verbosity / per-target filtering (default info); e.g. RUST_LOG=pounce::linsol=debug. |
POUNCE_LOG_FORMAT |
text (default) or json — structured JSON sink on stderr for Studio / CI. |
NO_COLOR / CLICOLOR_FORCE |
Disable / force the colored iteration table. |
See docs/src/options.md and
docs/src/troubleshooting.md for details.
The pounce-sensitivity crate is a Rust port of upstream Ipopt's
contrib/sIPOPT/ (Pirnay, López-Negrete & Biegler 2012, DOI
10.1007/s12532-012-0043-2).
Three entry points cover the common workflows:
-
AMPL CLI — the main
pouncedriver auto-detects the sIPOPT suffixes (sens_state_1,sens_state_value_1,sens_init_constr) in an input.nl, runs a post-optimal sensitivity step after the solve, and writes the perturbed primal back assens_sol_state_1— no separate binary or flag needed:pounce problem.nl # writes problem.sol pounce problem.nl out.sol --json-output result.json --json-detail fullpounce_sensis retained as a thin backward-compatibility alias:pounce_sens in.nl out.solis identical topounce in.nl out.sol, so existing AMPL / solver scripts keep working unchanged. -
Rust library —
SensSolveis a builder that wraps theon_convergedcallback plumbing into a single call:use pounce_sensitivity::SensSolve; let result = SensSolve::new(vec![2, 3]) .with_deltas(vec![0.05, 0.0]) .with_reduced_hessian() .run(&mut app, tnlp); // result.dx, result.reduced_hessian, result.status
-
Python (
pounce.Problem) —solve_with_sensexposes the same capability from the cyipopt-compatible Python wrapper. Seepython/notebooks/04_sensitivity.ipynbfor a walkthrough.
All three are verified against upstream sIPOPT 3.14.19's
parametric_cpp golden output to 1e-8. Reduced-Hessian eigendecomposition
is available via --rh-eigendecomp (AMPL CLI),
SensSolve::with_reduced_hessian_eigen (Rust), and
solve_with_sens(rh_eigendecomp=True) (Python). Bound projection of the
perturbed step is available via --sens-boundcheck [--sens-bound-eps EPS]
(AMPL CLI), SensSolve::with_boundcheck(eps) (Rust), and
solve_with_sens(sens_boundcheck=True, sens_bound_eps=…) (Python). The
bound projection is a single-pass clamp; upstream's iterative Schur
refinement (re-factorize on each violation) is intentionally not ported.
For workflows that issue several follow-up operations against the converged KKT factor — sensitivity sweeps, reduced Hessians over many pinned-row sets, raw KKT back-solves — the session APIs hold the factor alive between calls:
- Python —
pounce.Solver(problem)with.solve(...),.parametric_step(...),.reduced_hessian(...),.kkt_solve(...). - C —
IpoptCreateSolver(&prob)/IpoptSolverSolve/IpoptSolverParametricStep/IpoptSolverReducedHessian/IpoptSolverKktSolve/IpoptFreeSolver. The classicIpoptSolveAPI is unchanged and unaffected. - Rust —
pounce_sensitivity::Solver; orpounce_linsol::Factorizationfor the underlying factor-only primitive (no IPM in the loop).
See docs/src/sessions.md for the full
walkthrough.
benchmarks/ contains comparison harnesses against upstream Ipopt
across several suites (CUTEst, Mittelmann ampl-nlp, CHO, electrolyte,
grid, gas, water, large-scale synthetic NLPs, GAMS nlpbench). All
suites feed a single composite report at benchmarks/BENCHMARK_REPORT.md
with provenance metadata (versions, git SHA, linear solvers).
make benchmark # full sweep: every suite + composite report
make benchmark-report # regenerate the composite report only
make benchmark-water # one suite at a time (water, gas, cutest, …)The Ipopt comparison side runs against a locally-built Ipopt-MA57
(ref/Ipopt/install-ma57/). Build it once with
make -C benchmarks build-ipopt-ma57. See benchmarks/README.md for
the full list and per-suite details.
POUNCE is a Rust port of Ipopt, the interior-point nonlinear programming solver by Andreas Wächter, Lorenz T. Biegler, and the COIN-OR community. Its algorithm, console output, and option semantics are modeled directly on that codebase, which is released under the EPL-2.0.
It is a sibling of ripopt, an earlier memory-safe interior-point NLP optimizer in Rust by the same author (doi:10.5281/zenodo.19542664).
I want to thank Carl Laird and Victor Alves for encouraging this particular development path. In ripopt I codeveloped the ipm and linear algebra solvers, which led to a plateau in progress because of the difficulty in debugging which side the problems were on. They encouraged me to instead use a good, known linear algebra library and just focus on the ipm development. In parallel, I also did the same for the linear algebra library, and separated out feral to focus only on that. This package is the union of these two efforts, and it is much more robust than ripopt is.
- Wächter, A., Biegler, L.T. "On the implementation of an interior-point filter line-search algorithm for large-scale nonlinear programming." Mathematical Programming 106(1), 25–57 (2006). doi:10.1007/s10107-004-0559-y — the algorithm POUNCE implements.
- Wächter, A., Biegler, L.T. "Line search filter methods for nonlinear programming: Motivation and global convergence." SIAM Journal on Optimization 16(1), 1–31 (2005). doi:10.1137/S1052623403426556
- Wächter, A., Biegler, L.T. "Line search filter methods for nonlinear programming: Local convergence." SIAM Journal on Optimization 16(1), 32–48 (2005). doi:10.1137/S1052623403426544
- Fletcher, R., Leyffer, S. "Nonlinear programming without a penalty function." Mathematical Programming 91(2), 239–269 (2002). doi:10.1007/s101070100244 — the filter concept underlying the line search.
- Pirnay, H., López-Negrete, R., Biegler, L.T. "Optimal sensitivity
based on IPOPT." Mathematical Programming Computation 4(4),
307–331 (2012).
doi:10.1007/s12532-012-0043-2
— the sIPOPT method behind
pounce-sensitivity. - Duff, I.S. "MA57—a code for the solution of sparse symmetric
definite and indefinite systems." ACM Transactions on Mathematical
Software 30(2), 118–144 (2004).
doi:10.1145/992200.992202
— the optional
ma57linear-solver backend.
