use std::cell::RefCell;
use std::rc::Rc;
use pounce_algorithm::application::IpoptApplication;
use pounce_common::types::{Index, Number};
use pounce_nlp::return_codes::ApplicationReturnStatus;
use pounce_nlp::tnlp::{
BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, Solution, SparsityRequest, StartingPoint,
TNLP,
};
use pounce_sensitivity::{
IndexSchurData, PdSensBacksolver, SensApplication, SensBacksolver, SensOptions,
};
struct ParametricTNLP {
nominal_eta1: Number,
nominal_eta2: Number,
}
impl ParametricTNLP {
fn new(eta1: Number, eta2: Number) -> Self {
Self {
nominal_eta1: eta1,
nominal_eta2: eta2,
}
}
}
impl TNLP for ParametricTNLP {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: 5,
m: 4,
nnz_jac_g: 10,
nnz_h_lag: 5,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
for k in 0..3 {
b.x_l[k] = 0.0;
b.x_u[k] = 1.0e19;
}
b.x_l[3] = -1.0e19;
b.x_u[3] = 1.0e19;
b.x_l[4] = -1.0e19;
b.x_u[4] = 1.0e19;
b.g_l[0] = 0.0;
b.g_u[0] = 0.0;
b.g_l[1] = 0.0;
b.g_u[1] = 0.0;
b.g_l[2] = self.nominal_eta1;
b.g_u[2] = self.nominal_eta1;
b.g_l[3] = self.nominal_eta2;
b.g_u[3] = self.nominal_eta2;
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
sp.x[0] = 0.15;
sp.x[1] = 0.15;
sp.x[2] = 0.0;
sp.x[3] = 0.0;
sp.x[4] = 0.0;
true
}
fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
Some(x[0] * x[0] + x[1] * x[1] + x[2] * x[2])
}
fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
g[0] = 2.0 * x[0];
g[1] = 2.0 * x[1];
g[2] = 2.0 * x[2];
g[3] = 0.0;
g[4] = 0.0;
true
}
fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
let (x1, x2, x3, eta1, eta2) = (x[0], x[1], x[2], x[3], x[4]);
g[0] = 6.0 * x1 + 3.0 * x2 + 2.0 * x3 - eta1;
g[1] = eta2 * x1 + x2 - x3 - 1.0;
g[2] = eta1;
g[3] = eta2;
true
}
fn eval_jac_g(
&mut self,
x: Option<&[Number]>,
_new_x: bool,
mode: SparsityRequest<'_>,
) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
let rs: [Index; 10] = [0, 0, 0, 0, 1, 1, 1, 1, 2, 3];
let cs: [Index; 10] = [0, 1, 2, 3, 0, 1, 2, 4, 3, 4];
irow.copy_from_slice(&rs);
jcol.copy_from_slice(&cs);
}
SparsityRequest::Values { values } => {
let x = x.expect("eval_jac_g(Values) without x");
values[0] = 6.0;
values[1] = 3.0;
values[2] = 2.0;
values[3] = -1.0;
values[4] = x[4]; values[5] = 1.0;
values[6] = -1.0;
values[7] = x[0]; values[8] = 1.0;
values[9] = 1.0;
}
}
true
}
fn eval_h(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
obj_factor: Number,
lambda: Option<&[Number]>,
_new_lambda: bool,
mode: SparsityRequest<'_>,
) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
let rs: [Index; 5] = [0, 1, 2, 4, 0];
let cs: [Index; 5] = [0, 1, 2, 0, 0];
irow.copy_from_slice(&rs);
jcol.copy_from_slice(&cs);
}
SparsityRequest::Values { values } => {
let lam = lambda.expect("eval_h(Values) without lambda");
values[0] = 2.0 * obj_factor;
values[1] = 2.0 * obj_factor;
values[2] = 2.0 * obj_factor;
values[3] = lam[1];
values[4] = 0.0;
}
}
true
}
fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
}
fn solve_at(eta1: Number, eta2: Number) -> [Number; 5] {
let mut app = IpoptApplication::new();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.options_mut()
.set_string_value("sb", "yes", true, false)
.unwrap();
app.initialize().unwrap();
let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(ParametricTNLP::new(eta1, eta2)));
let captured: Rc<RefCell<Option<[Number; 5]>>> = Rc::new(RefCell::new(None));
let cap_for_cb = Rc::clone(&captured);
app.set_on_converged(Box::new(move |data, _cq, _nlp, _pd| {
let curr = data.borrow().curr.clone().expect("curr at convergence");
let dx = curr
.x
.as_any()
.downcast_ref::<pounce_linalg::dense_vector::DenseVector>()
.expect("x is dense");
let v = dx.expanded_values();
let mut out = [0.0; 5];
out.copy_from_slice(&v[..5]);
*cap_for_cb.borrow_mut() = Some(out);
}));
let status = app.optimize_tnlp(tnlp);
assert!(
matches!(
status,
ApplicationReturnStatus::SolveSucceeded
| ApplicationReturnStatus::SolvedToAcceptableLevel
),
"solve_at({eta1}, {eta2}) failed: {status:?}",
);
let out = captured.borrow().expect("on_converged fired");
out
}
const UPSTREAM_X_PERTURBED_NOBC: [Number; 5] = [
0.576_530_601_168_321_9,
0.377_551_038_130_684_8,
-0.045_918_360_700_993_31,
4.500_000_000_000_000,
1.000_000_000_000_000,
];
const UPSTREAM_X_NOMINAL: [Number; 5] = [
0.632_653_057_519_998_2,
0.387_755_107_968_002_7,
0.020_408_165_488_001_08,
5.000_000_000_000_000,
1.000_000_000_000_000,
];
fn run_sensitivity_step(delta_p: [Number; 2]) -> [Number; 5] {
let dx_full_out: Rc<RefCell<Option<Vec<Number>>>> = Rc::new(RefCell::new(None));
let dx_full_clone = Rc::clone(&dx_full_out);
let mut app = IpoptApplication::new();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.options_mut()
.set_string_value("sb", "yes", true, false)
.unwrap();
app.initialize().unwrap();
let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(ParametricTNLP::new(5.0, 1.0)));
app.set_on_converged(Box::new(move |data, cq, nlp, pd| {
let curr = data.borrow().curr.clone().expect("curr at convergence");
let n_x = curr.x.dim() as usize;
let n_s = curr.s.dim() as usize;
let y_c_offset = n_x + n_s;
let param_rows = vec![(y_c_offset + 2) as Index, (y_c_offset + 3) as Index];
let backsolver =
PdSensBacksolver::new(data, cq, nlp, pd).expect("PdSensBacksolver construction");
let n_full = backsolver.dim();
let a_data = IndexSchurData::from_parts(param_rows, vec![1, 1]).expect("A SchurData");
let opts = SensOptions {
run_sens: true,
..SensOptions::default()
};
let sens_app = SensApplication::new(a_data, backsolver, opts);
let mut dx_full = vec![0.0; n_full];
assert!(
sens_app.parametric_step(&delta_p, &mut dx_full),
"SensApplication::parametric_step failed"
);
*dx_full_clone.borrow_mut() = Some(dx_full);
}));
let status = app.optimize_tnlp(tnlp);
assert!(
matches!(
status,
ApplicationReturnStatus::SolveSucceeded
| ApplicationReturnStatus::SolvedToAcceptableLevel
),
"nominal solve failed: {status:?}",
);
let dx_full = dx_full_out
.borrow()
.clone()
.expect("on_converged populated dx_full");
std::array::from_fn(|i| dx_full[i])
}
#[test]
fn parametric_cpp_matches_upstream_sipopt() {
let delta_p = [-0.5, 0.0];
let dx_sens = run_sensitivity_step(delta_p);
let upstream_dx: [Number; 5] =
std::array::from_fn(|i| UPSTREAM_X_PERTURBED_NOBC[i] - UPSTREAM_X_NOMINAL[i]);
eprintln!(
"Upstream-golden comparison @ Δeta = (-0.5, 0):\n upstream Δx = {:?}\n pounce Δx = {:?}",
upstream_dx, dx_sens,
);
for k in 0..5 {
let err = (dx_sens[k] - upstream_dx[k]).abs();
assert!(
err < 1e-8,
"dx[{k}]: pounce={}, upstream={}, |err|={err} not < 1e-8",
dx_sens[k],
upstream_dx[k],
);
}
}
#[test]
fn parametric_cpp_first_order_sensitivity_matches_finite_difference() {
let dx_step: Number = 1.0e-2;
let eta1_nominal: Number = 5.0;
let eta2_nominal: Number = 1.0;
let x_nominal = solve_at(eta1_nominal, eta2_nominal);
let dx_full_out: Rc<RefCell<Option<Vec<Number>>>> = Rc::new(RefCell::new(None));
let dx_full_clone = Rc::clone(&dx_full_out);
let mut app = IpoptApplication::new();
app.options_mut()
.set_integer_value("print_level", 0, true, false)
.unwrap();
app.options_mut()
.set_string_value("sb", "yes", true, false)
.unwrap();
app.initialize().unwrap();
let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(ParametricTNLP::new(
eta1_nominal,
eta2_nominal,
)));
app.set_on_converged(Box::new(move |data, cq, nlp, pd| {
let curr = data.borrow().curr.clone().expect("curr at convergence");
let n_x = curr.x.dim() as usize;
let n_s = curr.s.dim() as usize;
let y_c_offset = n_x + n_s;
let param_rows = vec![
(y_c_offset + 2) as pounce_common::types::Index,
(y_c_offset + 3) as pounce_common::types::Index,
];
let backsolver =
PdSensBacksolver::new(data, cq, nlp, pd).expect("PdSensBacksolver construction");
let n_full = backsolver.dim();
let a_data = IndexSchurData::from_parts(param_rows, vec![1, 1]).expect("A SchurData");
let opts = SensOptions {
run_sens: true,
..SensOptions::default()
};
let sens_app = SensApplication::new(a_data, backsolver, opts);
let delta_p: Vec<Number> = vec![dx_step, 0.0]; let mut dx_full = vec![0.0; n_full];
assert!(
sens_app.parametric_step(&delta_p, &mut dx_full),
"SensApplication::parametric_step failed"
);
*dx_full_clone.borrow_mut() = Some(dx_full);
}));
let status = app.optimize_tnlp(tnlp);
assert!(
matches!(
status,
ApplicationReturnStatus::SolveSucceeded
| ApplicationReturnStatus::SolvedToAcceptableLevel
),
"nominal solve for sensitivity step failed: {status:?}",
);
let dx_full = dx_full_out
.borrow()
.clone()
.expect("on_converged populated dx_full");
let eta1_perturbed = eta1_nominal + dx_step;
let x_perturbed = solve_at(eta1_perturbed, eta2_nominal);
let dx_fd: [Number; 5] = std::array::from_fn(|i| x_perturbed[i] - x_nominal[i]);
let dx_x: [Number; 5] = std::array::from_fn(|i| dx_full[i]);
eprintln!(
"ParametricTNLP sensitivity test:\n x_nom = {:?}\n x_pert = {:?}\n dx_fd = {:?}\n dx_sens= {:?}",
x_nominal, x_perturbed, dx_fd, dx_x
);
assert!(
(dx_x[3].abs() - dx_step).abs() < 1e-6,
"Δeta1 slot (dx_full[3]) magnitude {} differs from |Δp|={} by more than 1e-6",
dx_x[3].abs(),
dx_step,
);
assert!(
dx_x[4].abs() < 1e-7,
"Δeta2 slot (dx_full[4]) = {} not near zero",
dx_x[4],
);
let sign = (dx_x[3] * dx_fd[3]).signum();
assert!(
sign > 0.0,
"sensitivity sign convention disagrees with FD reference: dx_x[3]={}, dx_fd[3]={}",
dx_x[3],
dx_fd[3]
);
for k in 0..3 {
let pred = sign * dx_x[k];
let err = (pred - dx_fd[k]).abs();
assert!(
err < 1e-6,
"dx[{k}]: sens (signed)={pred}, fd={}, |err|={err} not < 1e-6",
dx_fd[k],
);
}
}