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` / `full_g_to_d_block` trait methods (which
30//!   delegate to `BoundClassification.x_not_fixed_map` / `full_to_c` /
31//!   `full_to_d`).
32//! * **Reduced-Hessian eigendecomposition** ✔ — pure-Rust cyclic Jacobi
33//!   in [`pounce_linalg::symmetric_eigen`] (shared with the convex QP
34//!   sensitivity path); surfaced via
35//!   [`SensApplication::compute_reduced_hessian_eigen`],
36//!   [`SensSolve::with_reduced_hessian_eigen`], the `pounce_sens
37//!   --rh-eigendecomp` flag, and the Python `solve_with_sens(rh_eigendecomp=True)`
38//!   kwarg. Over **parameter-pin rows** the decomposed matrix is `−H_R`,
39//!   so its ascending spectrum runs stiffest-first (gh#937); see
40//!   [`Solver::compute_reduced_hessian`].
41//! * **`sens_boundcheck` bound refinement** ✔ —
42//!   [`boundcheck::refine_step_onto_bounds`] repairs the active set the
43//!   step implies, both halves of upstream's fix-relax: a coordinate
44//!   the step carries past a bound is pinned AT that bound, and a bound
45//!   multiplier the step drives negative is set to zero so the variable
46//!   can leave. Either way the system is re-solved, so the other
47//!   coordinates move with it. Surfaced via
48//!   [`SensSolve::with_boundcheck`], `pounce_sens --sens-boundcheck`,
49//!   the Python `solve_with_sens(sens_boundcheck=True)` kwarg, and
50//!   `estimate(mode="fix_relax")` in pyomo-pounce, all four running the
51//!   same refinement.
52//!
53//! # Algorithmic reference
54//!
55//! Pirnay, H., López-Negrete, R., and Biegler, L.T. (2012).
56//! *Optimal sensitivity based on IPOPT.*
57//! Mathematical Programming Computation, **4**(4), 307–331.
58//! [DOI: 10.1007/s12532-012-0043-2](https://doi.org/10.1007/s12532-012-0043-2).
59//!
60//! Verified 2026-05-14 via Crossref: title, authors (Hans Pirnay; Rodrigo
61//! López-Negrete; Lorenz T. Biegler), MPC volume 4 issue 4 pp 307–331.
62//!
63//! # Upstream source mirror
64//!
65//! Port targets `ref/Ipopt/contrib/sIPOPT/src/` in this repo
66//! (EPL-2.0, © Hans Pirnay 2009–2011 per the file headers). Each
67//! public item in this crate documents the upstream symbol it mirrors
68//! with file path and (where stable) line numbers.
69
70#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
71
72pub mod activity;
73pub mod algorithm_backsolver;
74pub mod convenience;
75pub mod corrector;
76pub mod diff_handoff;
77pub mod index;
78pub mod options;
79pub mod solver;
80mod vec_util;
81
82/// The engine-agnostic core, for anything not surfaced below.
83///
84/// The half of this crate that does not know which solver produced the KKT
85/// system — the `SensBacksolver` contract, `boundcheck`'s fix-relax / path /
86/// directional machinery, and the Schur-complement stack — lives in
87/// `pounce-sens-core` so the convex arm can reach it without pulling in the
88/// NLP engine.
89pub use pounce_sens_core;
90
91// Re-exporting the *modules*, not merely their items, is what keeps this
92// crate's published API unchanged across that move: a `pub use` of a module
93// creates a valid path at that name, so
94// `pounce_sensitivity::boundcheck::refine_step_onto_bounds` still resolves for
95// `pounce-cli` and for this crate's own tests, and the internal
96// `crate::backsolver::SensBacksolver` spellings in `solver.rs`, `activity.rs`
97// and `corrector.rs` needed no edit at all.
98pub use pounce_sens_core::boundcheck::PathOperator;
99pub use pounce_sens_core::{
100    backsolver, boundcheck, p_calculator, reduced_hessian, rowlimit, schur_data, schur_driver,
101    sens_app, step_calc,
102};
103
104pub use algorithm_backsolver::PdSensBacksolver;
105pub use convenience::{SensResult, SensSolve};
106pub use diff_handoff::{DEFAULT_ACTIVE_TOL, DiffHandoff};
107pub use options::{
108    DEFAULT_SENS_BOUND_EPS, SensOptionOverrides, pdpert_verdict, release_floor_from_options,
109};
110pub use pounce_sens_core::backsolver::{DenseLuBacksolver, SensBacksolver};
111pub use pounce_sens_core::p_calculator::{IndexPCalculator, PCalculator};
112pub use pounce_sens_core::reduced_hessian::compute_reduced_hessian;
113pub use pounce_sens_core::schur_data::{IndexSchurData, SchurData};
114pub use pounce_sens_core::schur_driver::{DenseGenSchurDriver, SchurDriver};
115pub use pounce_sens_core::sens_app::{SensApplication, SensOptions, register_options};
116pub use pounce_sens_core::step_calc::{SensStepCalc, StdStepCalc, WithBacksolver};
117// Hoisted to pounce-linalg so the convex QP sensitivity path can share it;
118// re-exported here to preserve `pounce_sensitivity::symmetric_eigen`.
119pub use pounce_linalg::symmetric_eigen;
120pub use solver::{ConvergedState, Solver, SolverError};
121
122/// Run a sensitivity-producing solve in the original TNLP coordinate system.
123///
124/// Presolve can reduce or reorder the KKT system, while sIPOPT pin indices and
125/// reduced-Hessian coordinates are defined against the submitted TNLP. Keep
126/// the public `presolve` option intact for callers, but bypass its generic
127/// wrapper for this solve.
128pub(crate) fn optimize_tnlp_for_sensitivity(
129    app: &mut pounce_algorithm::IpoptApplication,
130    tnlp: std::rc::Rc<std::cell::RefCell<dyn pounce_nlp::TNLP>>,
131) -> pounce_nlp::return_codes::ApplicationReturnStatus {
132    let presolve_enabled = app
133        .options()
134        .get_bool_value("presolve", "")
135        .ok()
136        .map(|(value, _)| value)
137        .unwrap_or(false);
138    if presolve_enabled {
139        tracing::warn!(
140            target: "pounce::sensitivity",
141            "disabling generic presolve for sensitivity / reduced-Hessian analysis; \
142             its KKT coordinates must match the original TNLP"
143        );
144    }
145    app.optimize_tnlp_without_presolve(tnlp)
146}