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,
40 SensOptionOverrides, SensOptions,
41};
42
43use crate::nl_reader::NlSuffixes;
44use crate::nl_writer::{SolSuffix, SolSuffixTarget, SolSuffixValues};
45use crate::solve_report::SolutionSuffix;
46
47pub fn is_sensitivity_input(suffixes: &NlSuffixes) -> bool {
51 suffixes.var_int.contains_key("sens_state_1")
52 && suffixes.var_real.contains_key("sens_state_value_1")
53 && suffixes.con_int.contains_key("sens_init_constr")
54}
55
56pub struct RedHessianResult {
61 pub var_indices: Vec<usize>,
66 pub hr: Vec<Number>,
68 pub eigenvalues: Option<Vec<Number>>,
70 pub eigenvectors: Option<Vec<Number>>,
72}
73
74#[allow(clippy::too_many_arguments)]
86pub fn compute_sens_perturbed_x(
87 data: &pounce_algorithm::ipopt_data::IpoptDataHandle,
88 cq: &pounce_algorithm::ipopt_cq::IpoptCqHandle,
89 nlp: &Rc<RefCell<dyn pounce_nlp::ipopt_nlp::IpoptNlp>>,
90 pd: Rc<RefCell<pounce_algorithm::kkt::pd_full_space_solver::PdFullSpaceSolver>>,
91 suffixes: &NlSuffixes,
92 n_full: usize,
93 m_full: usize,
94 x_full: &[Number],
95 boundcheck_eps: Option<Number>,
96 release_eps: Number,
97 sens_options: &SensOptionOverrides,
98) -> Option<Vec<Number>> {
99 let dx = try_compute_sens_step(
100 data,
101 cq,
102 nlp,
103 pd,
104 suffixes,
105 n_full,
106 m_full,
107 x_full,
108 boundcheck_eps,
109 release_eps,
110 sens_options,
111 )?;
112 let curr = data.borrow().curr.clone()?;
113 let n_x = curr.x.dim() as usize;
114
115 let mut x_pert = x_full.to_vec();
118 let nlp_ref = nlp.borrow();
119 for var_idx in 0..n_x {
120 let full_idx = nlp_ref.var_x_to_full_x(var_idx as Index) as usize;
121 x_pert[full_idx] += dx[var_idx];
122 }
123 Some(x_pert)
124}
125
126pub fn sol_suffix_to_report(s: &SolSuffix) -> SolutionSuffix {
129 let target = match s.target {
130 SolSuffixTarget::Var => "var",
131 SolSuffixTarget::Con => "con",
132 SolSuffixTarget::Obj => "obj",
133 SolSuffixTarget::Problem => "problem",
134 }
135 .to_string();
136 let (kind, values, int_values) = match &s.values {
137 SolSuffixValues::Real(v) => ("real".to_string(), v.clone(), Vec::new()),
138 SolSuffixValues::Int(v) => ("int".to_string(), Vec::new(), v.clone()),
139 SolSuffixValues::ProblemReal(v) => ("real".to_string(), vec![*v], Vec::new()),
140 SolSuffixValues::ProblemInt(v) => ("int".to_string(), Vec::new(), vec![*v]),
141 };
142 SolutionSuffix {
143 name: s.name.clone(),
144 target,
145 kind,
146 values,
147 int_values,
148 }
149}
150
151pub fn print_red_hessian_to_stderr(rh: &RedHessianResult) {
156 let n = rh.var_indices.len();
157 eprintln!("\n=== Reduced Hessian (n={n}) ===");
158 eprintln!("var indices: {:?}", rh.var_indices);
159 for i in 0..n {
160 let mut row = String::new();
161 for j in 0..n {
162 row.push_str(&format!(" {:>14.6e}", rh.hr[i + n * j]));
164 }
165 eprintln!(" [{i:>3}]{row}");
166 }
167 if let Some(w) = &rh.eigenvalues {
168 eprintln!("\n=== Reduced-Hessian eigenvalues (ascending) ===");
169 for (k, v) in w.iter().enumerate() {
170 eprintln!(" [{k:>3}] {v:>14.6e}");
171 }
172 }
173 eprintln!();
174}
175
176pub fn try_compute_red_hessian(
185 data: &pounce_algorithm::ipopt_data::IpoptDataHandle,
186 cq: &pounce_algorithm::ipopt_cq::IpoptCqHandle,
187 nlp: &Rc<RefCell<dyn pounce_nlp::ipopt_nlp::IpoptNlp>>,
188 pd: Rc<RefCell<pounce_algorithm::kkt::pd_full_space_solver::PdFullSpaceSolver>>,
189 suffixes: &NlSuffixes,
190 compute_eigen: bool,
191 sens_options: &SensOptionOverrides,
192) -> Option<RedHessianResult> {
193 let red_hessian_tags = suffixes.var_int.get("red_hessian")?;
194 let max_slot = red_hessian_tags.iter().copied().max().unwrap_or(0);
195 if max_slot <= 0 {
196 return None;
197 }
198 let n_slots = max_slot as usize;
199
200 let nlp_ref = nlp.borrow();
204 let mut full_for_slot: Vec<Option<usize>> = vec![None; n_slots];
205 for (full_idx, &slot) in red_hessian_tags.iter().enumerate() {
206 if slot > 0 {
207 let s = slot as usize;
208 if s <= n_slots {
209 full_for_slot[s - 1] = Some(full_idx);
210 }
211 }
212 }
213 let mut var_indices: Vec<usize> = Vec::with_capacity(n_slots);
214 for (k, slot) in full_for_slot.iter().enumerate() {
215 let full_idx = match slot {
216 Some(i) => *i,
217 None => {
218 eprintln!("pounce: red_hessian slot {} has no tagged variable", k + 1);
219 return None;
220 }
221 };
222 match nlp_ref.full_x_to_var_x(full_idx as Index) {
223 Some(v) => var_indices.push(v as usize),
224 None => {
225 eprintln!(
226 "pounce: red_hessian slot {} tags fixed variable {} (skipping)",
227 k + 1,
228 full_idx
229 );
230 return None;
231 }
232 }
233 }
234 drop(nlp_ref);
235
236 let rows: Vec<Index> = var_indices.iter().map(|&v| v as Index).collect();
239 let signs: Vec<Index> = vec![1; var_indices.len()];
240 let a_data = IndexSchurData::from_parts(rows, signs).ok()?;
241
242 let backsolver = PdSensBacksolver::new(data, cq, nlp, pd).ok()?;
243 if let Some(msg) = sens_options.pdpert_refusal(&backsolver.kkt_perturbations()) {
247 eprintln!("pounce: reduced Hessian: {msg}");
248 return None;
249 }
250 let opts = SensOptions {
251 compute_red_hessian: true,
252 rh_eigendecomp: compute_eigen,
253 ..SensOptions::default()
254 };
255 let mut app = SensApplication::new(a_data, backsolver, opts);
256 let n = var_indices.len();
257 let mut hr = vec![0.0; n * n];
258 let (eigenvalues, eigenvectors) = if compute_eigen {
259 let mut w = vec![0.0; n];
260 let mut v = vec![0.0; n * n];
261 if !app.compute_reduced_hessian_eigen(&mut hr, &mut w, &mut v) {
262 eprintln!("pounce: reduced-Hessian eigendecomp failed");
263 return None;
264 }
265 (Some(w), Some(v))
266 } else {
267 if !app.compute_reduced_hessian(&mut hr) {
268 eprintln!("pounce: reduced-Hessian computation failed");
269 return None;
270 }
271 (None, None)
272 };
273 let _ = cq;
274 Some(RedHessianResult {
275 var_indices,
276 hr,
277 eigenvalues,
278 eigenvectors,
279 })
280}
281
282#[allow(clippy::too_many_arguments)]
287fn try_compute_sens_step(
288 data: &pounce_algorithm::ipopt_data::IpoptDataHandle,
289 cq: &pounce_algorithm::ipopt_cq::IpoptCqHandle,
290 nlp: &Rc<RefCell<dyn pounce_nlp::ipopt_nlp::IpoptNlp>>,
291 pd: Rc<RefCell<pounce_algorithm::kkt::pd_full_space_solver::PdFullSpaceSolver>>,
292 suffixes: &NlSuffixes,
293 n_full: usize,
294 _m_full: usize,
295 x_nominal: &[Number],
296 boundcheck_eps: Option<Number>,
297 release_eps: Number,
298 sens_options: &SensOptionOverrides,
299) -> Option<Vec<Number>> {
300 let sens_state = suffixes.var_int.get("sens_state_1")?;
304 let sens_state_value = suffixes.var_real.get("sens_state_value_1")?;
305 let sens_init_constr = suffixes.con_int.get("sens_init_constr")?;
306
307 if sens_state.len() != n_full || sens_state_value.len() != n_full {
308 eprintln!("pounce: sens_state_1 / sens_state_value_1 length mismatch (expected {n_full})");
309 return None;
310 }
311
312 let n_params = sens_state.iter().copied().max().unwrap_or(0).max(0) as usize;
316 if n_params == 0 {
317 return None;
318 }
319
320 let mut param_var_idx: Vec<Option<usize>> = vec![None; n_params];
324 for (var_idx, &slot) in sens_state.iter().enumerate() {
325 if slot > 0 {
326 let s = slot as usize;
327 if s <= n_params {
328 param_var_idx[s - 1] = Some(var_idx);
329 }
330 }
331 }
332 let mut param_con_idx: Vec<Option<usize>> = vec![None; n_params];
333 for (con_idx, &slot) in sens_init_constr.iter().enumerate() {
334 if slot > 0 {
335 let s = slot as usize;
336 if s <= n_params {
337 param_con_idx[s - 1] = Some(con_idx);
338 }
339 }
340 }
341 for k in 0..n_params {
342 if param_var_idx[k].is_none() || param_con_idx[k].is_none() {
343 eprintln!(
344 "pounce: parameter {} missing sens_state_1 or sens_init_constr tag",
345 k + 1
346 );
347 return None;
348 }
349 }
350
351 let backsolver = PdSensBacksolver::new(data, cq, nlp, pd)
360 .map_err(|e| eprintln!("pounce: could not capture the KKT factor: {e}"))
361 .ok()?;
362 if let Some(msg) = sens_options.pdpert_refusal(&backsolver.kkt_perturbations()) {
366 eprintln!("pounce: {msg}");
367 return None;
368 }
369 let pin_g: Vec<Index> = param_con_idx
370 .iter()
371 .map(|ci| ci.unwrap() as Index)
372 .collect();
373 let rows = match backsolver.map_pin_g_to_kkt_rows(&pin_g) {
374 Ok(r) => r,
375 Err(e) => {
376 eprintln!("pounce: {e}");
377 return None;
378 }
379 };
380 let signs: Vec<Index> = vec![1; n_params];
381 let a_data = IndexSchurData::from_parts(rows, signs).ok()?;
382
383 let mut delta_p: Vec<Number> = Vec::with_capacity(n_params);
389 for k in 0..n_params {
390 let vi = param_var_idx[k].unwrap();
391 delta_p.push(sens_state_value[vi] - x_nominal[vi]);
392 }
393 let n_full_pd = backsolver.dim();
394 let mut rhs_full = vec![0.0; n_full_pd];
395 a_data
396 .trans_multiply(&delta_p, &mut rhs_full)
397 .map_err(|e| eprintln!("pounce: trans_multiply error: {e:?}"))
398 .ok()?;
399 let mut dx_full = vec![0.0; n_full_pd];
400 if !backsolver.solve(&rhs_full, &mut dx_full) {
401 eprintln!("pounce: KKT backsolve failed");
402 return None;
403 }
404
405 if let Some(eps) = boundcheck_eps {
410 let n_x = backsolver.block_dims()[0];
411 let x_curr = {
412 let d = data.borrow();
413 let curr = d.curr.as_ref()?;
414 curr.x
415 .as_any()
416 .downcast_ref::<DenseVector>()
417 .map(|v| v.expanded_values())
418 .unwrap_or_default()
419 };
420 let (mut lo, mut hi) = {
421 let nl = nlp.borrow();
422 pounce_sensitivity::boundcheck::expand_bounds(
423 n_x,
424 &nl.px_l(),
425 &nl.px_u(),
426 nl.x_l(),
427 nl.x_u(),
428 )
429 };
430 let mut x_nat = x_curr.clone();
434 {
435 let nlp_ref = nlp.borrow();
436 if let Some(d) = nlp_ref.variable_scaling() {
437 for i in 0..n_x.min(x_nat.len()) {
438 let di = d[nlp_ref.var_x_to_full_x(i as Index) as usize];
439 if di == 0.0 || di == 1.0 {
440 continue;
441 }
442 x_nat[i] /= di;
443 let (a, b) = (lo[i] / di, hi[i] / di);
444 lo[i] = a.min(b);
445 hi[i] = a.max(b);
446 }
447 }
448 }
449 let mults = {
452 let dims = backsolver.block_dims();
453 let base = dims[0] + dims[1] + dims[2] + dims[3];
454 let d = data.borrow();
455 let curr = d.curr.as_ref()?;
456 let mut out = Vec::new();
457 for (off, v) in [(base, &curr.z_l), (base + dims[4], &curr.z_u)] {
458 let vals = v
459 .as_any()
460 .downcast_ref::<DenseVector>()
461 .map(|d| d.expanded_values())
462 .unwrap_or_default();
463 for (k, &b) in vals.iter().enumerate() {
464 out.push(pounce_sensitivity::boundcheck::BoundMultiplier {
465 row: off + k,
466 base: b,
467 });
468 }
469 }
470 out
471 };
472 match pounce_sensitivity::boundcheck::refine_step_onto_bounds(
473 &backsolver,
474 &dx_full,
475 &x_nat[..n_x.min(x_nat.len())],
476 &lo,
477 &hi,
478 &mults,
479 &rhs_full,
482 eps,
483 release_eps,
484 16,
485 ) {
486 Ok((refined, rows, stop)) => {
487 if !rows.is_empty() {
488 eprintln!(
489 "pounce: --sens-boundcheck pinned or released {} bound(s) \
490 and re-solved",
491 rows.len()
492 );
493 }
494 match stop {
498 pounce_sensitivity::boundcheck::RefineStop::Settled => {}
499 pounce_sensitivity::boundcheck::RefineStop::IterationLimit => eprintln!(
500 "pounce: --sens-boundcheck stopped at its pass limit with \
501 bounds still violated"
502 ),
503 pounce_sensitivity::boundcheck::RefineStop::DegreesOfFreedom => eprintln!(
504 "pounce: --sens-boundcheck could not hold every bound at \
505 once; the problem's degrees of freedom are spent"
506 ),
507 pounce_sensitivity::boundcheck::RefineStop::WorseThanPlain => eprintln!(
508 "pounce: --sens-boundcheck ended further outside the bounds \
509 than the unrefined step, which was returned instead"
510 ),
511 }
512 dx_full = refined;
513 }
514 Err(e) => {
515 eprintln!("pounce: --sens-boundcheck failed: {e}");
516 return None;
517 }
518 }
519 }
520 Some(dx_full)
521}