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
90 pub delta_x_curr: Number,
92 pub delta_s_curr: Number,
93 pub delta_c_curr: Number,
94 pub delta_d_curr: Number,
95 pub delta_x_last: Number,
96 pub delta_s_last: Number,
97 pub delta_c_last: Number,
98 pub delta_d_last: Number,
99 pub get_deltas_for_wrong_inertia_called: bool,
100 pub hess_degenerate: DegenType,
101 pub jac_degenerate: DegenType,
102 pub degen_iters: Index,
103 pub test_status: TrialStatus,
104 pub delta_c_rungs: Index,
108 pub delta_c_abandoned: bool,
114 pub delta_c_max_rungs: Index,
118 pub delta_x_at_withdrawal: Number,
123 pub delta_c_reinstated: bool,
127 pub withdrawal_untested: bool,
136}
137
138impl Default for PdPerturbationHandler {
139 fn default() -> Self {
140 Self {
141 delta_xs_max: 1e20,
142 delta_xs_min: 1e-20,
143 delta_xs_first_inc_fact: 100.0,
144 delta_xs_inc_fact: 8.0,
145 delta_xs_dec_fact: 1.0 / 3.0,
146 delta_xs_init: 1e-4,
147 delta_cd_val: 1e-8,
148 delta_cd_exp: 0.25,
149 perturb_always_cd: false,
150 reset_last: false,
151 degen_iters_max: 3,
152 delta_x_curr: 0.0,
153 delta_s_curr: 0.0,
154 delta_c_curr: 0.0,
155 delta_d_curr: 0.0,
156 delta_x_last: 0.0,
157 delta_s_last: 0.0,
158 delta_c_last: 0.0,
159 delta_d_last: 0.0,
160 get_deltas_for_wrong_inertia_called: false,
161 hess_degenerate: DegenType::NotYetDetermined,
162 jac_degenerate: DegenType::NotYetDetermined,
163 degen_iters: 0,
164 test_status: TrialStatus::NoTest,
165 delta_c_rungs: 0,
171 delta_c_abandoned: false,
172 delta_c_max_rungs: 3,
173 delta_x_at_withdrawal: 0.0,
174 delta_c_reinstated: false,
175 withdrawal_untested: false,
176 }
177 }
178}
179
180#[derive(Debug, Clone, Copy, PartialEq)]
182pub struct Deltas {
183 pub delta_x: Number,
184 pub delta_s: Number,
185 pub delta_c: Number,
186 pub delta_d: Number,
187}
188
189impl PdPerturbationHandler {
190 pub fn new() -> Self {
191 Self::default()
192 }
193
194 pub fn set_perturb_always_cd(&mut self, on: bool) {
197 self.perturb_always_cd = on;
198 self.jac_degenerate = if on {
199 DegenType::NotDegenerate
200 } else {
201 DegenType::NotYetDetermined
202 };
203 }
204
205 pub fn consider_new_system(
210 &mut self,
211 mu: Number,
212 ip_data: Option<&dyn PerturbationSink>,
213 ) -> Option<Deltas> {
214 self.finalize_test(ip_data);
215
216 if self.reset_last {
219 self.delta_x_last = self.delta_x_curr;
220 self.delta_s_last = self.delta_s_curr;
221 self.delta_c_last = self.delta_c_curr;
222 self.delta_d_last = self.delta_d_curr;
223 } else {
224 if self.delta_x_curr > 0.0 {
225 self.delta_x_last = self.delta_x_curr;
226 }
227 if self.delta_s_curr > 0.0 {
228 self.delta_s_last = self.delta_s_curr;
229 }
230 if self.delta_c_curr > 0.0 {
231 self.delta_c_last = self.delta_c_curr;
232 }
233 if self.delta_d_curr > 0.0 {
234 self.delta_d_last = self.delta_d_curr;
235 }
236 }
237
238 let undet = matches!(self.hess_degenerate, DegenType::NotYetDetermined)
239 || matches!(self.jac_degenerate, DegenType::NotYetDetermined);
240 self.test_status = if undet {
241 if self.perturb_always_cd {
242 TrialStatus::DcGt0DxEq0
243 } else {
244 TrialStatus::DcEq0DxEq0
245 }
246 } else {
247 TrialStatus::NoTest
248 };
249
250 let mut delta_c = if matches!(self.jac_degenerate, DegenType::Degenerate) {
251 let v = self.delta_cd(mu);
252 self.delta_c_curr = v;
253 append_info(ip_data, "l");
254 v
255 } else if self.perturb_always_cd {
256 let v = self.delta_cd(mu);
257 self.delta_c_curr = v;
258 v
259 } else {
260 self.delta_c_curr = 0.0;
261 0.0
262 };
263 let mut delta_d = delta_c;
264 self.delta_d_curr = delta_d;
265
266 let mut delta_x = 0.0;
267 let mut delta_s = 0.0;
268
269 if matches!(self.hess_degenerate, DegenType::Degenerate) {
270 self.delta_x_curr = 0.0;
271 self.delta_s_curr = 0.0;
272 if !self.get_deltas_for_wrong_inertia(
273 &mut delta_x,
274 &mut delta_s,
275 &mut delta_c,
276 &mut delta_d,
277 ip_data,
278 ) {
279 return None;
280 }
281 }
282
283 self.delta_x_curr = delta_x;
284 self.delta_s_curr = delta_s;
285 self.delta_c_curr = delta_c;
286 self.delta_d_curr = delta_d;
287 set_info_regu_x(ip_data, delta_x);
288 self.get_deltas_for_wrong_inertia_called = false;
289 self.delta_c_rungs = 0;
291 self.delta_c_abandoned = false;
292 self.delta_x_at_withdrawal = 0.0;
293 self.delta_c_reinstated = false;
294 self.withdrawal_untested = false;
295
296 Some(Deltas {
297 delta_x,
298 delta_s,
299 delta_c,
300 delta_d,
301 })
302 }
303
304 pub fn perturb_for_singular(
307 &mut self,
308 mu: Number,
309 ip_data: Option<&dyn PerturbationSink>,
310 ) -> Option<Deltas> {
311 self.perturb_for_singular_with(mu, false, ip_data)
312 }
313
314 pub fn perturb_for_singular_with(
328 &mut self,
329 mu: Number,
330 certified: bool,
331 ip_data: Option<&dyn PerturbationSink>,
332 ) -> Option<Deltas> {
333 let mut delta_x = 0.0;
334 let mut delta_s = 0.0;
335 let mut delta_c = 0.0;
336 let mut delta_d = 0.0;
337
338 let first_without_delta_c = self.withdrawal_untested;
345 self.withdrawal_untested = false;
346 if self.delta_c_abandoned && certified && first_without_delta_c && !self.delta_c_reinstated
347 {
348 self.delta_c_abandoned = false;
350 self.delta_c_reinstated = true;
351 let v = self.delta_cd(mu);
352 self.delta_c_curr = v;
353 self.delta_d_curr = v;
354 if self.delta_x_curr < self.delta_x_at_withdrawal {
358 self.delta_x_curr = self.delta_x_at_withdrawal;
359 self.delta_s_curr = self.delta_x_at_withdrawal;
360 }
361 self.test_status = TrialStatus::NoTest;
362 append_info(ip_data, "W");
363 if !self.get_deltas_for_wrong_inertia(
364 &mut delta_x,
365 &mut delta_s,
366 &mut delta_c,
367 &mut delta_d,
368 ip_data,
369 ) {
370 return None;
371 }
372 set_info_regu_x(ip_data, self.delta_x_curr);
373 return Some(Deltas {
374 delta_x: self.delta_x_curr,
375 delta_s: self.delta_s_curr,
376 delta_c: self.delta_c_curr,
377 delta_d: self.delta_d_curr,
378 });
379 }
380 if self.delta_c_abandoned {
381 self.test_status = TrialStatus::NoTest;
382 if !self.get_deltas_for_wrong_inertia(
383 &mut delta_x,
384 &mut delta_s,
385 &mut delta_c,
386 &mut delta_d,
387 ip_data,
388 ) {
389 return None;
390 }
391 set_info_regu_x(ip_data, self.delta_x_curr);
392 return Some(Deltas {
393 delta_x: self.delta_x_curr,
394 delta_s: self.delta_s_curr,
395 delta_c: self.delta_c_curr,
396 delta_d: self.delta_d_curr,
397 });
398 }
399
400 let probe_intact = match self.test_status {
413 TrialStatus::DcEq0DxEq0 => self.delta_x_curr == 0.0 && self.delta_c_curr == 0.0,
414 TrialStatus::DcGt0DxEq0 => self.delta_x_curr == 0.0 && self.delta_c_curr > 0.0,
415 TrialStatus::DcEq0DxGt0 => self.delta_x_curr > 0.0 && self.delta_c_curr == 0.0,
416 TrialStatus::DcGt0DxGt0 | TrialStatus::NoTest => true,
417 };
418 if !probe_intact {
419 self.test_status = TrialStatus::NoTest;
420 }
421
422 let undet = probe_intact
423 && (matches!(self.hess_degenerate, DegenType::NotYetDetermined)
424 || matches!(self.jac_degenerate, DegenType::NotYetDetermined));
425 if undet {
426 match self.test_status {
427 TrialStatus::DcEq0DxEq0 => {
428 debug_assert!(self.delta_x_curr == 0.0 && self.delta_c_curr == 0.0);
429 if matches!(self.jac_degenerate, DegenType::NotYetDetermined) {
430 let v = self.delta_cd(mu);
431 self.delta_c_curr = v;
432 self.delta_d_curr = v;
433 self.test_status = TrialStatus::DcGt0DxEq0;
434 } else {
435 debug_assert!(matches!(self.hess_degenerate, DegenType::NotYetDetermined));
436 if !self.get_deltas_for_wrong_inertia(
437 &mut delta_x,
438 &mut delta_s,
439 &mut delta_c,
440 &mut delta_d,
441 ip_data,
442 ) {
443 return None;
444 }
445 self.test_status = TrialStatus::DcEq0DxGt0;
446 }
447 }
448 TrialStatus::DcGt0DxEq0 => {
449 debug_assert!(self.delta_x_curr == 0.0 && self.delta_c_curr > 0.0);
450 debug_assert!(matches!(self.jac_degenerate, DegenType::NotYetDetermined));
451 if !self.perturb_always_cd {
452 self.delta_c_curr = 0.0;
453 self.delta_d_curr = 0.0;
454 if !self.get_deltas_for_wrong_inertia(
455 &mut delta_x,
456 &mut delta_s,
457 &mut delta_c,
458 &mut delta_d,
459 ip_data,
460 ) {
461 return None;
462 }
463 self.test_status = TrialStatus::DcEq0DxGt0;
464 } else if !self.get_deltas_for_wrong_inertia(
465 &mut delta_x,
466 &mut delta_s,
467 &mut delta_c,
468 &mut delta_d,
469 ip_data,
470 ) {
471 return None;
472 } else {
473 self.test_status = TrialStatus::DcGt0DxGt0;
474 }
475 }
476 TrialStatus::DcEq0DxGt0 => {
477 debug_assert!(self.delta_x_curr > 0.0 && self.delta_c_curr == 0.0);
478 let v = self.delta_cd(mu);
479 self.delta_c_curr = v;
480 self.delta_d_curr = v;
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::DcGt0DxGt0;
491 }
492 TrialStatus::DcGt0DxGt0 => {
493 if !self.get_deltas_for_wrong_inertia(
494 &mut delta_x,
495 &mut delta_s,
496 &mut delta_c,
497 &mut delta_d,
498 ip_data,
499 ) {
500 return None;
501 }
502 }
503 TrialStatus::NoTest => {
504 debug_assert!(false, "perturb_for_singular: NoTest in undetermined branch");
505 }
506 }
507 } else if self.delta_c_curr > 0.0 {
508 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 }
518 } else {
519 let v = self.delta_cd(mu);
520 self.delta_c_curr = v;
521 self.delta_d_curr = v;
522 append_info(ip_data, "L");
523 }
524
525 let out = Deltas {
526 delta_x: self.delta_x_curr,
527 delta_s: self.delta_s_curr,
528 delta_c: self.delta_c_curr,
529 delta_d: self.delta_d_curr,
530 };
531 set_info_regu_x(ip_data, out.delta_x);
532 Some(out)
533 }
534
535 pub fn perturb_for_wrong_inertia(
538 &mut self,
539 mu: Number,
540 ip_data: Option<&dyn PerturbationSink>,
541 ) -> Option<Deltas> {
542 if std::env::var_os("POUNCE_DBG_PERT").is_some() {
543 if let Some(d) = ip_data {
544 d.debug(&format!(
545 "[PERT] iter={} WRONG_INERTIA mu={:.2e} dx_last={:.2e} dx_curr={:.2e}",
546 d.iter_count(),
547 mu,
548 self.delta_x_last,
549 self.delta_x_curr
550 ));
551 }
552 }
553 self.finalize_test(ip_data);
554 self.withdrawal_untested = false;
557 self.maybe_withdraw_delta_c(ip_data);
558
559 let mut delta_x = 0.0;
560 let mut delta_s = 0.0;
561 let mut delta_c = 0.0;
562 let mut delta_d = 0.0;
563 let mut ok = self.get_deltas_for_wrong_inertia(
564 &mut delta_x,
565 &mut delta_s,
566 &mut delta_c,
567 &mut delta_d,
568 ip_data,
569 );
570 if !ok && self.delta_c_curr == 0.0 {
577 debug_assert_eq!(self.delta_d_curr, 0.0);
578 let v = self.delta_cd(mu);
579 self.delta_c_curr = v;
580 self.delta_d_curr = v;
581 self.delta_x_curr = 0.0;
582 self.delta_s_curr = 0.0;
583 self.test_status = TrialStatus::NoTest;
584 if matches!(self.hess_degenerate, DegenType::Degenerate) {
585 self.hess_degenerate = DegenType::NotYetDetermined;
586 }
587 ok = self.get_deltas_for_wrong_inertia(
588 &mut delta_x,
589 &mut delta_s,
590 &mut delta_c,
591 &mut delta_d,
592 ip_data,
593 );
594 }
595 if !ok {
596 return None;
597 }
598 Some(Deltas {
599 delta_x,
600 delta_s,
601 delta_c,
602 delta_d,
603 })
604 }
605
606 fn maybe_withdraw_delta_c(&mut self, ip_data: Option<&dyn PerturbationSink>) {
632 if self.delta_c_max_rungs <= 0
633 || self.delta_c_abandoned
634 || self.delta_c_reinstated
635 || self.delta_c_curr <= 0.0
636 {
637 return;
638 }
639 self.delta_c_rungs += 1;
640 if self.delta_c_rungs < self.delta_c_max_rungs {
641 return;
642 }
643 self.delta_x_at_withdrawal = self.delta_x_curr;
644 self.withdrawal_untested = true;
645 self.delta_c_curr = 0.0;
646 self.delta_d_curr = 0.0;
647 self.delta_x_curr = 0.0;
648 self.delta_s_curr = 0.0;
649 self.delta_c_abandoned = true;
650 self.test_status = TrialStatus::NoTest;
651 append_info(ip_data, "w");
652 }
653
654 pub fn current_perturbation(&self) -> Deltas {
656 Deltas {
657 delta_x: self.delta_x_curr,
658 delta_s: self.delta_s_curr,
659 delta_c: self.delta_c_curr,
660 delta_d: self.delta_d_curr,
661 }
662 }
663
664 fn get_deltas_for_wrong_inertia(
668 &mut self,
669 delta_x: &mut Number,
670 delta_s: &mut Number,
671 delta_c: &mut Number,
672 delta_d: &mut Number,
673 ip_data: Option<&dyn PerturbationSink>,
674 ) -> bool {
675 if self.delta_x_curr == 0.0 {
676 self.delta_x_curr = if self.delta_x_last == 0.0 {
677 self.delta_xs_init
678 } else {
679 self.delta_xs_min
680 .max(self.delta_x_last * self.delta_xs_dec_fact)
681 };
682 } else if self.delta_x_last == 0.0 || 1e5 * self.delta_x_last < self.delta_x_curr {
683 self.delta_x_curr *= self.delta_xs_first_inc_fact;
684 } else {
685 self.delta_x_curr *= self.delta_xs_inc_fact;
686 }
687 if self.delta_x_curr > self.delta_xs_max {
688 self.delta_x_last = 0.0;
689 self.delta_s_last = 0.0;
690 self.delta_x_curr = 0.0;
702 self.delta_s_curr = 0.0;
703 append_info(ip_data, "dx");
704 return false;
705 }
706 self.delta_s_curr = self.delta_x_curr;
707
708 *delta_x = self.delta_x_curr;
709 *delta_s = self.delta_s_curr;
710 *delta_c = self.delta_c_curr;
711 *delta_d = self.delta_d_curr;
712 set_info_regu_x(ip_data, *delta_x);
713 self.get_deltas_for_wrong_inertia_called = true;
714 true
715 }
716
717 fn delta_cd(&self, mu: Number) -> Number {
718 self.delta_cd_val * mu.powf(self.delta_cd_exp)
719 }
720
721 fn finalize_test(&mut self, ip_data: Option<&dyn PerturbationSink>) {
725 match self.test_status {
726 TrialStatus::NoTest => (),
727 TrialStatus::DcEq0DxEq0 => {
728 if matches!(self.hess_degenerate, DegenType::NotYetDetermined)
729 && matches!(self.jac_degenerate, DegenType::NotYetDetermined)
730 {
731 self.hess_degenerate = DegenType::NotDegenerate;
732 self.jac_degenerate = DegenType::NotDegenerate;
733 append_info(ip_data, "Nhj ");
734 } else if matches!(self.hess_degenerate, DegenType::NotYetDetermined) {
735 self.hess_degenerate = DegenType::NotDegenerate;
736 append_info(ip_data, "Nh ");
737 } else if matches!(self.jac_degenerate, DegenType::NotYetDetermined) {
738 self.jac_degenerate = DegenType::NotDegenerate;
739 append_info(ip_data, "Nj ");
740 }
741 }
742 TrialStatus::DcGt0DxEq0 => {
743 if matches!(self.hess_degenerate, DegenType::NotYetDetermined) {
744 self.hess_degenerate = DegenType::NotDegenerate;
745 append_info(ip_data, "Nh ");
746 }
747 if matches!(self.jac_degenerate, DegenType::NotYetDetermined) {
748 self.degen_iters += 1;
749 if self.degen_iters >= self.degen_iters_max {
750 self.jac_degenerate = DegenType::Degenerate;
751 append_info(ip_data, "Dj ");
752 }
753 append_info(ip_data, "L");
754 }
755 }
756 TrialStatus::DcEq0DxGt0 => {
757 if matches!(self.jac_degenerate, DegenType::NotYetDetermined) {
758 self.jac_degenerate = DegenType::NotDegenerate;
759 append_info(ip_data, "Nj ");
760 }
761 if matches!(self.hess_degenerate, DegenType::NotYetDetermined) {
762 self.degen_iters += 1;
763 if self.degen_iters >= self.degen_iters_max {
764 self.hess_degenerate = DegenType::Degenerate;
765 append_info(ip_data, "Dh ");
766 }
767 }
768 }
769 TrialStatus::DcGt0DxGt0 => {
770 self.degen_iters += 1;
771 if self.degen_iters >= self.degen_iters_max {
772 self.hess_degenerate = DegenType::Degenerate;
773 self.jac_degenerate = DegenType::Degenerate;
774 append_info(ip_data, "Dhj ");
775 }
776 append_info(ip_data, "L");
777 }
778 }
779 }
780}
781
782fn append_info(sink: Option<&dyn PerturbationSink>, s: &str) {
783 if let Some(h) = sink {
784 h.append_info(s);
785 }
786}
787
788fn set_info_regu_x(sink: Option<&dyn PerturbationSink>, v: Number) {
789 if let Some(h) = sink {
790 h.set_regu_x(v);
791 }
792}
793
794#[cfg(test)]
795mod tests {
796 use super::*;
797
798 #[test]
806 fn an_overflowed_ladder_does_not_poison_the_next_system() {
807 let mut h = PdPerturbationHandler::new();
808 h.consider_new_system(0.1, None).unwrap();
809 let mut rungs = 0;
810 while h.perturb_for_wrong_inertia(0.1, None).is_some() {
811 rungs += 1;
812 assert!(rungs < 1000, "the ladder never reached delta_xs_max");
813 }
814
815 h.consider_new_system(0.1, None).unwrap();
816 assert_eq!(h.delta_x_last, 0.0, "the overflowed rung was remembered");
817 let d = h
818 .perturb_for_wrong_inertia(0.1, None)
819 .expect("a fresh system's first rung must not fail");
820 assert!(
821 d.delta_x < h.delta_xs_max / 1e6,
822 "the next ladder started at {:e}, near the cap {:e}",
823 d.delta_x,
824 h.delta_xs_max,
825 );
826 }
827
828 #[test]
832 fn delta_c_is_withdrawn_after_the_ladder_has_climbed_without_it() {
833 let mut h = PdPerturbationHandler::new();
834 h.consider_new_system(0.1, None).unwrap();
835
836 let d = h.perturb_for_singular(0.1, None).unwrap();
838 assert!(d.delta_c > 0.0, "δ_c should be the first response");
839 assert_eq!(d.delta_x, 0.0);
840
841 for rung in 1..=2 {
843 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
844 assert!(d.delta_c > 0.0, "δ_c withdrawn early, at rung {rung}");
845 assert!(d.delta_x > 0.0);
846 }
847
848 let before = h.delta_x_curr;
851 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
852 assert_eq!(d.delta_c, 0.0, "δ_c was not withdrawn at the third rung");
853 assert_eq!(d.delta_d, 0.0);
854 assert!(
855 d.delta_x > 0.0 && d.delta_x < before,
856 "the ladder did not restart"
857 );
858 }
859
860 #[test]
865 fn a_withdrawn_delta_c_is_not_raised_again_until_the_next_iterate() {
866 let mut h = PdPerturbationHandler::new();
867 h.consider_new_system(0.1, None).unwrap();
868 h.perturb_for_singular(0.1, None).unwrap();
869 for _ in 0..3 {
870 h.perturb_for_wrong_inertia(0.1, None).unwrap();
871 }
872 assert!(h.delta_c_abandoned);
873
874 let d = h.perturb_for_singular(0.1, None).unwrap();
875 assert_eq!(d.delta_c, 0.0, "δ_c came back inside the same aug-system");
876 assert!(d.delta_x > 0.0, "the `Singular` was not answered at all");
877
878 h.consider_new_system(0.1, None).unwrap();
881 assert!(!h.delta_c_abandoned);
882 assert_eq!(h.delta_c_rungs, 0);
883 let d = h.perturb_for_singular(0.1, None).unwrap();
884 assert!(d.delta_c > 0.0, "δ_c is still latched off a new aug-system");
885 }
886
887 #[test]
891 fn one_rung_under_delta_c_leaves_it_alone() {
892 let mut h = PdPerturbationHandler::new();
893 h.consider_new_system(0.1, None).unwrap();
894 h.perturb_for_singular(0.1, None).unwrap();
895 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
896 assert!(d.delta_c > 0.0);
897 h.consider_new_system(0.1, None).unwrap();
899 assert_eq!(h.delta_c_rungs, 0);
900 }
901
902 #[test]
905 fn zero_max_rungs_disables_the_walkback() {
906 let mut h = PdPerturbationHandler::new();
907 h.delta_c_max_rungs = 0;
908 h.consider_new_system(0.1, None).unwrap();
909 h.perturb_for_singular(0.1, None).unwrap();
910 let mut last = 0.0;
911 for _ in 0..6 {
912 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
913 assert!(d.delta_c > 0.0, "δ_c was withdrawn with the walk-back off");
914 assert!(d.delta_x > last, "the ladder stopped climbing");
915 last = d.delta_x;
916 }
917 assert!(!h.delta_c_abandoned);
918 }
919
920 #[test]
921 fn first_wrong_inertia_perturbation_is_delta_xs_init() {
922 let mut h = PdPerturbationHandler::new();
923 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
924 assert!((d.delta_x - 1e-4).abs() < 1e-20);
926 assert_eq!(d.delta_x, d.delta_s);
927 assert_eq!(d.delta_c, 0.0);
928 assert_eq!(d.delta_d, 0.0);
929 }
930
931 #[test]
932 fn second_perturbation_uses_first_inc_fact() {
933 let mut h = PdPerturbationHandler::new();
937 let d1 = h.perturb_for_wrong_inertia(0.1, None).unwrap();
938 let d2 = h.perturb_for_wrong_inertia(0.1, None).unwrap();
939 assert!((d2.delta_x - d1.delta_x * 100.0).abs() < 1e-15);
940 }
941
942 #[test]
943 fn third_perturbation_uses_inc_fact() {
944 let mut h = PdPerturbationHandler::new();
947 h.delta_x_curr = 1e-2;
948 h.delta_x_last = 1e-2;
949 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
950 assert!((d.delta_x - 1e-2 * 8.0).abs() < 1e-15);
951 }
952
953 #[test]
954 fn perturbation_caps_at_max_when_dcd_already_active() {
955 let mut h = PdPerturbationHandler::new();
960 h.delta_x_curr = h.delta_xs_max;
961 h.delta_c_curr = 1e-4;
962 h.delta_d_curr = 1e-4;
963 assert!(h.perturb_for_wrong_inertia(0.1, None).is_none());
964 }
965
966 #[test]
967 fn consider_new_system_with_perturb_always_cd_seeds_dcd() {
968 let mut h = PdPerturbationHandler::new();
969 h.set_perturb_always_cd(true);
970 let mu = 0.1;
971 let d = h.consider_new_system(mu, None).unwrap();
972 let expected = h.delta_cd_val * mu.powf(h.delta_cd_exp);
973 assert!((d.delta_c - expected).abs() < 1e-15);
974 assert!((d.delta_d - expected).abs() < 1e-15);
975 assert_eq!(d.delta_x, 0.0);
976 assert_eq!(d.delta_s, 0.0);
977 }
978
979 #[test]
980 fn consider_new_system_default_zeros_dcd() {
981 let mut h = PdPerturbationHandler::new();
982 let d = h.consider_new_system(0.1, None).unwrap();
983 assert_eq!(
984 d,
985 Deltas {
986 delta_x: 0.0,
987 delta_s: 0.0,
988 delta_c: 0.0,
989 delta_d: 0.0
990 }
991 );
992 }
993
994 #[test]
995 fn singular_in_test_dc_eq0_dx_eq0_seeds_dcd() {
996 let mut h = PdPerturbationHandler::new();
997 let _ = h.consider_new_system(0.1, None).unwrap();
998 assert_eq!(h.test_status, TrialStatus::DcEq0DxEq0);
1002 let d = h.perturb_for_singular(0.1, None).unwrap();
1003 let expected = h.delta_cd_val * (0.1_f64).powf(h.delta_cd_exp);
1004 assert!((d.delta_c - expected).abs() < 1e-15);
1005 assert!((d.delta_d - expected).abs() < 1e-15);
1006 assert_eq!(d.delta_x, 0.0);
1007 assert_eq!(h.test_status, TrialStatus::DcGt0DxEq0);
1008 }
1009
1010 #[test]
1011 fn singular_when_determined_with_dc_zero_seeds_dcd() {
1012 let mut h = PdPerturbationHandler::new();
1013 h.hess_degenerate = DegenType::NotDegenerate;
1014 h.jac_degenerate = DegenType::NotDegenerate;
1015 h.test_status = TrialStatus::NoTest;
1016 let d = h.perturb_for_singular(0.1, None).unwrap();
1017 let expected = h.delta_cd_val * (0.1_f64).powf(h.delta_cd_exp);
1018 assert!((d.delta_c - expected).abs() < 1e-15);
1019 }
1020
1021 #[test]
1032 fn a_second_singular_verdict_from_the_ladder_does_not_trip_the_probe() {
1033 let mut h = PdPerturbationHandler::new();
1034 let _ = h.consider_new_system(0.1, None).unwrap();
1035 let _ = h.perturb_for_singular(0.1, None).unwrap();
1038 assert_eq!(h.test_status, TrialStatus::DcGt0DxEq0);
1039 assert_eq!(h.jac_degenerate, DegenType::NotYetDetermined);
1040 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
1044 assert!(d.delta_x > 0.0);
1045 assert_eq!(h.jac_degenerate, DegenType::NotYetDetermined);
1046 assert_eq!(h.test_status, TrialStatus::DcGt0DxEq0);
1047 let d = h
1050 .perturb_for_singular(0.1, None)
1051 .expect("a singular verdict from the ladder must not be fatal");
1052 assert!(
1053 d.delta_c > 0.0,
1054 "δ_c was dropped by the abandoned probe: {}",
1055 d.delta_c,
1056 );
1057 assert_eq!(
1058 h.test_status,
1059 TrialStatus::NoTest,
1060 "a probe whose precondition no longer holds must be abandoned",
1061 );
1062 }
1063
1064 #[test]
1068 fn singular_after_a_delta_x_step_abandons_the_probe() {
1069 let mut h = PdPerturbationHandler::new();
1070 let _ = h.consider_new_system(0.1, None).unwrap();
1073 assert_eq!(h.test_status, TrialStatus::DcEq0DxEq0);
1074 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
1077 assert!(d.delta_x > 0.0);
1078 let d = h.perturb_for_singular(0.1, None).unwrap();
1080 let expected_cd = h.delta_cd_val * (0.1_f64).powf(h.delta_cd_exp);
1081 assert!(
1082 (d.delta_c - expected_cd).abs() < 1e-20,
1083 "δ_c was not raised: {}",
1084 d.delta_c,
1085 );
1086 assert_eq!(d.delta_c, d.delta_d);
1087 assert_eq!(
1088 d.delta_x, h.delta_xs_init,
1089 "the δ_x ladder lost the rung it had already paid for",
1090 );
1091 assert_eq!(
1092 h.test_status,
1093 TrialStatus::NoTest,
1094 "a probe whose precondition no longer holds must be abandoned, \
1095 not carried into finalize_test",
1096 );
1097 }
1098
1099 #[test]
1100 fn finalize_test_sets_not_degenerate_after_dc_eq0_dx_eq0_pass() {
1101 let mut h = PdPerturbationHandler::new();
1102 let _ = h.consider_new_system(0.1, None).unwrap();
1103 let _ = h.consider_new_system(0.1, None).unwrap();
1107 assert_eq!(h.hess_degenerate, DegenType::NotDegenerate);
1108 assert_eq!(h.jac_degenerate, DegenType::NotDegenerate);
1109 }
1110
1111 #[test]
1112 fn current_perturbation_returns_committed_values() {
1113 let mut h = PdPerturbationHandler::new();
1114 h.delta_x_curr = 1.0;
1115 h.delta_s_curr = 2.0;
1116 h.delta_c_curr = 3.0;
1117 h.delta_d_curr = 4.0;
1118 let d = h.current_perturbation();
1119 assert_eq!(d.delta_x, 1.0);
1120 assert_eq!(d.delta_s, 2.0);
1121 assert_eq!(d.delta_c, 3.0);
1122 assert_eq!(d.delta_d, 4.0);
1123 }
1124
1125 fn withdraw(h: &mut PdPerturbationHandler) -> Number {
1129 h.consider_new_system(0.1, None).unwrap();
1130 h.perturb_for_singular(0.1, None).unwrap();
1131 let mut at = 0.0;
1132 for _ in 0..3 {
1133 at = h.delta_x_curr;
1134 h.perturb_for_wrong_inertia(0.1, None).unwrap();
1135 }
1136 assert!(h.delta_c_abandoned, "precondition: δ_c was withdrawn");
1137 assert_eq!(h.delta_x_at_withdrawal, at);
1138 at
1139 }
1140
1141 #[test]
1149 fn a_certified_singular_from_the_first_delta_c_free_factor_reinstates_delta_c() {
1150 let mut h = PdPerturbationHandler::new();
1151 let at = withdraw(&mut h);
1152 let d = h.perturb_for_singular_with(0.1, true, None).unwrap();
1153 assert!(d.delta_c > 0.0, "δ_c was not reinstated");
1154 assert_eq!(d.delta_d, d.delta_c);
1155 assert!(
1156 d.delta_x > at,
1157 "the ladder restarted below the withdrawal rung ({} <= {at})",
1158 d.delta_x
1159 );
1160 assert!(h.delta_c_reinstated && !h.delta_c_abandoned);
1161
1162 for _ in 0..6 {
1165 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
1166 assert!(d.delta_c > 0.0, "δ_c withdrawn a second time");
1167 }
1168 h.consider_new_system(0.1, None).unwrap();
1170 assert!(!h.delta_c_reinstated && !h.withdrawal_untested);
1171 }
1172
1173 #[test]
1179 fn an_uncertified_singular_does_not_reinstate_delta_c() {
1180 let mut h = PdPerturbationHandler::new();
1181 withdraw(&mut h);
1182 for _ in 0..5 {
1183 let d = h.perturb_for_singular_with(0.1, false, None).unwrap();
1184 assert_eq!(d.delta_c, 0.0, "δ_c came back on an untrusted report");
1185 assert!(d.delta_x > 0.0);
1186 }
1187 assert!(!h.delta_c_reinstated);
1188 }
1189
1190 #[test]
1196 fn a_certified_singular_later_in_the_climb_does_not_reinstate_delta_c() {
1197 let mut h = PdPerturbationHandler::new();
1198 withdraw(&mut h);
1199 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
1201 assert_eq!(d.delta_c, 0.0);
1202 assert!(!h.withdrawal_untested);
1203 h.perturb_for_singular_with(0.1, false, None).unwrap();
1205 let d = h.perturb_for_singular_with(0.1, true, None).unwrap();
1206 assert_eq!(d.delta_c, 0.0, "a late zero pivot reinstated δ_c");
1207 assert!(!h.delta_c_reinstated);
1208 }
1209
1210 #[test]
1213 fn an_uncertified_first_report_closes_the_window() {
1214 let mut h = PdPerturbationHandler::new();
1215 withdraw(&mut h);
1216 h.perturb_for_singular_with(0.1, false, None).unwrap();
1217 let d = h.perturb_for_singular_with(0.1, true, None).unwrap();
1218 assert_eq!(d.delta_c, 0.0);
1219 }
1220
1221 #[test]
1224 fn a_certified_singular_before_any_withdrawal_is_the_ordinary_path() {
1225 let mut h = PdPerturbationHandler::new();
1226 h.consider_new_system(0.1, None).unwrap();
1227 let a = h.perturb_for_singular_with(0.1, true, None).unwrap();
1228 let mut g = PdPerturbationHandler::new();
1229 g.consider_new_system(0.1, None).unwrap();
1230 let b = g.perturb_for_singular(0.1, None).unwrap();
1231 assert_eq!(a, b);
1232 assert!(!h.delta_c_reinstated && !h.delta_c_abandoned);
1233 }
1234}