1use crate::ipopt_data::IpoptDataHandle;
28use pounce_common::types::{Index, Number};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum TrialStatus {
33 NoTest,
34 DcEq0DxEq0,
35 DcGt0DxEq0,
36 DcEq0DxGt0,
37 DcGt0DxGt0,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum DegenType {
43 NotDegenerate,
44 Degenerate,
45 NotYetDetermined,
46}
47
48#[derive(Debug, Clone)]
51pub struct PdPerturbationHandler {
52 pub delta_xs_max: Number,
54 pub delta_xs_min: Number,
55 pub delta_xs_first_inc_fact: Number,
56 pub delta_xs_inc_fact: Number,
57 pub delta_xs_dec_fact: Number,
58 pub delta_xs_init: Number,
59 pub delta_cd_val: Number,
60 pub delta_cd_exp: Number,
61 pub perturb_always_cd: bool,
62 pub reset_last: bool,
63 pub degen_iters_max: Index,
64
65 pub delta_x_curr: Number,
67 pub delta_s_curr: Number,
68 pub delta_c_curr: Number,
69 pub delta_d_curr: Number,
70 pub delta_x_last: Number,
71 pub delta_s_last: Number,
72 pub delta_c_last: Number,
73 pub delta_d_last: Number,
74 pub get_deltas_for_wrong_inertia_called: bool,
75 pub hess_degenerate: DegenType,
76 pub jac_degenerate: DegenType,
77 pub degen_iters: Index,
78 pub test_status: TrialStatus,
79}
80
81impl Default for PdPerturbationHandler {
82 fn default() -> Self {
83 Self {
84 delta_xs_max: 1e20,
85 delta_xs_min: 1e-20,
86 delta_xs_first_inc_fact: 100.0,
87 delta_xs_inc_fact: 8.0,
88 delta_xs_dec_fact: 1.0 / 3.0,
89 delta_xs_init: 1e-4,
90 delta_cd_val: 1e-8,
91 delta_cd_exp: 0.25,
92 perturb_always_cd: false,
93 reset_last: false,
94 degen_iters_max: 3,
95 delta_x_curr: 0.0,
96 delta_s_curr: 0.0,
97 delta_c_curr: 0.0,
98 delta_d_curr: 0.0,
99 delta_x_last: 0.0,
100 delta_s_last: 0.0,
101 delta_c_last: 0.0,
102 delta_d_last: 0.0,
103 get_deltas_for_wrong_inertia_called: false,
104 hess_degenerate: DegenType::NotYetDetermined,
105 jac_degenerate: DegenType::NotYetDetermined,
106 degen_iters: 0,
107 test_status: TrialStatus::NoTest,
108 }
109 }
110}
111
112#[derive(Debug, Clone, Copy, PartialEq)]
114pub struct Deltas {
115 pub delta_x: Number,
116 pub delta_s: Number,
117 pub delta_c: Number,
118 pub delta_d: Number,
119}
120
121impl PdPerturbationHandler {
122 pub fn new() -> Self {
123 Self::default()
124 }
125
126 pub fn set_perturb_always_cd(&mut self, on: bool) {
129 self.perturb_always_cd = on;
130 self.jac_degenerate = if on {
131 DegenType::NotDegenerate
132 } else {
133 DegenType::NotYetDetermined
134 };
135 }
136
137 pub fn consider_new_system(
142 &mut self,
143 mu: Number,
144 ip_data: Option<&IpoptDataHandle>,
145 ) -> Option<Deltas> {
146 self.finalize_test(ip_data);
147
148 if self.reset_last {
151 self.delta_x_last = self.delta_x_curr;
152 self.delta_s_last = self.delta_s_curr;
153 self.delta_c_last = self.delta_c_curr;
154 self.delta_d_last = self.delta_d_curr;
155 } else {
156 if self.delta_x_curr > 0.0 {
157 self.delta_x_last = self.delta_x_curr;
158 }
159 if self.delta_s_curr > 0.0 {
160 self.delta_s_last = self.delta_s_curr;
161 }
162 if self.delta_c_curr > 0.0 {
163 self.delta_c_last = self.delta_c_curr;
164 }
165 if self.delta_d_curr > 0.0 {
166 self.delta_d_last = self.delta_d_curr;
167 }
168 }
169
170 let undet = matches!(self.hess_degenerate, DegenType::NotYetDetermined)
171 || matches!(self.jac_degenerate, DegenType::NotYetDetermined);
172 self.test_status = if undet {
173 if self.perturb_always_cd {
174 TrialStatus::DcGt0DxEq0
175 } else {
176 TrialStatus::DcEq0DxEq0
177 }
178 } else {
179 TrialStatus::NoTest
180 };
181
182 let mut delta_c = if matches!(self.jac_degenerate, DegenType::Degenerate) {
183 let v = self.delta_cd(mu);
184 self.delta_c_curr = v;
185 append_info(ip_data, "l");
186 v
187 } else if self.perturb_always_cd {
188 let v = self.delta_cd(mu);
189 self.delta_c_curr = v;
190 v
191 } else {
192 self.delta_c_curr = 0.0;
193 0.0
194 };
195 let mut delta_d = delta_c;
196 self.delta_d_curr = delta_d;
197
198 let mut delta_x = 0.0;
199 let mut delta_s = 0.0;
200
201 if matches!(self.hess_degenerate, DegenType::Degenerate) {
202 self.delta_x_curr = 0.0;
203 self.delta_s_curr = 0.0;
204 if !self.get_deltas_for_wrong_inertia(
205 &mut delta_x,
206 &mut delta_s,
207 &mut delta_c,
208 &mut delta_d,
209 ip_data,
210 ) {
211 return None;
212 }
213 }
214
215 self.delta_x_curr = delta_x;
216 self.delta_s_curr = delta_s;
217 self.delta_c_curr = delta_c;
218 self.delta_d_curr = delta_d;
219 set_info_regu_x(ip_data, delta_x);
220 self.get_deltas_for_wrong_inertia_called = false;
221
222 Some(Deltas {
223 delta_x,
224 delta_s,
225 delta_c,
226 delta_d,
227 })
228 }
229
230 pub fn perturb_for_singular(
233 &mut self,
234 mu: Number,
235 ip_data: Option<&IpoptDataHandle>,
236 ) -> Option<Deltas> {
237 let mut delta_x = 0.0;
238 let mut delta_s = 0.0;
239 let mut delta_c = 0.0;
240 let mut delta_d = 0.0;
241
242 let undet = matches!(self.hess_degenerate, DegenType::NotYetDetermined)
243 || matches!(self.jac_degenerate, DegenType::NotYetDetermined);
244 if undet {
245 match self.test_status {
246 TrialStatus::DcEq0DxEq0 => {
247 debug_assert!(self.delta_x_curr == 0.0 && self.delta_c_curr == 0.0);
248 if matches!(self.jac_degenerate, DegenType::NotYetDetermined) {
249 let v = self.delta_cd(mu);
250 self.delta_c_curr = v;
251 self.delta_d_curr = v;
252 self.test_status = TrialStatus::DcGt0DxEq0;
253 } else {
254 debug_assert!(matches!(self.hess_degenerate, DegenType::NotYetDetermined));
255 if !self.get_deltas_for_wrong_inertia(
256 &mut delta_x,
257 &mut delta_s,
258 &mut delta_c,
259 &mut delta_d,
260 ip_data,
261 ) {
262 return None;
263 }
264 self.test_status = TrialStatus::DcEq0DxGt0;
265 }
266 }
267 TrialStatus::DcGt0DxEq0 => {
268 debug_assert!(self.delta_x_curr == 0.0 && self.delta_c_curr > 0.0);
269 debug_assert!(matches!(self.jac_degenerate, DegenType::NotYetDetermined));
270 if !self.perturb_always_cd {
271 self.delta_c_curr = 0.0;
272 self.delta_d_curr = 0.0;
273 if !self.get_deltas_for_wrong_inertia(
274 &mut delta_x,
275 &mut delta_s,
276 &mut delta_c,
277 &mut delta_d,
278 ip_data,
279 ) {
280 return None;
281 }
282 self.test_status = TrialStatus::DcEq0DxGt0;
283 } else if !self.get_deltas_for_wrong_inertia(
284 &mut delta_x,
285 &mut delta_s,
286 &mut delta_c,
287 &mut delta_d,
288 ip_data,
289 ) {
290 return None;
291 } else {
292 self.test_status = TrialStatus::DcGt0DxGt0;
293 }
294 }
295 TrialStatus::DcEq0DxGt0 => {
296 debug_assert!(self.delta_x_curr > 0.0 && self.delta_c_curr == 0.0);
297 let v = self.delta_cd(mu);
298 self.delta_c_curr = v;
299 self.delta_d_curr = v;
300 if !self.get_deltas_for_wrong_inertia(
301 &mut delta_x,
302 &mut delta_s,
303 &mut delta_c,
304 &mut delta_d,
305 ip_data,
306 ) {
307 return None;
308 }
309 self.test_status = TrialStatus::DcGt0DxGt0;
310 }
311 TrialStatus::DcGt0DxGt0 => {
312 if !self.get_deltas_for_wrong_inertia(
313 &mut delta_x,
314 &mut delta_s,
315 &mut delta_c,
316 &mut delta_d,
317 ip_data,
318 ) {
319 return None;
320 }
321 }
322 TrialStatus::NoTest => {
323 debug_assert!(false, "perturb_for_singular: NoTest in undetermined branch");
324 }
325 }
326 } else if self.delta_c_curr > 0.0 {
327 if !self.get_deltas_for_wrong_inertia(
329 &mut delta_x,
330 &mut delta_s,
331 &mut delta_c,
332 &mut delta_d,
333 ip_data,
334 ) {
335 return None;
336 }
337 } else {
338 let v = self.delta_cd(mu);
339 self.delta_c_curr = v;
340 self.delta_d_curr = v;
341 append_info(ip_data, "L");
342 }
343
344 let out = Deltas {
345 delta_x: self.delta_x_curr,
346 delta_s: self.delta_s_curr,
347 delta_c: self.delta_c_curr,
348 delta_d: self.delta_d_curr,
349 };
350 set_info_regu_x(ip_data, out.delta_x);
351 Some(out)
352 }
353
354 pub fn perturb_for_wrong_inertia(
357 &mut self,
358 mu: Number,
359 ip_data: Option<&IpoptDataHandle>,
360 ) -> Option<Deltas> {
361 if std::env::var_os("POUNCE_DBG_PERT").is_some() {
362 let it = ip_data.map(|d| d.borrow().iter_count).unwrap_or(-1);
363 tracing::debug!(target: "pounce::linsol",
364 "[PERT] iter={} WRONG_INERTIA mu={:.2e} dx_last={:.2e} dx_curr={:.2e}",
365 it, mu, self.delta_x_last, self.delta_x_curr
366 );
367 }
368 self.finalize_test(ip_data);
369
370 let mut delta_x = 0.0;
371 let mut delta_s = 0.0;
372 let mut delta_c = 0.0;
373 let mut delta_d = 0.0;
374 let mut ok = self.get_deltas_for_wrong_inertia(
375 &mut delta_x,
376 &mut delta_s,
377 &mut delta_c,
378 &mut delta_d,
379 ip_data,
380 );
381 if !ok && self.delta_c_curr == 0.0 {
388 debug_assert_eq!(self.delta_d_curr, 0.0);
389 let v = self.delta_cd(mu);
390 self.delta_c_curr = v;
391 self.delta_d_curr = v;
392 self.delta_x_curr = 0.0;
393 self.delta_s_curr = 0.0;
394 self.test_status = TrialStatus::NoTest;
395 if matches!(self.hess_degenerate, DegenType::Degenerate) {
396 self.hess_degenerate = DegenType::NotYetDetermined;
397 }
398 ok = self.get_deltas_for_wrong_inertia(
399 &mut delta_x,
400 &mut delta_s,
401 &mut delta_c,
402 &mut delta_d,
403 ip_data,
404 );
405 }
406 if !ok {
407 return None;
408 }
409 Some(Deltas {
410 delta_x,
411 delta_s,
412 delta_c,
413 delta_d,
414 })
415 }
416
417 pub fn current_perturbation(&self) -> Deltas {
419 Deltas {
420 delta_x: self.delta_x_curr,
421 delta_s: self.delta_s_curr,
422 delta_c: self.delta_c_curr,
423 delta_d: self.delta_d_curr,
424 }
425 }
426
427 fn get_deltas_for_wrong_inertia(
431 &mut self,
432 delta_x: &mut Number,
433 delta_s: &mut Number,
434 delta_c: &mut Number,
435 delta_d: &mut Number,
436 ip_data: Option<&IpoptDataHandle>,
437 ) -> bool {
438 if self.delta_x_curr == 0.0 {
439 self.delta_x_curr = if self.delta_x_last == 0.0 {
440 self.delta_xs_init
441 } else {
442 self.delta_xs_min
443 .max(self.delta_x_last * self.delta_xs_dec_fact)
444 };
445 } else if self.delta_x_last == 0.0 || 1e5 * self.delta_x_last < self.delta_x_curr {
446 self.delta_x_curr *= self.delta_xs_first_inc_fact;
447 } else {
448 self.delta_x_curr *= self.delta_xs_inc_fact;
449 }
450 if self.delta_x_curr > self.delta_xs_max {
451 self.delta_x_last = 0.0;
452 self.delta_s_last = 0.0;
453 append_info(ip_data, "dx");
454 return false;
455 }
456 self.delta_s_curr = self.delta_x_curr;
457
458 *delta_x = self.delta_x_curr;
459 *delta_s = self.delta_s_curr;
460 *delta_c = self.delta_c_curr;
461 *delta_d = self.delta_d_curr;
462 set_info_regu_x(ip_data, *delta_x);
463 self.get_deltas_for_wrong_inertia_called = true;
464 true
465 }
466
467 fn delta_cd(&self, mu: Number) -> Number {
468 self.delta_cd_val * mu.powf(self.delta_cd_exp)
469 }
470
471 fn finalize_test(&mut self, ip_data: Option<&IpoptDataHandle>) {
475 match self.test_status {
476 TrialStatus::NoTest => (),
477 TrialStatus::DcEq0DxEq0 => {
478 if matches!(self.hess_degenerate, DegenType::NotYetDetermined)
479 && matches!(self.jac_degenerate, DegenType::NotYetDetermined)
480 {
481 self.hess_degenerate = DegenType::NotDegenerate;
482 self.jac_degenerate = DegenType::NotDegenerate;
483 append_info(ip_data, "Nhj ");
484 } else if matches!(self.hess_degenerate, DegenType::NotYetDetermined) {
485 self.hess_degenerate = DegenType::NotDegenerate;
486 append_info(ip_data, "Nh ");
487 } else if matches!(self.jac_degenerate, DegenType::NotYetDetermined) {
488 self.jac_degenerate = DegenType::NotDegenerate;
489 append_info(ip_data, "Nj ");
490 }
491 }
492 TrialStatus::DcGt0DxEq0 => {
493 if matches!(self.hess_degenerate, DegenType::NotYetDetermined) {
494 self.hess_degenerate = DegenType::NotDegenerate;
495 append_info(ip_data, "Nh ");
496 }
497 if matches!(self.jac_degenerate, DegenType::NotYetDetermined) {
498 self.degen_iters += 1;
499 if self.degen_iters >= self.degen_iters_max {
500 self.jac_degenerate = DegenType::Degenerate;
501 append_info(ip_data, "Dj ");
502 }
503 append_info(ip_data, "L");
504 }
505 }
506 TrialStatus::DcEq0DxGt0 => {
507 if matches!(self.jac_degenerate, DegenType::NotYetDetermined) {
508 self.jac_degenerate = DegenType::NotDegenerate;
509 append_info(ip_data, "Nj ");
510 }
511 if matches!(self.hess_degenerate, DegenType::NotYetDetermined) {
512 self.degen_iters += 1;
513 if self.degen_iters >= self.degen_iters_max {
514 self.hess_degenerate = DegenType::Degenerate;
515 append_info(ip_data, "Dh ");
516 }
517 }
518 }
519 TrialStatus::DcGt0DxGt0 => {
520 self.degen_iters += 1;
521 if self.degen_iters >= self.degen_iters_max {
522 self.hess_degenerate = DegenType::Degenerate;
523 self.jac_degenerate = DegenType::Degenerate;
524 append_info(ip_data, "Dhj ");
525 }
526 append_info(ip_data, "L");
527 }
528 }
529 }
530}
531
532fn append_info(ip_data: Option<&IpoptDataHandle>, s: &str) {
533 if let Some(h) = ip_data {
534 h.borrow_mut().append_info_string(s);
535 }
536}
537
538fn set_info_regu_x(ip_data: Option<&IpoptDataHandle>, v: Number) {
539 if let Some(h) = ip_data {
540 h.borrow_mut().info_regu_x = v;
541 }
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
547
548 #[test]
549 fn first_wrong_inertia_perturbation_is_delta_xs_init() {
550 let mut h = PdPerturbationHandler::new();
551 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
552 assert!((d.delta_x - 1e-4).abs() < 1e-20);
554 assert_eq!(d.delta_x, d.delta_s);
555 assert_eq!(d.delta_c, 0.0);
556 assert_eq!(d.delta_d, 0.0);
557 }
558
559 #[test]
560 fn second_perturbation_uses_first_inc_fact() {
561 let mut h = PdPerturbationHandler::new();
565 let d1 = h.perturb_for_wrong_inertia(0.1, None).unwrap();
566 let d2 = h.perturb_for_wrong_inertia(0.1, None).unwrap();
567 assert!((d2.delta_x - d1.delta_x * 100.0).abs() < 1e-15);
568 }
569
570 #[test]
571 fn third_perturbation_uses_inc_fact() {
572 let mut h = PdPerturbationHandler::new();
575 h.delta_x_curr = 1e-2;
576 h.delta_x_last = 1e-2;
577 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
578 assert!((d.delta_x - 1e-2 * 8.0).abs() < 1e-15);
579 }
580
581 #[test]
582 fn perturbation_caps_at_max_when_dcd_already_active() {
583 let mut h = PdPerturbationHandler::new();
588 h.delta_x_curr = h.delta_xs_max;
589 h.delta_c_curr = 1e-4;
590 h.delta_d_curr = 1e-4;
591 assert!(h.perturb_for_wrong_inertia(0.1, None).is_none());
592 }
593
594 #[test]
595 fn consider_new_system_with_perturb_always_cd_seeds_dcd() {
596 let mut h = PdPerturbationHandler::new();
597 h.set_perturb_always_cd(true);
598 let mu = 0.1;
599 let d = h.consider_new_system(mu, None).unwrap();
600 let expected = h.delta_cd_val * mu.powf(h.delta_cd_exp);
601 assert!((d.delta_c - expected).abs() < 1e-15);
602 assert!((d.delta_d - expected).abs() < 1e-15);
603 assert_eq!(d.delta_x, 0.0);
604 assert_eq!(d.delta_s, 0.0);
605 }
606
607 #[test]
608 fn consider_new_system_default_zeros_dcd() {
609 let mut h = PdPerturbationHandler::new();
610 let d = h.consider_new_system(0.1, None).unwrap();
611 assert_eq!(
612 d,
613 Deltas {
614 delta_x: 0.0,
615 delta_s: 0.0,
616 delta_c: 0.0,
617 delta_d: 0.0
618 }
619 );
620 }
621
622 #[test]
623 fn singular_in_test_dc_eq0_dx_eq0_seeds_dcd() {
624 let mut h = PdPerturbationHandler::new();
625 let _ = h.consider_new_system(0.1, None).unwrap();
626 assert_eq!(h.test_status, TrialStatus::DcEq0DxEq0);
630 let d = h.perturb_for_singular(0.1, None).unwrap();
631 let expected = h.delta_cd_val * (0.1_f64).powf(h.delta_cd_exp);
632 assert!((d.delta_c - expected).abs() < 1e-15);
633 assert!((d.delta_d - expected).abs() < 1e-15);
634 assert_eq!(d.delta_x, 0.0);
635 assert_eq!(h.test_status, TrialStatus::DcGt0DxEq0);
636 }
637
638 #[test]
639 fn singular_when_determined_with_dc_zero_seeds_dcd() {
640 let mut h = PdPerturbationHandler::new();
641 h.hess_degenerate = DegenType::NotDegenerate;
642 h.jac_degenerate = DegenType::NotDegenerate;
643 h.test_status = TrialStatus::NoTest;
644 let d = h.perturb_for_singular(0.1, None).unwrap();
645 let expected = h.delta_cd_val * (0.1_f64).powf(h.delta_cd_exp);
646 assert!((d.delta_c - expected).abs() < 1e-15);
647 }
648
649 #[test]
650 fn finalize_test_sets_not_degenerate_after_dc_eq0_dx_eq0_pass() {
651 let mut h = PdPerturbationHandler::new();
652 let _ = h.consider_new_system(0.1, None).unwrap();
653 let _ = h.consider_new_system(0.1, None).unwrap();
657 assert_eq!(h.hess_degenerate, DegenType::NotDegenerate);
658 assert_eq!(h.jac_degenerate, DegenType::NotDegenerate);
659 }
660
661 #[test]
662 fn current_perturbation_returns_committed_values() {
663 let mut h = PdPerturbationHandler::new();
664 h.delta_x_curr = 1.0;
665 h.delta_s_curr = 2.0;
666 h.delta_c_curr = 3.0;
667 h.delta_d_curr = 4.0;
668 let d = h.current_perturbation();
669 assert_eq!(d.delta_x, 1.0);
670 assert_eq!(d.delta_s, 2.0);
671 assert_eq!(d.delta_c, 3.0);
672 assert_eq!(d.delta_d, 4.0);
673 }
674}