Skip to main content

pounce_rs/
lib.rs

1//! # pounce-rs — solve optimization problems with POUNCE from Rust
2//!
3//! POUNCE's solver lives across several crates (`pounce-nlp` for the
4//! [`TNLP`] problem trait, `pounce-algorithm` for the [`IpoptApplication`]
5//! driver, `pounce-common` for the scalar types). This crate is a thin
6//! **facade**: it re-exports everything needed to define and solve a problem,
7//! so a Rust user depends on one crate and writes `use pounce_rs::prelude::*;`
8//! — the Rust counterpart to the one-import `import pounce` Python API.
9//!
10//! It is re-exports plus one ergonomic [`builder`] layer, and it pins a
11//! single curated public surface, so downstream code is insulated from churn
12//! in the internal crate layout.
13//!
14//! ## Feature flags — the paths beyond a single NLP solve
15//!
16//! The default build is the NLP path only. Everything else POUNCE solves is
17//! behind a feature, each landing in its own module so the `Qp*` type names
18//! of the two QP families never collide:
19//!
20//! | feature | module | what it covers |
21//! |---|---|---|
22//! | `convex` | [`convex`] | LP, convex QP, SOCP / exponential / power / PSD cones, SOS; batched and warm-started solves; QP sensitivity and reduced Hessian |
23//! | `qp` | [`qp`], [`sqp`] | sparse **parametric active-set** QP — the SQP / MPC / continuation engine, indefinite Hessians allowed — plus the SQP working-set warm-start contract |
24//! | `sensitivity` | [`sensitivity`] | sIPOPT-style NLP sensitivity: `∂x*/∂p` predictors, parametric warm starts, reduced Hessian |
25//! | `full` | — | all three |
26//!
27//! ```toml
28//! [dependencies]
29//! pounce-rs = { version = "0.9", features = ["convex", "sensitivity"] }
30//! ```
31//!
32//! `convex` and `qp` also bring in [`linsol`], which supplies the sparse
33//! symmetric factorization backend those entry points take as an argument.
34//!
35//! Enabling a feature widens what this crate *exports*; it is close to free
36//! at build time, because the default NLP path already pulls `pounce-qp`,
37//! `pounce-linsol`, and `pounce-feral` transitively. Only `convex` and
38//! `sensitivity` add crates to compile.
39//!
40//! ## Example: HS071 (Hock–Schittkowski problem 71)
41//!
42//! ```text
43//! min  x1*x4*(x1 + x2 + x3) + x3
44//! s.t. x1*x2*x3*x4 >= 25
45//!      x1^2 + x2^2 + x3^2 + x4^2 == 40
46//!      1 <= xi <= 5
47//! ```
48//!
49//! ```
50//! use pounce_rs::prelude::*;
51//! use std::cell::RefCell;
52//! use std::rc::Rc;
53//!
54//! #[derive(Default)]
55//! struct Hs071 {
56//!     obj: Option<f64>,
57//!     x: Option<[f64; 4]>,
58//! }
59//!
60//! impl TNLP for Hs071 {
61//!     fn get_nlp_info(&mut self) -> Option<NlpInfo> {
62//!         Some(NlpInfo { n: 4, m: 2, nnz_jac_g: 8, nnz_h_lag: 10, index_style: IndexStyle::C })
63//!     }
64//!
65//!     fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
66//!         b.x_l.copy_from_slice(&[1.0; 4]);
67//!         b.x_u.copy_from_slice(&[5.0; 4]);
68//!         b.g_l.copy_from_slice(&[25.0, 40.0]);          // g0 >= 25, g1 == 40
69//!         b.g_u.copy_from_slice(&[2.0e19, 40.0]);
70//!         true
71//!     }
72//!
73//!     fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
74//!         sp.x.copy_from_slice(&[1.0, 5.0, 5.0, 1.0]);
75//!         true
76//!     }
77//!
78//!     fn eval_f(&mut self, x: &[f64], _new_x: bool) -> Option<f64> {
79//!         Some(x[0] * x[3] * (x[0] + x[1] + x[2]) + x[2])
80//!     }
81//!
82//!     fn eval_grad_f(&mut self, x: &[f64], _new_x: bool, g: &mut [f64]) -> bool {
83//!         g[0] = x[3] * (2.0 * x[0] + x[1] + x[2]);
84//!         g[1] = x[0] * x[3];
85//!         g[2] = x[0] * x[3] + 1.0;
86//!         g[3] = x[0] * (x[0] + x[1] + x[2]);
87//!         true
88//!     }
89//!
90//!     fn eval_g(&mut self, x: &[f64], _new_x: bool, g: &mut [f64]) -> bool {
91//!         g[0] = x[0] * x[1] * x[2] * x[3];
92//!         g[1] = x[0] * x[0] + x[1] * x[1] + x[2] * x[2] + x[3] * x[3];
93//!         true
94//!     }
95//!
96//!     fn eval_jac_g(&mut self, x: Option<&[f64]>, _new_x: bool, mode: SparsityRequest<'_>) -> bool {
97//!         match mode {
98//!             SparsityRequest::Structure { irow, jcol } => {
99//!                 irow.copy_from_slice(&[0, 0, 0, 0, 1, 1, 1, 1]);
100//!                 jcol.copy_from_slice(&[0, 1, 2, 3, 0, 1, 2, 3]);
101//!             }
102//!             SparsityRequest::Values { values } => {
103//!                 let x = x.unwrap();
104//!                 values.copy_from_slice(&[
105//!                     x[1] * x[2] * x[3], x[0] * x[2] * x[3], x[0] * x[1] * x[3], x[0] * x[1] * x[2],
106//!                     2.0 * x[0], 2.0 * x[1], 2.0 * x[2], 2.0 * x[3],
107//!                 ]);
108//!             }
109//!         }
110//!         true
111//!     }
112//!
113//!     fn eval_h(&mut self, x: Option<&[f64]>, _new_x: bool, of: f64,
114//!               lambda: Option<&[f64]>, _new_lambda: bool, mode: SparsityRequest<'_>) -> bool {
115//!         match mode {
116//!             SparsityRequest::Structure { irow, jcol } => {
117//!                 irow.copy_from_slice(&[0, 1, 1, 2, 2, 2, 3, 3, 3, 3]);
118//!                 jcol.copy_from_slice(&[0, 0, 1, 0, 1, 2, 0, 1, 2, 3]);
119//!             }
120//!             SparsityRequest::Values { values } => {
121//!                 let x = x.unwrap();
122//!                 let l = lambda.unwrap();
123//!                 values.copy_from_slice(&[
124//!                     of * (2.0 * x[3]) + l[1] * 2.0,
125//!                     of * x[3] + l[0] * (x[2] * x[3]),
126//!                     l[1] * 2.0,
127//!                     of * x[3] + l[0] * (x[1] * x[3]),
128//!                     l[0] * (x[0] * x[3]),
129//!                     l[1] * 2.0,
130//!                     of * (2.0 * x[0] + x[1] + x[2]) + l[0] * (x[1] * x[2]),
131//!                     of * x[0] + l[0] * (x[0] * x[2]),
132//!                     of * x[0] + l[0] * (x[0] * x[1]),
133//!                     l[1] * 2.0,
134//!                 ]);
135//!             }
136//!         }
137//!         true
138//!     }
139//!
140//!     fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
141//!         self.obj = Some(sol.obj_value);
142//!         self.x = Some([sol.x[0], sol.x[1], sol.x[2], sol.x[3]]);
143//!     }
144//! }
145//!
146//! let mut app = IpoptApplication::new();
147//! app.initialize().unwrap();
148//! let prob = Rc::new(RefCell::new(Hs071::default()));
149//! let status = app.optimize_tnlp(Rc::clone(&prob) as Rc<RefCell<dyn TNLP>>);
150//!
151//! assert_eq!(status, ApplicationReturnStatus::SolveSucceeded);
152//! let obj = prob.borrow().obj.unwrap();
153//! assert!((obj - 17.014_017).abs() < 1e-4);            // known optimum
154//! ```
155//!
156//! ## Solve statistics and the iteration trajectory
157//!
158//! Every [`builder::Nlp::solve`] fills [`Solution::stats`](builder::Solution)
159//! with the solve's [`SolveStatistics`] (wall time, iteration count,
160//! evaluation counts, final infeasibilities) and the solution carries the
161//! constraint values `g` and bound multipliers `z_l`/`z_u`. Opt in to the
162//! per-iteration trajectory with `.capture_iterations()`.
163//!
164//! ```
165//! use pounce_rs::prelude::*;
166//!
167//! struct Quad; // min (x0-1)^2 + (x1-2)^2  s.t. x0 + x1 == 3
168//! impl Problem for Quad {
169//!     fn objective(&self, x: &[f64]) -> f64 {
170//!         (x[0] - 1.0).powi(2) + (x[1] - 2.0).powi(2)
171//!     }
172//!     fn n_constraints(&self) -> usize {
173//!         1
174//!     }
175//!     fn constraints(&self, x: &[f64], g: &mut [f64]) {
176//!         g[0] = x[0] + x[1];
177//!     }
178//! }
179//!
180//! let sol = Nlp::new(Quad)
181//!     .var_bounds(&[0.0, 0.0], &[5.0, 5.0])
182//!     .constraint_bounds(&[3.0], &[3.0])
183//!     .capture_iterations()
184//!     .solve();
185//! assert!(sol.success);
186//! assert!(sol.stats.iteration_count > 0);
187//! assert!(sol.stats.total_wallclock_time_secs > 0.0);
188//! assert!(!sol.stats.iterations.is_empty());           // one record per iteration
189//! ```
190//!
191//! For solves outside the builder, [`with_iter_capture`] wraps any closure
192//! with capture active and returns the recorded [`IterRecord`]s alongside
193//! the closure's result. For the [`IpoptApplication`] path, install
194//! [`collector_scope`] for the duration of the solve and read the history
195//! back from `statistics()`:
196//! `let _scope = collector_scope(); app.enable_iter_history(); …`.
197
198// --- scalar types -----------------------------------------------------------
199pub use pounce_common::types::{Index, Number};
200
201// --- the problem trait and its supporting types -----------------------------
202pub use pounce_nlp::return_codes::{AlgorithmMode, ApplicationReturnStatus};
203pub use pounce_nlp::tnlp::{
204    BoundsInfo, IndexStyle, IpoptCq, IpoptData, IterStats, Linearity, MetaData, NlpInfo,
205    ScalingRequest, Solution, SparsityRequest, StartingPoint, TNLP,
206};
207
208// --- the solver driver ------------------------------------------------------
209pub use pounce_algorithm::application::IpoptApplication;
210
211// --- iteration capture & observability --------------------------------------
212// Thread-scoped helpers so an embedding library can record a solve's
213// trajectory (and turn on console logs) with no direct `tracing` deps.
214pub use pounce_nlp::solve_statistics::{IterRecord, SolveStatistics};
215pub use pounce_observability::{
216    CollectorScope, IterCaptureGuard, ScopedIterCapture, collector_scope, init_subscriber,
217    with_iter_capture,
218};
219
220// --- the underlying crates, for anything not surfaced above -----------------
221pub use pounce_algorithm;
222pub use pounce_common;
223pub use pounce_nlp;
224pub use pounce_observability;
225
226// --- ergonomic builder API (argmin-style small trait + builder; #168) -------
227pub mod builder;
228pub use builder::{Nlp, Problem, Solution as NlpSolution};
229
230// --- feature-gated facets (gh #561) -----------------------------------------
231// Each path gets its own module rather than a flat re-export: `pounce-convex`
232// and `pounce-qp` are distinct solver families that both name their types
233// `QpProblem` / `QpSolution` / `QpStatus` / `QpOptions` / `QpWarmStart`, so a
234// flat surface could not carry both.
235#[cfg(feature = "convex")]
236pub mod convex;
237#[cfg(any(feature = "convex", feature = "qp"))]
238pub mod linsol;
239#[cfg(feature = "qp")]
240pub mod qp;
241#[cfg(feature = "sensitivity")]
242pub mod sensitivity;
243// The SQP working-set contract. Flipping `algorithm` to `active-set-sqp` needs
244// no feature; carrying a working set across solves needs the `WorkingSet` type,
245// which is pounce-qp's.
246#[cfg(feature = "qp")]
247pub mod sqp;
248
249/// The common case in one glob import. Brings in the ergonomic [`Problem`]
250/// trait + [`Nlp`] builder, plus the low-level [`TNLP`] surface and the
251/// [`IpoptApplication`] driver for full control.
252///
253/// ```
254/// use pounce_rs::prelude::*;
255/// ```
256pub mod prelude {
257    pub use crate::builder::{Nlp, Problem};
258    pub use pounce_algorithm::application::IpoptApplication;
259    pub use pounce_common::types::{Index, Number};
260    pub use pounce_nlp::return_codes::ApplicationReturnStatus;
261    pub use pounce_nlp::solve_statistics::{IterRecord, SolveStatistics};
262    pub use pounce_nlp::tnlp::{
263        BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, ScalingRequest, Solution,
264        SparsityRequest, StartingPoint, TNLP,
265    };
266    pub use pounce_observability::{collector_scope, with_iter_capture};
267}