1use std::cell::RefCell;
34use std::rc::Rc;
35
36use pounce_common::types::{Index, Number};
37use pounce_linalg::dense_vector::DenseVector;
38use pounce_sensitivity::{
39 IndexSchurData, PdSensBacksolver, SchurData, SensApplication, SensBacksolver, SensOptions,
40};
41
42use crate::nl_reader::NlSuffixes;
43use crate::nl_writer::{SolSuffix, SolSuffixTarget, SolSuffixValues};
44use crate::solve_report::SolutionSuffix;
45
46pub fn is_sensitivity_input(suffixes: &NlSuffixes) -> bool {
50 suffixes.var_int.contains_key("sens_state_1")
51 && suffixes.var_real.contains_key("sens_state_value_1")
52 && suffixes.con_int.contains_key("sens_init_constr")
53}
54
55pub struct RedHessianResult {
60 pub var_indices: Vec<usize>,
65 pub hr: Vec<Number>,
67 pub eigenvalues: Option<Vec<Number>>,
69 pub eigenvectors: Option<Vec<Number>>,
71}
72
73#[allow(clippy::too_many_arguments)]
82pub fn compute_sens_perturbed_x(
83 data: &pounce_algorithm::ipopt_data::IpoptDataHandle,
84 cq: &pounce_algorithm::ipopt_cq::IpoptCqHandle,
85 nlp: &Rc<RefCell<dyn pounce_nlp::ipopt_nlp::IpoptNlp>>,
86 pd: Rc<RefCell<pounce_algorithm::kkt::pd_full_space_solver::PdFullSpaceSolver>>,
87 suffixes: &NlSuffixes,
88 n_full: usize,
89 m_full: usize,
90 x_full: &[Number],
91 boundcheck_eps: Option<Number>,
92) -> Option<Vec<Number>> {
93 let mut dx = try_compute_sens_step(data, cq, nlp, pd, suffixes, n_full, m_full, x_full)?;
94 let curr = data.borrow().curr.clone()?;
95 let n_x = curr.x.dim() as usize;
96
97 let d_var: Vec<Number> = {
103 let nlp_ref = nlp.borrow();
104 match nlp_ref.variable_scaling() {
105 Some(d) => (0..n_x)
106 .map(|v| d[nlp_ref.var_x_to_full_x(v as Index) as usize])
107 .collect(),
108 None => vec![1.0; n_x],
109 }
110 };
111
112 if let Some(eps) = boundcheck_eps {
113 let x_curr_compressed: Vec<Number> = curr
117 .x
118 .as_any()
119 .downcast_ref::<DenseVector>()
120 .map(|d| d.values().to_vec())
121 .unwrap_or_default();
122 let mut dx_primal = dx[..n_x].to_vec();
123 for (s, &di) in dx_primal.iter_mut().zip(d_var.iter()) {
127 *s *= di;
128 }
129 let n_clamped = pounce_sensitivity::boundcheck::clamp_with_nlp(
130 &*nlp.borrow(),
131 &x_curr_compressed,
132 &mut dx_primal,
133 eps,
134 );
135 for (s, &di) in dx_primal.iter_mut().zip(d_var.iter()) {
136 *s /= di;
137 }
138 if n_clamped > 0 {
139 eprintln!("pounce: --sens-boundcheck clamped {n_clamped} primal coordinate(s)");
140 dx[..n_x].copy_from_slice(&dx_primal);
141 }
142 }
143
144 let mut x_pert = x_full.to_vec();
147 let nlp_ref = nlp.borrow();
148 for var_idx in 0..n_x {
149 let full_idx = nlp_ref.var_x_to_full_x(var_idx as Index) as usize;
150 x_pert[full_idx] += dx[var_idx];
151 }
152 Some(x_pert)
153}
154
155pub fn sol_suffix_to_report(s: &SolSuffix) -> SolutionSuffix {
158 let target = match s.target {
159 SolSuffixTarget::Var => "var",
160 SolSuffixTarget::Con => "con",
161 SolSuffixTarget::Obj => "obj",
162 SolSuffixTarget::Problem => "problem",
163 }
164 .to_string();
165 let (kind, values, int_values) = match &s.values {
166 SolSuffixValues::Real(v) => ("real".to_string(), v.clone(), Vec::new()),
167 SolSuffixValues::Int(v) => ("int".to_string(), Vec::new(), v.clone()),
168 SolSuffixValues::ProblemReal(v) => ("real".to_string(), vec![*v], Vec::new()),
169 SolSuffixValues::ProblemInt(v) => ("int".to_string(), Vec::new(), vec![*v]),
170 };
171 SolutionSuffix {
172 name: s.name.clone(),
173 target,
174 kind,
175 values,
176 int_values,
177 }
178}
179
180pub fn print_red_hessian_to_stderr(rh: &RedHessianResult) {
185 let n = rh.var_indices.len();
186 eprintln!("\n=== Reduced Hessian (n={n}) ===");
187 eprintln!("var indices: {:?}", rh.var_indices);
188 for i in 0..n {
189 let mut row = String::new();
190 for j in 0..n {
191 row.push_str(&format!(" {:>14.6e}", rh.hr[i + n * j]));
193 }
194 eprintln!(" [{i:>3}]{row}");
195 }
196 if let Some(w) = &rh.eigenvalues {
197 eprintln!("\n=== Reduced-Hessian eigenvalues (ascending) ===");
198 for (k, v) in w.iter().enumerate() {
199 eprintln!(" [{k:>3}] {v:>14.6e}");
200 }
201 }
202 eprintln!();
203}
204
205pub fn try_compute_red_hessian(
214 data: &pounce_algorithm::ipopt_data::IpoptDataHandle,
215 cq: &pounce_algorithm::ipopt_cq::IpoptCqHandle,
216 nlp: &Rc<RefCell<dyn pounce_nlp::ipopt_nlp::IpoptNlp>>,
217 pd: Rc<RefCell<pounce_algorithm::kkt::pd_full_space_solver::PdFullSpaceSolver>>,
218 suffixes: &NlSuffixes,
219 compute_eigen: bool,
220) -> Option<RedHessianResult> {
221 let red_hessian_tags = suffixes.var_int.get("red_hessian")?;
222 let max_slot = red_hessian_tags.iter().copied().max().unwrap_or(0);
223 if max_slot <= 0 {
224 return None;
225 }
226 let n_slots = max_slot as usize;
227
228 let nlp_ref = nlp.borrow();
232 let mut full_for_slot: Vec<Option<usize>> = vec![None; n_slots];
233 for (full_idx, &slot) in red_hessian_tags.iter().enumerate() {
234 if slot > 0 {
235 let s = slot as usize;
236 if s <= n_slots {
237 full_for_slot[s - 1] = Some(full_idx);
238 }
239 }
240 }
241 let mut var_indices: Vec<usize> = Vec::with_capacity(n_slots);
242 for (k, slot) in full_for_slot.iter().enumerate() {
243 let full_idx = match slot {
244 Some(i) => *i,
245 None => {
246 eprintln!("pounce: red_hessian slot {} has no tagged variable", k + 1);
247 return None;
248 }
249 };
250 match nlp_ref.full_x_to_var_x(full_idx as Index) {
251 Some(v) => var_indices.push(v as usize),
252 None => {
253 eprintln!(
254 "pounce: red_hessian slot {} tags fixed variable {} (skipping)",
255 k + 1,
256 full_idx
257 );
258 return None;
259 }
260 }
261 }
262 drop(nlp_ref);
263
264 let rows: Vec<Index> = var_indices.iter().map(|&v| v as Index).collect();
267 let signs: Vec<Index> = vec![1; var_indices.len()];
268 let a_data = IndexSchurData::from_parts(rows, signs).ok()?;
269
270 let backsolver = PdSensBacksolver::new(data, cq, nlp, pd).ok()?;
271 let opts = SensOptions {
272 compute_red_hessian: true,
273 rh_eigendecomp: compute_eigen,
274 ..SensOptions::default()
275 };
276 let mut app = SensApplication::new(a_data, backsolver, opts);
277 let n = var_indices.len();
278 let mut hr = vec![0.0; n * n];
279 let (eigenvalues, eigenvectors) = if compute_eigen {
280 let mut w = vec![0.0; n];
281 let mut v = vec![0.0; n * n];
282 if !app.compute_reduced_hessian_eigen(&mut hr, &mut w, &mut v) {
283 eprintln!("pounce: reduced-Hessian eigendecomp failed");
284 return None;
285 }
286 (Some(w), Some(v))
287 } else {
288 if !app.compute_reduced_hessian(&mut hr) {
289 eprintln!("pounce: reduced-Hessian computation failed");
290 return None;
291 }
292 (None, None)
293 };
294 let _ = cq;
295 Some(RedHessianResult {
296 var_indices,
297 hr,
298 eigenvalues,
299 eigenvectors,
300 })
301}
302
303#[allow(clippy::too_many_arguments)]
308fn try_compute_sens_step(
309 data: &pounce_algorithm::ipopt_data::IpoptDataHandle,
310 cq: &pounce_algorithm::ipopt_cq::IpoptCqHandle,
311 nlp: &Rc<RefCell<dyn pounce_nlp::ipopt_nlp::IpoptNlp>>,
312 pd: Rc<RefCell<pounce_algorithm::kkt::pd_full_space_solver::PdFullSpaceSolver>>,
313 suffixes: &NlSuffixes,
314 n_full: usize,
315 _m_full: usize,
316 x_nominal: &[Number],
317) -> Option<Vec<Number>> {
318 let sens_state = suffixes.var_int.get("sens_state_1")?;
322 let sens_state_value = suffixes.var_real.get("sens_state_value_1")?;
323 let sens_init_constr = suffixes.con_int.get("sens_init_constr")?;
324
325 if sens_state.len() != n_full || sens_state_value.len() != n_full {
326 eprintln!("pounce: sens_state_1 / sens_state_value_1 length mismatch (expected {n_full})");
327 return None;
328 }
329
330 let n_params = sens_state.iter().copied().max().unwrap_or(0).max(0) as usize;
334 if n_params == 0 {
335 return None;
336 }
337
338 let mut param_var_idx: Vec<Option<usize>> = vec![None; n_params];
342 for (var_idx, &slot) in sens_state.iter().enumerate() {
343 if slot > 0 {
344 let s = slot as usize;
345 if s <= n_params {
346 param_var_idx[s - 1] = Some(var_idx);
347 }
348 }
349 }
350 let mut param_con_idx: Vec<Option<usize>> = vec![None; n_params];
351 for (con_idx, &slot) in sens_init_constr.iter().enumerate() {
352 if slot > 0 {
353 let s = slot as usize;
354 if s <= n_params {
355 param_con_idx[s - 1] = Some(con_idx);
356 }
357 }
358 }
359 for k in 0..n_params {
360 if param_var_idx[k].is_none() || param_con_idx[k].is_none() {
361 eprintln!(
362 "pounce: parameter {} missing sens_state_1 or sens_init_constr tag",
363 k + 1
364 );
365 return None;
366 }
367 }
368
369 let backsolver = PdSensBacksolver::new(data, cq, nlp, pd)
378 .map_err(|e| eprintln!("pounce: could not capture the KKT factor: {e}"))
379 .ok()?;
380 let pin_g: Vec<Index> = param_con_idx
381 .iter()
382 .map(|ci| ci.unwrap() as Index)
383 .collect();
384 let rows = match backsolver.map_pin_g_to_kkt_rows(&pin_g) {
385 Ok(r) => r,
386 Err(e) => {
387 eprintln!("pounce: {e}");
388 return None;
389 }
390 };
391 let signs: Vec<Index> = vec![1; n_params];
392 let a_data = IndexSchurData::from_parts(rows, signs).ok()?;
393
394 let mut delta_p: Vec<Number> = Vec::with_capacity(n_params);
400 for k in 0..n_params {
401 let vi = param_var_idx[k].unwrap();
402 delta_p.push(sens_state_value[vi] - x_nominal[vi]);
403 }
404 let n_full_pd = backsolver.dim();
405 let mut rhs_full = vec![0.0; n_full_pd];
406 a_data
407 .trans_multiply(&delta_p, &mut rhs_full)
408 .map_err(|e| eprintln!("pounce: trans_multiply error: {e:?}"))
409 .ok()?;
410 let mut dx_full = vec![0.0; n_full_pd];
411 if !backsolver.solve(&rhs_full, &mut dx_full) {
412 eprintln!("pounce: KKT backsolve failed");
413 return None;
414 }
415 Some(dx_full)
416}