Expand description
§pounce-rs — solve optimization problems with POUNCE from Rust
POUNCE’s solver lives across several crates (pounce-nlp for the
TNLP problem trait, pounce-algorithm for the IpoptApplication
driver, pounce-common for the scalar types). This crate is a thin
facade: it re-exports everything needed to define and solve a problem,
so a Rust user depends on one crate and writes use pounce_rs::prelude::*;
— the Rust counterpart to the one-import import pounce Python API.
It is re-exports plus one ergonomic builder layer, and it pins a
single curated public surface, so downstream code is insulated from churn
in the internal crate layout.
§Feature flags — the paths beyond a single NLP solve
The default build is the NLP path only. Everything else POUNCE solves is
behind a feature, each landing in its own module so the Qp* type names
of the two QP families never collide:
| feature | module | what it covers |
|---|---|---|
convex | convex | LP, convex QP, SOCP / exponential / power / PSD cones, SOS; batched and warm-started solves; QP sensitivity and reduced Hessian |
qp | qp, sqp | sparse parametric active-set QP — the SQP / MPC / continuation engine, indefinite Hessians allowed — plus the SQP working-set warm-start contract |
sensitivity | sensitivity | sIPOPT-style NLP sensitivity: ∂x*/∂p predictors, parametric warm starts, reduced Hessian |
full | — | all three |
[dependencies]
pounce-rs = { version = "0.9", features = ["convex", "sensitivity"] }convex and qp also bring in linsol, which supplies the sparse
symmetric factorization backend those entry points take as an argument.
Enabling a feature widens what this crate exports; it is close to free
at build time, because the default NLP path already pulls pounce-qp,
pounce-linsol, and pounce-feral transitively. Only convex and
sensitivity add crates to compile.
§Example: HS071 (Hock–Schittkowski problem 71)
min x1*x4*(x1 + x2 + x3) + x3
s.t. x1*x2*x3*x4 >= 25
x1^2 + x2^2 + x3^2 + x4^2 == 40
1 <= xi <= 5use pounce_rs::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Default)]
struct Hs071 {
obj: Option<f64>,
x: Option<[f64; 4]>,
}
impl TNLP for Hs071 {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo { n: 4, m: 2, nnz_jac_g: 8, nnz_h_lag: 10, index_style: IndexStyle::C })
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l.copy_from_slice(&[1.0; 4]);
b.x_u.copy_from_slice(&[5.0; 4]);
b.g_l.copy_from_slice(&[25.0, 40.0]); // g0 >= 25, g1 == 40
b.g_u.copy_from_slice(&[2.0e19, 40.0]);
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
sp.x.copy_from_slice(&[1.0, 5.0, 5.0, 1.0]);
true
}
fn eval_f(&mut self, x: &[f64], _new_x: bool) -> Option<f64> {
Some(x[0] * x[3] * (x[0] + x[1] + x[2]) + x[2])
}
fn eval_grad_f(&mut self, x: &[f64], _new_x: bool, g: &mut [f64]) -> bool {
g[0] = x[3] * (2.0 * x[0] + x[1] + x[2]);
g[1] = x[0] * x[3];
g[2] = x[0] * x[3] + 1.0;
g[3] = x[0] * (x[0] + x[1] + x[2]);
true
}
fn eval_g(&mut self, x: &[f64], _new_x: bool, g: &mut [f64]) -> bool {
g[0] = x[0] * x[1] * x[2] * x[3];
g[1] = x[0] * x[0] + x[1] * x[1] + x[2] * x[2] + x[3] * x[3];
true
}
fn eval_jac_g(&mut self, x: Option<&[f64]>, _new_x: bool, mode: SparsityRequest<'_>) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
irow.copy_from_slice(&[0, 0, 0, 0, 1, 1, 1, 1]);
jcol.copy_from_slice(&[0, 1, 2, 3, 0, 1, 2, 3]);
}
SparsityRequest::Values { values } => {
let x = x.unwrap();
values.copy_from_slice(&[
x[1] * x[2] * x[3], x[0] * x[2] * x[3], x[0] * x[1] * x[3], x[0] * x[1] * x[2],
2.0 * x[0], 2.0 * x[1], 2.0 * x[2], 2.0 * x[3],
]);
}
}
true
}
fn eval_h(&mut self, x: Option<&[f64]>, _new_x: bool, of: f64,
lambda: Option<&[f64]>, _new_lambda: bool, mode: SparsityRequest<'_>) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
irow.copy_from_slice(&[0, 1, 1, 2, 2, 2, 3, 3, 3, 3]);
jcol.copy_from_slice(&[0, 0, 1, 0, 1, 2, 0, 1, 2, 3]);
}
SparsityRequest::Values { values } => {
let x = x.unwrap();
let l = lambda.unwrap();
values.copy_from_slice(&[
of * (2.0 * x[3]) + l[1] * 2.0,
of * x[3] + l[0] * (x[2] * x[3]),
l[1] * 2.0,
of * x[3] + l[0] * (x[1] * x[3]),
l[0] * (x[0] * x[3]),
l[1] * 2.0,
of * (2.0 * x[0] + x[1] + x[2]) + l[0] * (x[1] * x[2]),
of * x[0] + l[0] * (x[0] * x[2]),
of * x[0] + l[0] * (x[0] * x[1]),
l[1] * 2.0,
]);
}
}
true
}
fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
self.obj = Some(sol.obj_value);
self.x = Some([sol.x[0], sol.x[1], sol.x[2], sol.x[3]]);
}
}
let mut app = IpoptApplication::new();
app.initialize().unwrap();
let prob = Rc::new(RefCell::new(Hs071::default()));
let status = app.optimize_tnlp(Rc::clone(&prob) as Rc<RefCell<dyn TNLP>>);
assert_eq!(status, ApplicationReturnStatus::SolveSucceeded);
let obj = prob.borrow().obj.unwrap();
assert!((obj - 17.014_017).abs() < 1e-4); // known optimum§Solve statistics and the iteration trajectory
Every builder::Nlp::solve fills Solution::stats
with the solve’s SolveStatistics (wall time, iteration count,
evaluation counts, final infeasibilities) and the solution carries the
constraint values g and bound multipliers z_l/z_u. Opt in to the
per-iteration trajectory with .capture_iterations().
use pounce_rs::prelude::*;
struct Quad; // min (x0-1)^2 + (x1-2)^2 s.t. x0 + x1 == 3
impl Problem for Quad {
fn objective(&self, x: &[f64]) -> f64 {
(x[0] - 1.0).powi(2) + (x[1] - 2.0).powi(2)
}
fn n_constraints(&self) -> usize {
1
}
fn constraints(&self, x: &[f64], g: &mut [f64]) {
g[0] = x[0] + x[1];
}
}
let sol = Nlp::new(Quad)
.var_bounds(&[0.0, 0.0], &[5.0, 5.0])
.constraint_bounds(&[3.0], &[3.0])
.capture_iterations()
.solve();
assert!(sol.success);
assert!(sol.stats.iteration_count > 0);
assert!(sol.stats.total_wallclock_time_secs > 0.0);
assert!(!sol.stats.iterations.is_empty()); // one record per iterationFor solves outside the builder, with_iter_capture wraps any closure
with capture active and returns the recorded IterRecords alongside
the closure’s result. For the IpoptApplication path, install
collector_scope for the duration of the solve and read the history
back from statistics():
let _scope = collector_scope(); app.enable_iter_history(); ….
Re-exports§
pub use builder::Nlp;pub use builder::Problem;pub use builder::Solution as NlpSolution;pub use pounce_algorithm;pub use pounce_common;pub use pounce_nlp;pub use pounce_observability;
Modules§
- builder
- Ergonomic builder API over the
TNLPtrait. - convex
- LP, convex QP, and conic programming — the
pounce-convexinterior-point path, re-exported (featureconvex). - linsol
- The sparse symmetric linear-solver backend the QP entry points need.
- prelude
- The common case in one glob import. Brings in the ergonomic
Problemtrait +Nlpbuilder, plus the low-levelTNLPsurface and theIpoptApplicationdriver for full control. - qp
- Sparse parametric active-set QP — the
pounce-qpengine behind the active-set SQP path, re-exported (featureqp). - sensitivity
- NLP sensitivity, parametric warm starts, and reduced Hessians — the
pounce-sensitivityport of Ipopt’s sIPOPT, re-exported (featuresensitivity). - sqp
- Active-set SQP warm starting — the working-set contract, re-exported
(feature
qp).
Structs§
- Bounds
Info - Bound-data target buffers passed into
TNLP::get_bounds_info. - Collector
Scope - Opaque RAII scope guaranteeing
IterCollectorLayeris active on this thread until drop. - Ipopt
Application - IpoptCq
- Forward-declared placeholder for
IpoptCalculatedQuantities. Phase 5 fills this in. - Ipopt
Data - Forward-declared placeholder for
IpoptData. Phase 5 fills this in with the full mutable iterate-state structure; for Phase 3 it is opaque. - Iter
Capture Guard - RAII activation of per-iteration capture for one solve.
- Iter
Record - One row of per-iteration data — same numbers that
IpoptAlgorithmprints to stdout each iteration (the “iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls” line). Captured intoSolveStatistics::iterationswhen a JSON / programmatic consumer needs the trajectory rather than just the final state. - Iter
Stats - Per-iteration callback payload for
TNLP::intermediate_callback. - Meta
Data - Variable / constraint metadata buckets, mirroring upstream’s
(StringMetaDataMapType, IntegerMetaDataMapType, NumericMetaDataMapType). - NlpInfo
- Problem dimensions returned by
TNLP::get_nlp_info. - Scaling
Request - Scaling-factor target buffers passed into
TNLP::get_scaling_parameters. - Scoped
Iter Capture IterCaptureGuardbundled with acollector_scopesubscriber install: everything needed to capture one solve’s iteration history on this thread, with notracingwiring on the caller’s side.- Solution
- Solution as passed to
TNLP::finalize_solution. - Solve
Statistics - Starting
Point - Starting-point target buffers passed into
TNLP::get_starting_point. Eachinit_*flag matches upstream — mostly false unless warm-starting.
Enums§
- Algorithm
Mode - Mirrors
enum AlgorithmMode. Exposed inintermediate_callback. - Application
Return Status - Mirrors
enum ApplicationReturnStatus. - Index
Style - Index style for triplet I/O. Mirrors
TNLP::IndexStyleEnum.Fortran(1-based) is what MUMPS / HSL want directly;C(0-based) is more natural for Rust user code. - Linearity
- Linearity tags. Mirrors
TNLP::LinearityTypeupstream. - Sparsity
Request - Mode discriminator for the structure / values calls of
TNLP::eval_jac_gandTNLP::eval_h. Replaces upstream’siRow != NULLheuristic.
Traits§
- TNLP
- User-facing NLP interface — port of
class TNLP. Object-safe.
Functions§
- collector_
scope - Ensure
IterCollectorLayeris active on this thread until the returned guard drops. - init_
subscriber - Install the global tracing subscriber for a normal run. Idempotent
(
try_init): safe to call from multiple frontends or repeated Python imports. - with_
iter_ capture - Run
fwith iteration capture active on this thread, returning its result alongside the recorded trajectory.