pounce_sensitivity/lib.rs
1//! Sensitivity analysis for POUNCE — port of upstream Ipopt's `contrib/sIPOPT/`.
2//!
3//! # Status
4//!
5//! Phases A–C complete. Wired today:
6//!
7//! * [`schur_data::IndexSchurData`] + [`p_calculator::IndexPCalculator`]:
8//! row-selector representation of the perturbation matrix `B`.
9//! * [`backsolver::DenseLuBacksolver`] + [`PdSensBacksolver`]: backsolves
10//! against the converged KKT factor (test / live IPM, respectively).
11//! * [`schur_driver::DenseGenSchurDriver`]: dense Schur-complement
12//! factor `S = -B K⁻¹ Bᵀ` with parallel right-hand-side solves.
13//! * [`step_calc::StdStepCalc`] + [`sens_app::SensApplication`]:
14//! high-level `parametric_step(Δp, dx)` and
15//! [`reduced_hessian::compute_reduced_hessian`] entry points.
16//! * [`SensSolve`] / [`SensResult`]: one-call builder (covers the
17//! `on_converged` plumbing typically required to wire the above into
18//! an `IpoptApplication`).
19//!
20//! Verified against upstream sIPOPT 3.14.19's `parametric_cpp` golden
21//! output to 1e-8 (see `tests/parametric_cpp.rs`); the standalone
22//! `pounce_sens` AMPL driver in `pounce-cli` matches `sensitivity_amplsolver`'s
23//! `_sens_sol` output on representative .nl problems.
24//!
25//! **Phase D progress** (per [pounce#7](https://github.com/jkitchin/pounce/issues/7)):
26//!
27//! * **Fixed-variable lifting** ✔ — `pounce_sens` handles `n_x != n_full`
28//! via the `IpoptNlp::full_x_to_var_x` / `var_x_to_full_x` /
29//! `full_g_to_c_block` trait methods (which delegate to
30//! `BoundClassification.x_not_fixed_map` / `c_map`).
31//! * **Reduced-Hessian eigendecomposition** ✔ — pure-Rust cyclic Jacobi
32//! in [`pounce_linalg::symmetric_eigen`] (shared with the convex QP
33//! sensitivity path); surfaced via
34//! [`SensApplication::compute_reduced_hessian_eigen`],
35//! [`SensSolve::with_reduced_hessian_eigen`], the `pounce_sens
36//! --rh-eigendecomp` flag, and the Python `solve_with_sens(rh_eigendecomp=True)`
37//! kwarg.
38//! * **`sens_boundcheck` bound refinement** ✔ —
39//! [`boundcheck::refine_step_onto_bounds`] repairs the active set the
40//! step implies, both halves of upstream's fix-relax: a coordinate
41//! the step carries past a bound is pinned AT that bound, and a bound
42//! multiplier the step drives negative is set to zero so the variable
43//! can leave. Either way the system is re-solved, so the other
44//! coordinates move with it. Surfaced via
45//! [`SensSolve::with_boundcheck`], `pounce_sens --sens-boundcheck`,
46//! the Python `solve_with_sens(sens_boundcheck=True)` kwarg, and
47//! `estimate(mode="fix_relax")` in pyomo-pounce, all four running the
48//! same refinement.
49//!
50//! # Algorithmic reference
51//!
52//! Pirnay, H., López-Negrete, R., and Biegler, L.T. (2012).
53//! *Optimal sensitivity based on IPOPT.*
54//! Mathematical Programming Computation, **4**(4), 307–331.
55//! [DOI: 10.1007/s12532-012-0043-2](https://doi.org/10.1007/s12532-012-0043-2).
56//!
57//! Verified 2026-05-14 via Crossref: title, authors (Hans Pirnay; Rodrigo
58//! López-Negrete; Lorenz T. Biegler), MPC volume 4 issue 4 pp 307–331.
59//!
60//! # Upstream source mirror
61//!
62//! Port targets `ref/Ipopt/contrib/sIPOPT/src/` in this repo
63//! (EPL-2.0, © Hans Pirnay 2009–2011 per the file headers). Each
64//! public item in this crate documents the upstream symbol it mirrors
65//! with file path and (where stable) line numbers.
66
67#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
68
69pub mod activity;
70pub mod algorithm_backsolver;
71pub mod convenience;
72pub mod corrector;
73pub mod diff_handoff;
74pub mod index;
75pub mod options;
76pub mod solver;
77mod vec_util;
78
79/// The engine-agnostic core, for anything not surfaced below.
80///
81/// The half of this crate that does not know which solver produced the KKT
82/// system — the `SensBacksolver` contract, `boundcheck`'s fix-relax / path /
83/// directional machinery, and the Schur-complement stack — lives in
84/// `pounce-sens-core` so the convex arm can reach it without pulling in the
85/// NLP engine.
86pub use pounce_sens_core;
87
88// Re-exporting the *modules*, not merely their items, is what keeps this
89// crate's published API unchanged across that move: a `pub use` of a module
90// creates a valid path at that name, so
91// `pounce_sensitivity::boundcheck::refine_step_onto_bounds` still resolves for
92// `pounce-cli` and for this crate's own tests, and the internal
93// `crate::backsolver::SensBacksolver` spellings in `solver.rs`, `activity.rs`
94// and `corrector.rs` needed no edit at all.
95pub use pounce_sens_core::{
96 backsolver, boundcheck, p_calculator, reduced_hessian, schur_data, schur_driver, sens_app,
97 step_calc,
98};
99
100pub use algorithm_backsolver::PdSensBacksolver;
101pub use convenience::{SensResult, SensSolve};
102pub use diff_handoff::{DEFAULT_ACTIVE_TOL, DiffHandoff};
103pub use options::{
104 DEFAULT_SENS_BOUND_EPS, SensOptionOverrides, pdpert_verdict, release_floor_from_options,
105};
106pub use pounce_sens_core::backsolver::{DenseLuBacksolver, SensBacksolver};
107pub use pounce_sens_core::p_calculator::{IndexPCalculator, PCalculator};
108pub use pounce_sens_core::reduced_hessian::compute_reduced_hessian;
109pub use pounce_sens_core::schur_data::{IndexSchurData, SchurData};
110pub use pounce_sens_core::schur_driver::{DenseGenSchurDriver, SchurDriver};
111pub use pounce_sens_core::sens_app::{SensApplication, SensOptions, register_options};
112pub use pounce_sens_core::step_calc::{SensStepCalc, StdStepCalc, WithBacksolver};
113// Hoisted to pounce-linalg so the convex QP sensitivity path can share it;
114// re-exported here to preserve `pounce_sensitivity::symmetric_eigen`.
115pub use pounce_linalg::symmetric_eigen;
116pub use solver::{ConvergedState, Solver, SolverError};
117
118/// Run a sensitivity-producing solve in the original TNLP coordinate system.
119///
120/// Presolve can reduce or reorder the KKT system, while sIPOPT pin indices and
121/// reduced-Hessian coordinates are defined against the submitted TNLP. Keep
122/// the public `presolve` option intact for callers, but bypass its generic
123/// wrapper for this solve.
124pub(crate) fn optimize_tnlp_for_sensitivity(
125 app: &mut pounce_algorithm::IpoptApplication,
126 tnlp: std::rc::Rc<std::cell::RefCell<dyn pounce_nlp::TNLP>>,
127) -> pounce_nlp::return_codes::ApplicationReturnStatus {
128 let presolve_enabled = app
129 .options()
130 .get_bool_value("presolve", "")
131 .ok()
132 .map(|(value, _)| value)
133 .unwrap_or(false);
134 if presolve_enabled {
135 tracing::warn!(
136 target: "pounce::sensitivity",
137 "disabling generic presolve for sensitivity / reduced-Hessian analysis; \
138 its KKT coordinates must match the original TNLP"
139 );
140 }
141 app.optimize_tnlp_without_presolve(tnlp)
142}