use crate::backsolver::SensBacksolver;
use crate::eigen::symmetric_eigen;
use crate::p_calculator::IndexPCalculator;
use crate::reduced_hessian::compute_reduced_hessian;
use crate::schur_data::{IndexSchurData, SchurData};
use crate::schur_driver::{DenseGenSchurDriver, SchurDriver};
use crate::step_calc::{SensStepCalc, StdStepCalc};
use pounce_common::types::Number;
pub struct SensApplication<B: SensBacksolver> {
a_data: IndexSchurData,
backsolver: B,
options: SensOptions,
}
#[derive(Debug, Clone, Copy)]
pub struct SensOptions {
pub compute_red_hessian: bool,
pub run_sens: bool,
pub n_sens_steps: i32,
pub obj_scal: Number,
pub rh_eigendecomp: bool,
}
impl Default for SensOptions {
fn default() -> Self {
Self {
compute_red_hessian: false,
run_sens: false,
n_sens_steps: 1,
obj_scal: 1.0,
rh_eigendecomp: false,
}
}
}
impl<B: SensBacksolver> SensApplication<B> {
pub fn new(a_data: IndexSchurData, backsolver: B, options: SensOptions) -> Self {
Self {
a_data,
backsolver,
options,
}
}
pub fn compute_reduced_hessian(&mut self, out: &mut [Number]) -> bool
where
B: Clone,
{
let backsolver = self.backsolver.clone();
let mut pcalc = IndexPCalculator::new(backsolver, self.a_data.clone());
compute_reduced_hessian(&mut pcalc, &self.a_data, self.options.obj_scal, out)
}
pub fn compute_reduced_hessian_eigen(
&mut self,
hr_out: &mut [Number],
eigvals_out: &mut [Number],
eigvecs_out: &mut [Number],
) -> bool
where
B: Clone,
{
let n = self.a_data.nrows() as usize;
if hr_out.len() != n * n || eigvals_out.len() != n || eigvecs_out.len() != n * n {
return false;
}
if !self.compute_reduced_hessian(hr_out) {
return false;
}
symmetric_eigen(hr_out, n, eigvals_out, eigvecs_out)
}
pub fn run_sens_step(
&mut self,
b_data: &IndexSchurData,
rhs_u: &[Number],
du: &mut [Number],
dx_full: &mut [Number],
) -> bool
where
B: Clone,
{
let backsolver = self.backsolver.clone();
let pcalc = IndexPCalculator::new(backsolver, self.a_data.clone());
let mut driver = DenseGenSchurDriver::<_, B>::new(pcalc);
if !driver.schur_build_and_factor(b_data) {
return false;
}
let step = StdStepCalc::new(&driver, driver.pcalc());
step.compute_step(rhs_u, du, dx_full)
}
pub fn parametric_step(&self, delta_p: &[Number], dx_full: &mut [Number]) -> bool {
let n_full = self.backsolver.dim();
if dx_full.len() != n_full {
return false;
}
if delta_p.len() != self.a_data.nrows() as usize {
return false;
}
let mut rhs_full = vec![0.0; n_full];
if self.a_data.trans_multiply(delta_p, &mut rhs_full).is_err() {
return false;
}
self.backsolver.solve(&rhs_full, dx_full)
}
pub fn options(&self) -> &SensOptions {
&self.options
}
}
pub fn register_options(
r: &pounce_common::reg_options::RegisteredOptions,
) -> Result<(), pounce_common::exception::SolverException> {
r.set_registering_category("sIPOPT");
r.add_lower_bounded_integer_option(
"n_sens_steps",
"Number of sensitivity steps to perform per converged solve.",
0,
1,
"Number of parameter perturbations to step through. Mirrors upstream `n_sens_steps` (SensApplication.cpp:60).",
)?;
r.add_bool_option(
"compute_red_hessian",
"Compute the reduced Hessian at the converged solution.",
false,
"When set, after the IPM converges pounce-sensitivity assembles `H_R = obj_scal · B K⁻¹ Bᵀ` with B selecting the free variables. Output is written to the user via the sIPOPT C ABI (Phase D follow-up). Mirrors upstream `compute_red_hessian` (SensApplication.cpp:73).",
)?;
r.add_bool_option(
"run_sens",
"Run the sensitivity step calc after convergence.",
false,
"When set, pounce-sensitivity computes a forward-sensitivity step for the parameter perturbation declared via TNLP suffixes. Mirrors upstream `run_sens` (SensApplication.cpp:80).",
)?;
r.add_bool_option(
"sens_boundcheck",
"Verify the sensitivity step does not violate bound multipliers.",
false,
"Mirrors upstream `sens_boundcheck` (SensApplication.cpp:63).",
)?;
r.add_lower_bounded_number_option(
"sens_bound_eps",
"Safety margin enforced when sens_boundcheck is set.",
0.0,
true,
1.0e-3,
"Mirrors upstream `sens_bound_eps` (SensApplication.cpp:67).",
)?;
r.add_lower_bounded_number_option(
"sens_max_pdpert",
"Maximum primal-dual perturbation accepted in the sensitivity step.",
0.0,
true,
1.0e-3,
"Mirrors upstream `sens_max_pdpert` (SensApplication.cpp:98).",
)?;
r.add_bool_option(
"rh_eigendecomp",
"Compute eigendecomposition of the reduced Hessian.",
false,
"Mirrors upstream `rh_eigendecomp` (SensReducedHessianCalculator.cpp:38). When set together with `compute_red_hessian=yes`, pounce-sensitivity returns the eigenvalues and eigenvectors of `H_R` alongside the matrix itself (cyclic Jacobi rotation, pure Rust).",
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backsolver::DenseLuBacksolver;
use crate::schur_data::IndexSchurData;
#[test]
fn sens_application_computes_reduced_hessian() {
#[rustfmt::skip]
let k = vec![
2.0, -1.0, 0.0,
-1.0, 2.0, -1.0,
0.0, -1.0, 2.0,
];
let backsolver = DenseLuBacksolver::from_dense(3, &k).unwrap();
let a = IndexSchurData::from_parts(vec![0, 2], vec![1, 1]).unwrap();
let opts = SensOptions {
compute_red_hessian: true,
obj_scal: 1.0,
..SensOptions::default()
};
let mut app = SensApplication::new(a, backsolver, opts);
let mut hr = vec![0.0; 4];
assert!(app.compute_reduced_hessian(&mut hr));
assert!((hr[0] - 0.75).abs() < 1e-12);
assert!((hr[1] - 0.25).abs() < 1e-12);
assert!((hr[2] - 0.25).abs() < 1e-12);
assert!((hr[3] - 0.75).abs() < 1e-12);
}
#[test]
fn sens_application_runs_step() {
#[rustfmt::skip]
let k = vec![
2.0, -1.0, 0.0,
-1.0, 2.0, -1.0,
0.0, -1.0, 2.0,
];
let backsolver = DenseLuBacksolver::from_dense(3, &k).unwrap();
let a = IndexSchurData::from_parts(vec![0, 2], vec![1, 1]).unwrap();
let b = IndexSchurData::from_parts(vec![0, 2], vec![1, 1]).unwrap();
let opts = SensOptions {
run_sens: true,
..SensOptions::default()
};
let mut app = SensApplication::new(a, backsolver, opts);
let rhs_u = [1.0, 0.0];
let mut du = [0.0; 2];
let mut dx = [0.0; 3];
assert!(app.run_sens_step(&b, &rhs_u, &mut du, &mut dx));
assert!((du[0] - (-1.5)).abs() < 1e-10);
assert!((du[1] - 0.5).abs() < 1e-10);
assert!((dx[0] - (-1.0)).abs() < 1e-10);
assert!((dx[1] - (-0.5)).abs() < 1e-10);
assert!((dx[2] - 0.0).abs() < 1e-10);
}
#[test]
fn sens_application_computes_reduced_hessian_eigen() {
#[rustfmt::skip]
let k = vec![
2.0, -1.0, 0.0,
-1.0, 2.0, -1.0,
0.0, -1.0, 2.0,
];
let backsolver = DenseLuBacksolver::from_dense(3, &k).unwrap();
let a = IndexSchurData::from_parts(vec![0, 2], vec![1, 1]).unwrap();
let opts = SensOptions {
compute_red_hessian: true,
rh_eigendecomp: true,
..SensOptions::default()
};
let mut app = SensApplication::new(a, backsolver, opts);
let mut hr = vec![0.0; 4];
let mut w = vec![0.0; 2];
let mut v = vec![0.0; 4];
assert!(app.compute_reduced_hessian_eigen(&mut hr, &mut w, &mut v));
assert!((w[0] - 0.5).abs() < 1e-12, "eig0 = {}", w[0]);
assert!((w[1] - 1.0).abs() < 1e-12, "eig1 = {}", w[1]);
for j in 0..2 {
let v0 = v[2 * j];
let v1 = v[2 * j + 1];
let av0 = hr[0] * v0 + hr[2] * v1;
let av1 = hr[1] * v0 + hr[3] * v1;
assert!((av0 - w[j] * v0).abs() < 1e-10);
assert!((av1 - w[j] * v1).abs() < 1e-10);
}
}
#[test]
fn register_options_round_trips_through_options_list() {
let r = pounce_common::reg_options::RegisteredOptions::default();
register_options(&r).expect("registration must succeed");
}
}