Skip to main content

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}