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 if let Some(eps) = boundcheck_eps {
98 let x_curr_compressed: Vec<Number> = curr
102 .x
103 .as_any()
104 .downcast_ref::<DenseVector>()
105 .map(|d| d.values().to_vec())
106 .unwrap_or_default();
107 let mut dx_primal = dx[..n_x].to_vec();
108 let n_clamped = pounce_sensitivity::boundcheck::clamp_with_nlp(
109 &*nlp.borrow(),
110 &x_curr_compressed,
111 &mut dx_primal,
112 eps,
113 );
114 if n_clamped > 0 {
115 eprintln!("pounce: --sens-boundcheck clamped {n_clamped} primal coordinate(s)");
116 dx[..n_x].copy_from_slice(&dx_primal);
117 }
118 }
119
120 let mut x_pert = x_full.to_vec();
123 let nlp_ref = nlp.borrow();
124 for var_idx in 0..n_x {
125 let full_idx = nlp_ref.var_x_to_full_x(var_idx as Index) as usize;
126 x_pert[full_idx] += dx[var_idx];
127 }
128 Some(x_pert)
129}
130
131pub fn sol_suffix_to_report(s: &SolSuffix) -> SolutionSuffix {
134 let target = match s.target {
135 SolSuffixTarget::Var => "var",
136 SolSuffixTarget::Con => "con",
137 SolSuffixTarget::Obj => "obj",
138 SolSuffixTarget::Problem => "problem",
139 }
140 .to_string();
141 let (kind, values, int_values) = match &s.values {
142 SolSuffixValues::Real(v) => ("real".to_string(), v.clone(), Vec::new()),
143 SolSuffixValues::Int(v) => ("int".to_string(), Vec::new(), v.clone()),
144 SolSuffixValues::ProblemReal(v) => ("real".to_string(), vec![*v], Vec::new()),
145 SolSuffixValues::ProblemInt(v) => ("int".to_string(), Vec::new(), vec![*v]),
146 };
147 SolutionSuffix {
148 name: s.name.clone(),
149 target,
150 kind,
151 values,
152 int_values,
153 }
154}
155
156pub fn print_red_hessian_to_stderr(rh: &RedHessianResult) {
161 let n = rh.var_indices.len();
162 eprintln!("\n=== Reduced Hessian (n={n}) ===");
163 eprintln!("var indices: {:?}", rh.var_indices);
164 for i in 0..n {
165 let mut row = String::new();
166 for j in 0..n {
167 row.push_str(&format!(" {:>14.6e}", rh.hr[i + n * j]));
169 }
170 eprintln!(" [{i:>3}]{row}");
171 }
172 if let Some(w) = &rh.eigenvalues {
173 eprintln!("\n=== Reduced-Hessian eigenvalues (ascending) ===");
174 for (k, v) in w.iter().enumerate() {
175 eprintln!(" [{k:>3}] {v:>14.6e}");
176 }
177 }
178 eprintln!();
179}
180
181pub fn try_compute_red_hessian(
190 data: &pounce_algorithm::ipopt_data::IpoptDataHandle,
191 cq: &pounce_algorithm::ipopt_cq::IpoptCqHandle,
192 nlp: &Rc<RefCell<dyn pounce_nlp::ipopt_nlp::IpoptNlp>>,
193 pd: Rc<RefCell<pounce_algorithm::kkt::pd_full_space_solver::PdFullSpaceSolver>>,
194 suffixes: &NlSuffixes,
195 compute_eigen: bool,
196) -> Option<RedHessianResult> {
197 let red_hessian_tags = suffixes.var_int.get("red_hessian")?;
198 let max_slot = red_hessian_tags.iter().copied().max().unwrap_or(0);
199 if max_slot <= 0 {
200 return None;
201 }
202 let n_slots = max_slot as usize;
203
204 let nlp_ref = nlp.borrow();
208 let mut full_for_slot: Vec<Option<usize>> = vec![None; n_slots];
209 for (full_idx, &slot) in red_hessian_tags.iter().enumerate() {
210 if slot > 0 {
211 let s = slot as usize;
212 if s <= n_slots {
213 full_for_slot[s - 1] = Some(full_idx);
214 }
215 }
216 }
217 let mut var_indices: Vec<usize> = Vec::with_capacity(n_slots);
218 for (k, slot) in full_for_slot.iter().enumerate() {
219 let full_idx = match slot {
220 Some(i) => *i,
221 None => {
222 eprintln!("pounce: red_hessian slot {} has no tagged variable", k + 1);
223 return None;
224 }
225 };
226 match nlp_ref.full_x_to_var_x(full_idx as Index) {
227 Some(v) => var_indices.push(v as usize),
228 None => {
229 eprintln!(
230 "pounce: red_hessian slot {} tags fixed variable {} (skipping)",
231 k + 1,
232 full_idx
233 );
234 return None;
235 }
236 }
237 }
238 drop(nlp_ref);
239
240 let rows: Vec<Index> = var_indices.iter().map(|&v| v as Index).collect();
243 let signs: Vec<Index> = vec![1; var_indices.len()];
244 let a_data = IndexSchurData::from_parts(rows, signs).ok()?;
245
246 let backsolver = PdSensBacksolver::new(data, cq, nlp, pd).ok()?;
247 let opts = SensOptions {
248 compute_red_hessian: true,
249 rh_eigendecomp: compute_eigen,
250 ..SensOptions::default()
251 };
252 let mut app = SensApplication::new(a_data, backsolver, opts);
253 let n = var_indices.len();
254 let mut hr = vec![0.0; n * n];
255 let (eigenvalues, eigenvectors) = if compute_eigen {
256 let mut w = vec![0.0; n];
257 let mut v = vec![0.0; n * n];
258 if !app.compute_reduced_hessian_eigen(&mut hr, &mut w, &mut v) {
259 eprintln!("pounce: reduced-Hessian eigendecomp failed");
260 return None;
261 }
262 (Some(w), Some(v))
263 } else {
264 if !app.compute_reduced_hessian(&mut hr) {
265 eprintln!("pounce: reduced-Hessian computation failed");
266 return None;
267 }
268 (None, None)
269 };
270 let _ = cq;
271 Some(RedHessianResult {
272 var_indices,
273 hr,
274 eigenvalues,
275 eigenvectors,
276 })
277}
278
279#[allow(clippy::too_many_arguments)]
284fn try_compute_sens_step(
285 data: &pounce_algorithm::ipopt_data::IpoptDataHandle,
286 cq: &pounce_algorithm::ipopt_cq::IpoptCqHandle,
287 nlp: &Rc<RefCell<dyn pounce_nlp::ipopt_nlp::IpoptNlp>>,
288 pd: Rc<RefCell<pounce_algorithm::kkt::pd_full_space_solver::PdFullSpaceSolver>>,
289 suffixes: &NlSuffixes,
290 n_full: usize,
291 _m_full: usize,
292 x_nominal: &[Number],
293) -> Option<Vec<Number>> {
294 let sens_state = suffixes.var_int.get("sens_state_1")?;
298 let sens_state_value = suffixes.var_real.get("sens_state_value_1")?;
299 let sens_init_constr = suffixes.con_int.get("sens_init_constr")?;
300
301 if sens_state.len() != n_full || sens_state_value.len() != n_full {
302 eprintln!("pounce: sens_state_1 / sens_state_value_1 length mismatch (expected {n_full})");
303 return None;
304 }
305
306 let n_params = sens_state.iter().copied().max().unwrap_or(0).max(0) as usize;
310 if n_params == 0 {
311 return None;
312 }
313
314 let mut param_var_idx: Vec<Option<usize>> = vec![None; n_params];
318 for (var_idx, &slot) in sens_state.iter().enumerate() {
319 if slot > 0 {
320 let s = slot as usize;
321 if s <= n_params {
322 param_var_idx[s - 1] = Some(var_idx);
323 }
324 }
325 }
326 let mut param_con_idx: Vec<Option<usize>> = vec![None; n_params];
327 for (con_idx, &slot) in sens_init_constr.iter().enumerate() {
328 if slot > 0 {
329 let s = slot as usize;
330 if s <= n_params {
331 param_con_idx[s - 1] = Some(con_idx);
332 }
333 }
334 }
335 for k in 0..n_params {
336 if param_var_idx[k].is_none() || param_con_idx[k].is_none() {
337 eprintln!(
338 "pounce: parameter {} missing sens_state_1 or sens_init_constr tag",
339 k + 1
340 );
341 return None;
342 }
343 }
344
345 let curr = data.borrow().curr.clone()?;
356 let n_x = curr.x.dim() as usize;
357 let n_s = curr.s.dim() as usize;
358 let nlp_ref = nlp.borrow();
359 let y_c_offset = n_x + n_s;
360 let mut rows: Vec<Index> = Vec::with_capacity(n_params);
361 for k in 0..n_params {
362 let full_ci = param_con_idx[k].unwrap();
363 match nlp_ref.full_g_to_c_block(full_ci as Index) {
364 Some(c_idx) => rows.push(y_c_offset as Index + c_idx),
365 None => {
366 eprintln!(
367 "pounce: parameter {} pinning constraint #{} is an inequality (not in the c block)",
368 k + 1,
369 full_ci
370 );
371 return None;
372 }
373 }
374 }
375 let signs: Vec<Index> = vec![1; n_params];
376 let a_data = IndexSchurData::from_parts(rows, signs).ok()?;
377
378 let mut delta_p: Vec<Number> = Vec::with_capacity(n_params);
384 for k in 0..n_params {
385 let vi = param_var_idx[k].unwrap();
386 delta_p.push(sens_state_value[vi] - x_nominal[vi]);
387 }
388 drop(nlp_ref);
389
390 let backsolver = PdSensBacksolver::new(data, cq, nlp, pd).ok()?;
391 let n_full_pd = backsolver.dim();
392 let mut rhs_full = vec![0.0; n_full_pd];
393 a_data
394 .trans_multiply(&delta_p, &mut rhs_full)
395 .map_err(|e| eprintln!("pounce: trans_multiply error: {e:?}"))
396 .ok()?;
397 let mut dx_full = vec![0.0; n_full_pd];
398 if !backsolver.solve(&rhs_full, &mut dx_full) {
399 eprintln!("pounce: KKT backsolve failed");
400 return None;
401 }
402 Some(dx_full)
403}