1use crate::types::{Index, Number};
33
34pub trait PerturbationSink {
41 fn append_info(&self, s: &str);
43 fn set_regu_x(&self, v: Number);
46 fn iter_count(&self) -> Index {
48 -1
49 }
50 fn debug(&self, _msg: &str) {}
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum TrialStatus {
58 NoTest,
59 DcEq0DxEq0,
60 DcGt0DxEq0,
61 DcEq0DxGt0,
62 DcGt0DxGt0,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum DegenType {
68 NotDegenerate,
69 Degenerate,
70 NotYetDetermined,
71}
72
73#[derive(Debug, Clone)]
76pub struct PdPerturbationHandler {
77 pub delta_xs_max: Number,
79 pub delta_xs_min: Number,
80 pub delta_xs_first_inc_fact: Number,
81 pub delta_xs_inc_fact: Number,
82 pub delta_xs_dec_fact: Number,
83 pub delta_xs_init: Number,
84 pub delta_cd_val: Number,
85 pub delta_cd_exp: Number,
86 pub perturb_always_cd: bool,
87 pub reset_last: bool,
88 pub degen_iters_max: Index,
89 pub delta_s_fact: Number,
94 pub delta_s_max_rows: Index,
103 pub delta_s_rows: Index,
107
108 pub delta_x_curr: Number,
110 pub delta_s_curr: Number,
111 pub delta_c_curr: Number,
112 pub delta_d_curr: Number,
113 pub delta_x_last: Number,
114 pub delta_s_last: Number,
115 pub delta_c_last: Number,
116 pub delta_d_last: Number,
117 pub get_deltas_for_wrong_inertia_called: bool,
118 pub hess_degenerate: DegenType,
119 pub jac_degenerate: DegenType,
120 pub degen_iters: Index,
121 pub test_status: TrialStatus,
122 pub delta_c_rungs: Index,
126 pub delta_c_abandoned: bool,
132 pub delta_c_max_rungs: Index,
136 pub delta_x_at_withdrawal: Number,
141 pub delta_c_reinstated: bool,
145 pub withdrawal_untested: bool,
154}
155
156impl Default for PdPerturbationHandler {
157 fn default() -> Self {
158 Self {
159 delta_xs_max: 1e20,
160 delta_xs_min: 1e-20,
161 delta_xs_first_inc_fact: 100.0,
162 delta_xs_inc_fact: 8.0,
163 delta_xs_dec_fact: 1.0 / 3.0,
164 delta_xs_init: 1e-4,
165 delta_cd_val: 1e-8,
166 delta_cd_exp: 0.25,
167 perturb_always_cd: false,
168 reset_last: false,
169 degen_iters_max: 3,
170 delta_s_fact: 1.0,
171 delta_s_max_rows: 0,
172 delta_s_rows: 0,
173 delta_x_curr: 0.0,
174 delta_s_curr: 0.0,
175 delta_c_curr: 0.0,
176 delta_d_curr: 0.0,
177 delta_x_last: 0.0,
178 delta_s_last: 0.0,
179 delta_c_last: 0.0,
180 delta_d_last: 0.0,
181 get_deltas_for_wrong_inertia_called: false,
182 hess_degenerate: DegenType::NotYetDetermined,
183 jac_degenerate: DegenType::NotYetDetermined,
184 degen_iters: 0,
185 test_status: TrialStatus::NoTest,
186 delta_c_rungs: 0,
192 delta_c_abandoned: false,
193 delta_c_max_rungs: 3,
194 delta_x_at_withdrawal: 0.0,
195 delta_c_reinstated: false,
196 withdrawal_untested: false,
197 }
198 }
199}
200
201#[derive(Debug, Clone, Copy, PartialEq)]
203pub struct Deltas {
204 pub delta_x: Number,
205 pub delta_s: Number,
206 pub delta_c: Number,
207 pub delta_d: Number,
208}
209
210impl PdPerturbationHandler {
211 pub fn new() -> Self {
212 Self::default()
213 }
214
215 pub fn delta_s_fact_effective(&self) -> Number {
232 if self.delta_s_max_rows > 0 && self.delta_s_rows > self.delta_s_max_rows {
233 self.delta_s_fact * (self.delta_s_max_rows as Number) / (self.delta_s_rows as Number)
234 } else {
235 self.delta_s_fact
236 }
237 }
238
239 pub fn set_perturb_always_cd(&mut self, on: bool) {
242 self.perturb_always_cd = on;
243 self.jac_degenerate = if on {
244 DegenType::NotDegenerate
245 } else {
246 DegenType::NotYetDetermined
247 };
248 }
249
250 pub fn consider_new_system(
255 &mut self,
256 mu: Number,
257 ip_data: Option<&dyn PerturbationSink>,
258 ) -> Option<Deltas> {
259 self.finalize_test(ip_data);
260
261 if self.reset_last {
264 self.delta_x_last = self.delta_x_curr;
265 self.delta_s_last = self.delta_s_curr;
266 self.delta_c_last = self.delta_c_curr;
267 self.delta_d_last = self.delta_d_curr;
268 } else {
269 if self.delta_x_curr > 0.0 {
270 self.delta_x_last = self.delta_x_curr;
271 }
272 if self.delta_s_curr > 0.0 {
273 self.delta_s_last = self.delta_s_curr;
274 }
275 if self.delta_c_curr > 0.0 {
276 self.delta_c_last = self.delta_c_curr;
277 }
278 if self.delta_d_curr > 0.0 {
279 self.delta_d_last = self.delta_d_curr;
280 }
281 }
282
283 let undet = matches!(self.hess_degenerate, DegenType::NotYetDetermined)
284 || matches!(self.jac_degenerate, DegenType::NotYetDetermined);
285 self.test_status = if undet {
286 if self.perturb_always_cd {
287 TrialStatus::DcGt0DxEq0
288 } else {
289 TrialStatus::DcEq0DxEq0
290 }
291 } else {
292 TrialStatus::NoTest
293 };
294
295 let mut delta_c = if matches!(self.jac_degenerate, DegenType::Degenerate) {
296 let v = self.delta_cd(mu);
297 self.delta_c_curr = v;
298 append_info(ip_data, "l");
299 v
300 } else if self.perturb_always_cd {
301 let v = self.delta_cd(mu);
302 self.delta_c_curr = v;
303 v
304 } else {
305 self.delta_c_curr = 0.0;
306 0.0
307 };
308 let mut delta_d = delta_c;
309 self.delta_d_curr = delta_d;
310
311 let mut delta_x = 0.0;
312 let mut delta_s = 0.0;
313
314 if matches!(self.hess_degenerate, DegenType::Degenerate) {
315 self.delta_x_curr = 0.0;
316 self.delta_s_curr = 0.0;
317 if !self.get_deltas_for_wrong_inertia(
318 &mut delta_x,
319 &mut delta_s,
320 &mut delta_c,
321 &mut delta_d,
322 ip_data,
323 ) {
324 return None;
325 }
326 }
327
328 self.delta_x_curr = delta_x;
329 self.delta_s_curr = delta_s;
330 self.delta_c_curr = delta_c;
331 self.delta_d_curr = delta_d;
332 set_info_regu_x(ip_data, delta_x);
333 self.get_deltas_for_wrong_inertia_called = false;
334 self.delta_c_rungs = 0;
336 self.delta_c_abandoned = false;
337 self.delta_x_at_withdrawal = 0.0;
338 self.delta_c_reinstated = false;
339 self.withdrawal_untested = false;
340
341 Some(Deltas {
342 delta_x,
343 delta_s,
344 delta_c,
345 delta_d,
346 })
347 }
348
349 pub fn perturb_for_singular(
352 &mut self,
353 mu: Number,
354 ip_data: Option<&dyn PerturbationSink>,
355 ) -> Option<Deltas> {
356 self.perturb_for_singular_with(mu, false, ip_data)
357 }
358
359 pub fn perturb_for_singular_with(
373 &mut self,
374 mu: Number,
375 certified: bool,
376 ip_data: Option<&dyn PerturbationSink>,
377 ) -> Option<Deltas> {
378 let mut delta_x = 0.0;
379 let mut delta_s = 0.0;
380 let mut delta_c = 0.0;
381 let mut delta_d = 0.0;
382
383 let first_without_delta_c = self.withdrawal_untested;
390 self.withdrawal_untested = false;
391 if self.delta_c_abandoned && certified && first_without_delta_c && !self.delta_c_reinstated
392 {
393 self.delta_c_abandoned = false;
395 self.delta_c_reinstated = true;
396 let v = self.delta_cd(mu);
397 self.delta_c_curr = v;
398 self.delta_d_curr = v;
399 if self.delta_x_curr < self.delta_x_at_withdrawal {
403 self.delta_x_curr = self.delta_x_at_withdrawal;
404 self.delta_s_curr = self.delta_x_at_withdrawal * self.delta_s_fact_effective();
405 }
406 self.test_status = TrialStatus::NoTest;
407 append_info(ip_data, "W");
408 if !self.get_deltas_for_wrong_inertia(
409 &mut delta_x,
410 &mut delta_s,
411 &mut delta_c,
412 &mut delta_d,
413 ip_data,
414 ) {
415 return None;
416 }
417 set_info_regu_x(ip_data, self.delta_x_curr);
418 return Some(Deltas {
419 delta_x: self.delta_x_curr,
420 delta_s: self.delta_s_curr,
421 delta_c: self.delta_c_curr,
422 delta_d: self.delta_d_curr,
423 });
424 }
425 if self.delta_c_abandoned {
426 self.test_status = TrialStatus::NoTest;
427 if !self.get_deltas_for_wrong_inertia(
428 &mut delta_x,
429 &mut delta_s,
430 &mut delta_c,
431 &mut delta_d,
432 ip_data,
433 ) {
434 return None;
435 }
436 set_info_regu_x(ip_data, self.delta_x_curr);
437 return Some(Deltas {
438 delta_x: self.delta_x_curr,
439 delta_s: self.delta_s_curr,
440 delta_c: self.delta_c_curr,
441 delta_d: self.delta_d_curr,
442 });
443 }
444
445 let probe_intact = match self.test_status {
458 TrialStatus::DcEq0DxEq0 => self.delta_x_curr == 0.0 && self.delta_c_curr == 0.0,
459 TrialStatus::DcGt0DxEq0 => self.delta_x_curr == 0.0 && self.delta_c_curr > 0.0,
460 TrialStatus::DcEq0DxGt0 => self.delta_x_curr > 0.0 && self.delta_c_curr == 0.0,
461 TrialStatus::DcGt0DxGt0 | TrialStatus::NoTest => true,
462 };
463 if !probe_intact {
464 self.test_status = TrialStatus::NoTest;
465 }
466
467 let undet = probe_intact
468 && (matches!(self.hess_degenerate, DegenType::NotYetDetermined)
469 || matches!(self.jac_degenerate, DegenType::NotYetDetermined));
470 if undet {
471 match self.test_status {
472 TrialStatus::DcEq0DxEq0 => {
473 debug_assert!(self.delta_x_curr == 0.0 && self.delta_c_curr == 0.0);
474 if matches!(self.jac_degenerate, DegenType::NotYetDetermined) {
475 let v = self.delta_cd(mu);
476 self.delta_c_curr = v;
477 self.delta_d_curr = v;
478 self.test_status = TrialStatus::DcGt0DxEq0;
479 } else {
480 debug_assert!(matches!(self.hess_degenerate, DegenType::NotYetDetermined));
481 if !self.get_deltas_for_wrong_inertia(
482 &mut delta_x,
483 &mut delta_s,
484 &mut delta_c,
485 &mut delta_d,
486 ip_data,
487 ) {
488 return None;
489 }
490 self.test_status = TrialStatus::DcEq0DxGt0;
491 }
492 }
493 TrialStatus::DcGt0DxEq0 => {
494 debug_assert!(self.delta_x_curr == 0.0 && self.delta_c_curr > 0.0);
495 debug_assert!(matches!(self.jac_degenerate, DegenType::NotYetDetermined));
496 if !self.perturb_always_cd {
497 self.delta_c_curr = 0.0;
498 self.delta_d_curr = 0.0;
499 if !self.get_deltas_for_wrong_inertia(
500 &mut delta_x,
501 &mut delta_s,
502 &mut delta_c,
503 &mut delta_d,
504 ip_data,
505 ) {
506 return None;
507 }
508 self.test_status = TrialStatus::DcEq0DxGt0;
509 } else if !self.get_deltas_for_wrong_inertia(
510 &mut delta_x,
511 &mut delta_s,
512 &mut delta_c,
513 &mut delta_d,
514 ip_data,
515 ) {
516 return None;
517 } else {
518 self.test_status = TrialStatus::DcGt0DxGt0;
519 }
520 }
521 TrialStatus::DcEq0DxGt0 => {
522 debug_assert!(self.delta_x_curr > 0.0 && self.delta_c_curr == 0.0);
523 let v = self.delta_cd(mu);
524 self.delta_c_curr = v;
525 self.delta_d_curr = v;
526 if !self.get_deltas_for_wrong_inertia(
527 &mut delta_x,
528 &mut delta_s,
529 &mut delta_c,
530 &mut delta_d,
531 ip_data,
532 ) {
533 return None;
534 }
535 self.test_status = TrialStatus::DcGt0DxGt0;
536 }
537 TrialStatus::DcGt0DxGt0 => {
538 if !self.get_deltas_for_wrong_inertia(
539 &mut delta_x,
540 &mut delta_s,
541 &mut delta_c,
542 &mut delta_d,
543 ip_data,
544 ) {
545 return None;
546 }
547 }
548 TrialStatus::NoTest => {
549 debug_assert!(false, "perturb_for_singular: NoTest in undetermined branch");
550 }
551 }
552 } else if self.delta_c_curr > 0.0 {
553 if !self.get_deltas_for_wrong_inertia(
555 &mut delta_x,
556 &mut delta_s,
557 &mut delta_c,
558 &mut delta_d,
559 ip_data,
560 ) {
561 return None;
562 }
563 } else {
564 let v = self.delta_cd(mu);
565 self.delta_c_curr = v;
566 self.delta_d_curr = v;
567 append_info(ip_data, "L");
568 }
569
570 let out = Deltas {
571 delta_x: self.delta_x_curr,
572 delta_s: self.delta_s_curr,
573 delta_c: self.delta_c_curr,
574 delta_d: self.delta_d_curr,
575 };
576 set_info_regu_x(ip_data, out.delta_x);
577 Some(out)
578 }
579
580 pub fn perturb_for_wrong_inertia(
583 &mut self,
584 mu: Number,
585 ip_data: Option<&dyn PerturbationSink>,
586 ) -> Option<Deltas> {
587 if std::env::var_os("POUNCE_DBG_PERT").is_some() {
588 if let Some(d) = ip_data {
589 d.debug(&format!(
590 "[PERT] iter={} WRONG_INERTIA mu={:.2e} dx_last={:.2e} dx_curr={:.2e}",
591 d.iter_count(),
592 mu,
593 self.delta_x_last,
594 self.delta_x_curr
595 ));
596 }
597 }
598 self.finalize_test(ip_data);
599 self.withdrawal_untested = false;
602 self.maybe_withdraw_delta_c(ip_data);
603
604 let mut delta_x = 0.0;
605 let mut delta_s = 0.0;
606 let mut delta_c = 0.0;
607 let mut delta_d = 0.0;
608 let mut ok = self.get_deltas_for_wrong_inertia(
609 &mut delta_x,
610 &mut delta_s,
611 &mut delta_c,
612 &mut delta_d,
613 ip_data,
614 );
615 if !ok && self.delta_c_curr == 0.0 {
622 debug_assert_eq!(self.delta_d_curr, 0.0);
623 let v = self.delta_cd(mu);
624 self.delta_c_curr = v;
625 self.delta_d_curr = v;
626 self.delta_x_curr = 0.0;
627 self.delta_s_curr = 0.0;
628 self.test_status = TrialStatus::NoTest;
629 if matches!(self.hess_degenerate, DegenType::Degenerate) {
630 self.hess_degenerate = DegenType::NotYetDetermined;
631 }
632 ok = self.get_deltas_for_wrong_inertia(
633 &mut delta_x,
634 &mut delta_s,
635 &mut delta_c,
636 &mut delta_d,
637 ip_data,
638 );
639 }
640 if !ok {
641 return None;
642 }
643 Some(Deltas {
644 delta_x,
645 delta_s,
646 delta_c,
647 delta_d,
648 })
649 }
650
651 fn maybe_withdraw_delta_c(&mut self, ip_data: Option<&dyn PerturbationSink>) {
677 if self.delta_c_max_rungs <= 0
678 || self.delta_c_abandoned
679 || self.delta_c_reinstated
680 || self.delta_c_curr <= 0.0
681 {
682 return;
683 }
684 self.delta_c_rungs += 1;
685 if self.delta_c_rungs < self.delta_c_max_rungs {
686 return;
687 }
688 self.delta_x_at_withdrawal = self.delta_x_curr;
689 self.withdrawal_untested = true;
690 self.delta_c_curr = 0.0;
691 self.delta_d_curr = 0.0;
692 self.delta_x_curr = 0.0;
693 self.delta_s_curr = 0.0;
694 self.delta_c_abandoned = true;
695 self.test_status = TrialStatus::NoTest;
696 append_info(ip_data, "w");
697 }
698
699 pub fn current_perturbation(&self) -> Deltas {
701 Deltas {
702 delta_x: self.delta_x_curr,
703 delta_s: self.delta_s_curr,
704 delta_c: self.delta_c_curr,
705 delta_d: self.delta_d_curr,
706 }
707 }
708
709 fn get_deltas_for_wrong_inertia(
713 &mut self,
714 delta_x: &mut Number,
715 delta_s: &mut Number,
716 delta_c: &mut Number,
717 delta_d: &mut Number,
718 ip_data: Option<&dyn PerturbationSink>,
719 ) -> bool {
720 if self.delta_x_curr == 0.0 {
721 self.delta_x_curr = if self.delta_x_last == 0.0 {
722 self.delta_xs_init
723 } else {
724 self.delta_xs_min
725 .max(self.delta_x_last * self.delta_xs_dec_fact)
726 };
727 } else if self.delta_x_last == 0.0 || 1e5 * self.delta_x_last < self.delta_x_curr {
728 self.delta_x_curr *= self.delta_xs_first_inc_fact;
729 } else {
730 self.delta_x_curr *= self.delta_xs_inc_fact;
731 }
732 if self.delta_x_curr > self.delta_xs_max {
733 self.delta_x_last = 0.0;
734 self.delta_s_last = 0.0;
735 self.delta_x_curr = 0.0;
747 self.delta_s_curr = 0.0;
748 append_info(ip_data, "dx");
749 return false;
750 }
751 self.delta_s_curr = self.delta_x_curr * self.delta_s_fact_effective();
754
755 *delta_x = self.delta_x_curr;
756 *delta_s = self.delta_s_curr;
757 *delta_c = self.delta_c_curr;
758 *delta_d = self.delta_d_curr;
759 set_info_regu_x(ip_data, *delta_x);
760 self.get_deltas_for_wrong_inertia_called = true;
761 true
762 }
763
764 fn delta_cd(&self, mu: Number) -> Number {
765 self.delta_cd_val * mu.powf(self.delta_cd_exp)
766 }
767
768 fn finalize_test(&mut self, ip_data: Option<&dyn PerturbationSink>) {
772 match self.test_status {
773 TrialStatus::NoTest => (),
774 TrialStatus::DcEq0DxEq0 => {
775 if matches!(self.hess_degenerate, DegenType::NotYetDetermined)
776 && matches!(self.jac_degenerate, DegenType::NotYetDetermined)
777 {
778 self.hess_degenerate = DegenType::NotDegenerate;
779 self.jac_degenerate = DegenType::NotDegenerate;
780 append_info(ip_data, "Nhj ");
781 } else if matches!(self.hess_degenerate, DegenType::NotYetDetermined) {
782 self.hess_degenerate = DegenType::NotDegenerate;
783 append_info(ip_data, "Nh ");
784 } else if matches!(self.jac_degenerate, DegenType::NotYetDetermined) {
785 self.jac_degenerate = DegenType::NotDegenerate;
786 append_info(ip_data, "Nj ");
787 }
788 }
789 TrialStatus::DcGt0DxEq0 => {
790 if matches!(self.hess_degenerate, DegenType::NotYetDetermined) {
791 self.hess_degenerate = DegenType::NotDegenerate;
792 append_info(ip_data, "Nh ");
793 }
794 if matches!(self.jac_degenerate, DegenType::NotYetDetermined) {
795 self.degen_iters += 1;
796 if self.degen_iters >= self.degen_iters_max {
797 self.jac_degenerate = DegenType::Degenerate;
798 append_info(ip_data, "Dj ");
799 }
800 append_info(ip_data, "L");
801 }
802 }
803 TrialStatus::DcEq0DxGt0 => {
804 if matches!(self.jac_degenerate, DegenType::NotYetDetermined) {
805 self.jac_degenerate = DegenType::NotDegenerate;
806 append_info(ip_data, "Nj ");
807 }
808 if matches!(self.hess_degenerate, DegenType::NotYetDetermined) {
809 self.degen_iters += 1;
810 if self.degen_iters >= self.degen_iters_max {
811 self.hess_degenerate = DegenType::Degenerate;
812 append_info(ip_data, "Dh ");
813 }
814 }
815 }
816 TrialStatus::DcGt0DxGt0 => {
817 self.degen_iters += 1;
818 if self.degen_iters >= self.degen_iters_max {
819 self.hess_degenerate = DegenType::Degenerate;
820 self.jac_degenerate = DegenType::Degenerate;
821 append_info(ip_data, "Dhj ");
822 }
823 append_info(ip_data, "L");
824 }
825 }
826 }
827}
828
829fn append_info(sink: Option<&dyn PerturbationSink>, s: &str) {
830 if let Some(h) = sink {
831 h.append_info(s);
832 }
833}
834
835fn set_info_regu_x(sink: Option<&dyn PerturbationSink>, v: Number) {
836 if let Some(h) = sink {
837 h.set_regu_x(v);
838 }
839}
840
841#[cfg(test)]
842mod tests {
843 use super::*;
844
845 #[test]
851 fn the_slack_row_budget_scales_only_above_the_cap() {
852 let mut h = PdPerturbationHandler::new();
853 h.delta_s_rows = 10_005;
855 assert_eq!(h.delta_s_fact_effective(), 1.0);
856
857 h.delta_s_max_rows = 1000;
858 for rows in [0, 1, 999, 1000] {
859 h.delta_s_rows = rows;
860 assert_eq!(
861 h.delta_s_fact_effective(),
862 1.0,
863 "rows={rows} is below the cap"
864 );
865 }
866 h.delta_s_rows = 2000;
867 assert!((h.delta_s_fact_effective() - 0.5).abs() < 1e-15);
868 h.delta_s_rows = 10_005;
869 assert!((h.delta_s_fact_effective() - 1000.0 / 10_005.0).abs() < 1e-15);
870
871 h.delta_s_fact = 0.03;
873 h.delta_s_rows = 500;
874 assert!((h.delta_s_fact_effective() - 0.03).abs() < 1e-15);
875 h.delta_s_rows = 2000;
876 assert!((h.delta_s_fact_effective() - 0.015).abs() < 1e-15);
877
878 h.delta_s_max_rows = 0;
880 assert!((h.delta_s_fact_effective() - 0.03).abs() < 1e-15);
881 }
882
883 #[test]
886 fn the_ladder_applies_the_effective_slack_factor() {
887 let mut h = PdPerturbationHandler::new();
888 h.delta_s_max_rows = 1000;
889 h.delta_s_rows = 4000;
890 h.consider_new_system(0.1, None).unwrap();
891 for _ in 0..3 {
892 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
893 assert!(d.delta_x > 0.0);
894 assert!(
895 (d.delta_s - 0.25 * d.delta_x).abs() <= 1e-15 * d.delta_x,
896 "delta_s={:e} delta_x={:e}",
897 d.delta_s,
898 d.delta_x
899 );
900 }
901 let mut u = PdPerturbationHandler::new();
903 u.delta_s_rows = 4000;
904 u.consider_new_system(0.1, None).unwrap();
905 let d = u.perturb_for_wrong_inertia(0.1, None).unwrap();
906 assert_eq!(d.delta_s, d.delta_x);
907 }
908
909 #[test]
917 fn an_overflowed_ladder_does_not_poison_the_next_system() {
918 let mut h = PdPerturbationHandler::new();
919 h.consider_new_system(0.1, None).unwrap();
920 let mut rungs = 0;
921 while h.perturb_for_wrong_inertia(0.1, None).is_some() {
922 rungs += 1;
923 assert!(rungs < 1000, "the ladder never reached delta_xs_max");
924 }
925
926 h.consider_new_system(0.1, None).unwrap();
927 assert_eq!(h.delta_x_last, 0.0, "the overflowed rung was remembered");
928 let d = h
929 .perturb_for_wrong_inertia(0.1, None)
930 .expect("a fresh system's first rung must not fail");
931 assert!(
932 d.delta_x < h.delta_xs_max / 1e6,
933 "the next ladder started at {:e}, near the cap {:e}",
934 d.delta_x,
935 h.delta_xs_max,
936 );
937 }
938
939 #[test]
943 fn delta_c_is_withdrawn_after_the_ladder_has_climbed_without_it() {
944 let mut h = PdPerturbationHandler::new();
945 h.consider_new_system(0.1, None).unwrap();
946
947 let d = h.perturb_for_singular(0.1, None).unwrap();
949 assert!(d.delta_c > 0.0, "δ_c should be the first response");
950 assert_eq!(d.delta_x, 0.0);
951
952 for rung in 1..=2 {
954 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
955 assert!(d.delta_c > 0.0, "δ_c withdrawn early, at rung {rung}");
956 assert!(d.delta_x > 0.0);
957 }
958
959 let before = h.delta_x_curr;
962 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
963 assert_eq!(d.delta_c, 0.0, "δ_c was not withdrawn at the third rung");
964 assert_eq!(d.delta_d, 0.0);
965 assert!(
966 d.delta_x > 0.0 && d.delta_x < before,
967 "the ladder did not restart"
968 );
969 }
970
971 #[test]
976 fn a_withdrawn_delta_c_is_not_raised_again_until_the_next_iterate() {
977 let mut h = PdPerturbationHandler::new();
978 h.consider_new_system(0.1, None).unwrap();
979 h.perturb_for_singular(0.1, None).unwrap();
980 for _ in 0..3 {
981 h.perturb_for_wrong_inertia(0.1, None).unwrap();
982 }
983 assert!(h.delta_c_abandoned);
984
985 let d = h.perturb_for_singular(0.1, None).unwrap();
986 assert_eq!(d.delta_c, 0.0, "δ_c came back inside the same aug-system");
987 assert!(d.delta_x > 0.0, "the `Singular` was not answered at all");
988
989 h.consider_new_system(0.1, None).unwrap();
992 assert!(!h.delta_c_abandoned);
993 assert_eq!(h.delta_c_rungs, 0);
994 let d = h.perturb_for_singular(0.1, None).unwrap();
995 assert!(d.delta_c > 0.0, "δ_c is still latched off a new aug-system");
996 }
997
998 #[test]
1002 fn one_rung_under_delta_c_leaves_it_alone() {
1003 let mut h = PdPerturbationHandler::new();
1004 h.consider_new_system(0.1, None).unwrap();
1005 h.perturb_for_singular(0.1, None).unwrap();
1006 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
1007 assert!(d.delta_c > 0.0);
1008 h.consider_new_system(0.1, None).unwrap();
1010 assert_eq!(h.delta_c_rungs, 0);
1011 }
1012
1013 #[test]
1016 fn zero_max_rungs_disables_the_walkback() {
1017 let mut h = PdPerturbationHandler::new();
1018 h.delta_c_max_rungs = 0;
1019 h.consider_new_system(0.1, None).unwrap();
1020 h.perturb_for_singular(0.1, None).unwrap();
1021 let mut last = 0.0;
1022 for _ in 0..6 {
1023 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
1024 assert!(d.delta_c > 0.0, "δ_c was withdrawn with the walk-back off");
1025 assert!(d.delta_x > last, "the ladder stopped climbing");
1026 last = d.delta_x;
1027 }
1028 assert!(!h.delta_c_abandoned);
1029 }
1030
1031 #[test]
1032 fn first_wrong_inertia_perturbation_is_delta_xs_init() {
1033 let mut h = PdPerturbationHandler::new();
1034 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
1035 assert!((d.delta_x - 1e-4).abs() < 1e-20);
1037 assert_eq!(d.delta_x, d.delta_s);
1038 assert_eq!(d.delta_c, 0.0);
1039 assert_eq!(d.delta_d, 0.0);
1040 }
1041
1042 #[test]
1043 fn second_perturbation_uses_first_inc_fact() {
1044 let mut h = PdPerturbationHandler::new();
1048 let d1 = h.perturb_for_wrong_inertia(0.1, None).unwrap();
1049 let d2 = h.perturb_for_wrong_inertia(0.1, None).unwrap();
1050 assert!((d2.delta_x - d1.delta_x * 100.0).abs() < 1e-15);
1051 }
1052
1053 #[test]
1054 fn third_perturbation_uses_inc_fact() {
1055 let mut h = PdPerturbationHandler::new();
1058 h.delta_x_curr = 1e-2;
1059 h.delta_x_last = 1e-2;
1060 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
1061 assert!((d.delta_x - 1e-2 * 8.0).abs() < 1e-15);
1062 }
1063
1064 #[test]
1065 fn perturbation_caps_at_max_when_dcd_already_active() {
1066 let mut h = PdPerturbationHandler::new();
1071 h.delta_x_curr = h.delta_xs_max;
1072 h.delta_c_curr = 1e-4;
1073 h.delta_d_curr = 1e-4;
1074 assert!(h.perturb_for_wrong_inertia(0.1, None).is_none());
1075 }
1076
1077 #[test]
1078 fn consider_new_system_with_perturb_always_cd_seeds_dcd() {
1079 let mut h = PdPerturbationHandler::new();
1080 h.set_perturb_always_cd(true);
1081 let mu = 0.1;
1082 let d = h.consider_new_system(mu, None).unwrap();
1083 let expected = h.delta_cd_val * mu.powf(h.delta_cd_exp);
1084 assert!((d.delta_c - expected).abs() < 1e-15);
1085 assert!((d.delta_d - expected).abs() < 1e-15);
1086 assert_eq!(d.delta_x, 0.0);
1087 assert_eq!(d.delta_s, 0.0);
1088 }
1089
1090 #[test]
1091 fn consider_new_system_default_zeros_dcd() {
1092 let mut h = PdPerturbationHandler::new();
1093 let d = h.consider_new_system(0.1, None).unwrap();
1094 assert_eq!(
1095 d,
1096 Deltas {
1097 delta_x: 0.0,
1098 delta_s: 0.0,
1099 delta_c: 0.0,
1100 delta_d: 0.0
1101 }
1102 );
1103 }
1104
1105 #[test]
1106 fn singular_in_test_dc_eq0_dx_eq0_seeds_dcd() {
1107 let mut h = PdPerturbationHandler::new();
1108 let _ = h.consider_new_system(0.1, None).unwrap();
1109 assert_eq!(h.test_status, TrialStatus::DcEq0DxEq0);
1113 let d = h.perturb_for_singular(0.1, None).unwrap();
1114 let expected = h.delta_cd_val * (0.1_f64).powf(h.delta_cd_exp);
1115 assert!((d.delta_c - expected).abs() < 1e-15);
1116 assert!((d.delta_d - expected).abs() < 1e-15);
1117 assert_eq!(d.delta_x, 0.0);
1118 assert_eq!(h.test_status, TrialStatus::DcGt0DxEq0);
1119 }
1120
1121 #[test]
1122 fn singular_when_determined_with_dc_zero_seeds_dcd() {
1123 let mut h = PdPerturbationHandler::new();
1124 h.hess_degenerate = DegenType::NotDegenerate;
1125 h.jac_degenerate = DegenType::NotDegenerate;
1126 h.test_status = TrialStatus::NoTest;
1127 let d = h.perturb_for_singular(0.1, None).unwrap();
1128 let expected = h.delta_cd_val * (0.1_f64).powf(h.delta_cd_exp);
1129 assert!((d.delta_c - expected).abs() < 1e-15);
1130 }
1131
1132 #[test]
1143 fn a_second_singular_verdict_from_the_ladder_does_not_trip_the_probe() {
1144 let mut h = PdPerturbationHandler::new();
1145 let _ = h.consider_new_system(0.1, None).unwrap();
1146 let _ = h.perturb_for_singular(0.1, None).unwrap();
1149 assert_eq!(h.test_status, TrialStatus::DcGt0DxEq0);
1150 assert_eq!(h.jac_degenerate, DegenType::NotYetDetermined);
1151 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
1155 assert!(d.delta_x > 0.0);
1156 assert_eq!(h.jac_degenerate, DegenType::NotYetDetermined);
1157 assert_eq!(h.test_status, TrialStatus::DcGt0DxEq0);
1158 let d = h
1161 .perturb_for_singular(0.1, None)
1162 .expect("a singular verdict from the ladder must not be fatal");
1163 assert!(
1164 d.delta_c > 0.0,
1165 "δ_c was dropped by the abandoned probe: {}",
1166 d.delta_c,
1167 );
1168 assert_eq!(
1169 h.test_status,
1170 TrialStatus::NoTest,
1171 "a probe whose precondition no longer holds must be abandoned",
1172 );
1173 }
1174
1175 #[test]
1179 fn singular_after_a_delta_x_step_abandons_the_probe() {
1180 let mut h = PdPerturbationHandler::new();
1181 let _ = h.consider_new_system(0.1, None).unwrap();
1184 assert_eq!(h.test_status, TrialStatus::DcEq0DxEq0);
1185 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
1188 assert!(d.delta_x > 0.0);
1189 let d = h.perturb_for_singular(0.1, None).unwrap();
1191 let expected_cd = h.delta_cd_val * (0.1_f64).powf(h.delta_cd_exp);
1192 assert!(
1193 (d.delta_c - expected_cd).abs() < 1e-20,
1194 "δ_c was not raised: {}",
1195 d.delta_c,
1196 );
1197 assert_eq!(d.delta_c, d.delta_d);
1198 assert_eq!(
1199 d.delta_x, h.delta_xs_init,
1200 "the δ_x ladder lost the rung it had already paid for",
1201 );
1202 assert_eq!(
1203 h.test_status,
1204 TrialStatus::NoTest,
1205 "a probe whose precondition no longer holds must be abandoned, \
1206 not carried into finalize_test",
1207 );
1208 }
1209
1210 #[test]
1211 fn finalize_test_sets_not_degenerate_after_dc_eq0_dx_eq0_pass() {
1212 let mut h = PdPerturbationHandler::new();
1213 let _ = h.consider_new_system(0.1, None).unwrap();
1214 let _ = h.consider_new_system(0.1, None).unwrap();
1218 assert_eq!(h.hess_degenerate, DegenType::NotDegenerate);
1219 assert_eq!(h.jac_degenerate, DegenType::NotDegenerate);
1220 }
1221
1222 #[test]
1223 fn current_perturbation_returns_committed_values() {
1224 let mut h = PdPerturbationHandler::new();
1225 h.delta_x_curr = 1.0;
1226 h.delta_s_curr = 2.0;
1227 h.delta_c_curr = 3.0;
1228 h.delta_d_curr = 4.0;
1229 let d = h.current_perturbation();
1230 assert_eq!(d.delta_x, 1.0);
1231 assert_eq!(d.delta_s, 2.0);
1232 assert_eq!(d.delta_c, 3.0);
1233 assert_eq!(d.delta_d, 4.0);
1234 }
1235
1236 fn withdraw(h: &mut PdPerturbationHandler) -> Number {
1240 h.consider_new_system(0.1, None).unwrap();
1241 h.perturb_for_singular(0.1, None).unwrap();
1242 let mut at = 0.0;
1243 for _ in 0..3 {
1244 at = h.delta_x_curr;
1245 h.perturb_for_wrong_inertia(0.1, None).unwrap();
1246 }
1247 assert!(h.delta_c_abandoned, "precondition: δ_c was withdrawn");
1248 assert_eq!(h.delta_x_at_withdrawal, at);
1249 at
1250 }
1251
1252 #[test]
1260 fn a_certified_singular_from_the_first_delta_c_free_factor_reinstates_delta_c() {
1261 let mut h = PdPerturbationHandler::new();
1262 let at = withdraw(&mut h);
1263 let d = h.perturb_for_singular_with(0.1, true, None).unwrap();
1264 assert!(d.delta_c > 0.0, "δ_c was not reinstated");
1265 assert_eq!(d.delta_d, d.delta_c);
1266 assert!(
1267 d.delta_x > at,
1268 "the ladder restarted below the withdrawal rung ({} <= {at})",
1269 d.delta_x
1270 );
1271 assert!(h.delta_c_reinstated && !h.delta_c_abandoned);
1272
1273 for _ in 0..6 {
1276 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
1277 assert!(d.delta_c > 0.0, "δ_c withdrawn a second time");
1278 }
1279 h.consider_new_system(0.1, None).unwrap();
1281 assert!(!h.delta_c_reinstated && !h.withdrawal_untested);
1282 }
1283
1284 #[test]
1290 fn an_uncertified_singular_does_not_reinstate_delta_c() {
1291 let mut h = PdPerturbationHandler::new();
1292 withdraw(&mut h);
1293 for _ in 0..5 {
1294 let d = h.perturb_for_singular_with(0.1, false, None).unwrap();
1295 assert_eq!(d.delta_c, 0.0, "δ_c came back on an untrusted report");
1296 assert!(d.delta_x > 0.0);
1297 }
1298 assert!(!h.delta_c_reinstated);
1299 }
1300
1301 #[test]
1307 fn a_certified_singular_later_in_the_climb_does_not_reinstate_delta_c() {
1308 let mut h = PdPerturbationHandler::new();
1309 withdraw(&mut h);
1310 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
1312 assert_eq!(d.delta_c, 0.0);
1313 assert!(!h.withdrawal_untested);
1314 h.perturb_for_singular_with(0.1, false, None).unwrap();
1316 let d = h.perturb_for_singular_with(0.1, true, None).unwrap();
1317 assert_eq!(d.delta_c, 0.0, "a late zero pivot reinstated δ_c");
1318 assert!(!h.delta_c_reinstated);
1319 }
1320
1321 #[test]
1324 fn an_uncertified_first_report_closes_the_window() {
1325 let mut h = PdPerturbationHandler::new();
1326 withdraw(&mut h);
1327 h.perturb_for_singular_with(0.1, false, None).unwrap();
1328 let d = h.perturb_for_singular_with(0.1, true, None).unwrap();
1329 assert_eq!(d.delta_c, 0.0);
1330 }
1331
1332 #[test]
1335 fn a_certified_singular_before_any_withdrawal_is_the_ordinary_path() {
1336 let mut h = PdPerturbationHandler::new();
1337 h.consider_new_system(0.1, None).unwrap();
1338 let a = h.perturb_for_singular_with(0.1, true, None).unwrap();
1339 let mut g = PdPerturbationHandler::new();
1340 g.consider_new_system(0.1, None).unwrap();
1341 let b = g.perturb_for_singular(0.1, None).unwrap();
1342 assert_eq!(a, b);
1343 assert!(!h.delta_c_reinstated && !h.delta_c_abandoned);
1344 }
1345}