Skip to main content

Crate pounce_rs

Crate pounce_rs 

Source
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:

featuremodulewhat it covers
convexconvexLP, convex QP, SOCP / exponential / power / PSD cones, SOS; batched and warm-started solves; QP sensitivity and reduced Hessian
qpqp, sqpsparse parametric active-set QP — the SQP / MPC / continuation engine, indefinite Hessians allowed — plus the SQP working-set warm-start contract
sensitivitysensitivitysIPOPT-style NLP sensitivity: ∂x*/∂p predictors, parametric warm starts, reduced Hessian
fullall 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 <= 5
use 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 iteration

For 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 TNLP trait.
convex
LP, convex QP, and conic programming — the pounce-convex interior-point path, re-exported (feature convex).
linsol
The sparse symmetric linear-solver backend the QP entry points need.
prelude
The common case in one glob import. Brings in the ergonomic Problem trait + Nlp builder, plus the low-level TNLP surface and the IpoptApplication driver for full control.
qp
Sparse parametric active-set QP — the pounce-qp engine behind the active-set SQP path, re-exported (feature qp).
sensitivity
NLP sensitivity, parametric warm starts, and reduced Hessians — the pounce-sensitivity port of Ipopt’s sIPOPT, re-exported (feature sensitivity).
sqp
Active-set SQP warm starting — the working-set contract, re-exported (feature qp).

Structs§

BoundsInfo
Bound-data target buffers passed into TNLP::get_bounds_info.
CollectorScope
Opaque RAII scope guaranteeing IterCollectorLayer is active on this thread until drop.
IpoptApplication
IpoptCq
Forward-declared placeholder for IpoptCalculatedQuantities. Phase 5 fills this in.
IpoptData
Forward-declared placeholder for IpoptData. Phase 5 fills this in with the full mutable iterate-state structure; for Phase 3 it is opaque.
IterCaptureGuard
RAII activation of per-iteration capture for one solve.
IterRecord
One row of per-iteration data — same numbers that IpoptAlgorithm prints to stdout each iteration (the “iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls” line). Captured into SolveStatistics::iterations when a JSON / programmatic consumer needs the trajectory rather than just the final state.
IterStats
Per-iteration callback payload for TNLP::intermediate_callback.
MetaData
Variable / constraint metadata buckets, mirroring upstream’s (StringMetaDataMapType, IntegerMetaDataMapType, NumericMetaDataMapType).
NlpInfo
Problem dimensions returned by TNLP::get_nlp_info.
ScalingRequest
Scaling-factor target buffers passed into TNLP::get_scaling_parameters.
ScopedIterCapture
IterCaptureGuard bundled with a collector_scope subscriber install: everything needed to capture one solve’s iteration history on this thread, with no tracing wiring on the caller’s side.
Solution
Solution as passed to TNLP::finalize_solution.
SolveStatistics
StartingPoint
Starting-point target buffers passed into TNLP::get_starting_point. Each init_* flag matches upstream — mostly false unless warm-starting.

Enums§

AlgorithmMode
Mirrors enum AlgorithmMode. Exposed in intermediate_callback.
ApplicationReturnStatus
Mirrors enum ApplicationReturnStatus.
IndexStyle
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::LinearityType upstream.
SparsityRequest
Mode discriminator for the structure / values calls of TNLP::eval_jac_g and TNLP::eval_h. Replaces upstream’s iRow != NULL heuristic.

Traits§

TNLP
User-facing NLP interface — port of class TNLP. Object-safe.

Functions§

collector_scope
Ensure IterCollectorLayer is 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 f with iteration capture active on this thread, returning its result alongside the recorded trajectory.

Type Aliases§

Index
Signed index — Index in Ipopt. Held at 32 bits for ABI parity with MUMPS, MA27, etc.
Number
Floating-point scalar — Number in Ipopt.