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}
119
120impl Default for PdPerturbationHandler {
121 fn default() -> Self {
122 Self {
123 delta_xs_max: 1e20,
124 delta_xs_min: 1e-20,
125 delta_xs_first_inc_fact: 100.0,
126 delta_xs_inc_fact: 8.0,
127 delta_xs_dec_fact: 1.0 / 3.0,
128 delta_xs_init: 1e-4,
129 delta_cd_val: 1e-8,
130 delta_cd_exp: 0.25,
131 perturb_always_cd: false,
132 reset_last: false,
133 degen_iters_max: 3,
134 delta_x_curr: 0.0,
135 delta_s_curr: 0.0,
136 delta_c_curr: 0.0,
137 delta_d_curr: 0.0,
138 delta_x_last: 0.0,
139 delta_s_last: 0.0,
140 delta_c_last: 0.0,
141 delta_d_last: 0.0,
142 get_deltas_for_wrong_inertia_called: false,
143 hess_degenerate: DegenType::NotYetDetermined,
144 jac_degenerate: DegenType::NotYetDetermined,
145 degen_iters: 0,
146 test_status: TrialStatus::NoTest,
147 delta_c_rungs: 0,
153 delta_c_abandoned: false,
154 delta_c_max_rungs: 3,
155 }
156 }
157}
158
159#[derive(Debug, Clone, Copy, PartialEq)]
161pub struct Deltas {
162 pub delta_x: Number,
163 pub delta_s: Number,
164 pub delta_c: Number,
165 pub delta_d: Number,
166}
167
168impl PdPerturbationHandler {
169 pub fn new() -> Self {
170 Self::default()
171 }
172
173 pub fn set_perturb_always_cd(&mut self, on: bool) {
176 self.perturb_always_cd = on;
177 self.jac_degenerate = if on {
178 DegenType::NotDegenerate
179 } else {
180 DegenType::NotYetDetermined
181 };
182 }
183
184 pub fn consider_new_system(
189 &mut self,
190 mu: Number,
191 ip_data: Option<&dyn PerturbationSink>,
192 ) -> Option<Deltas> {
193 self.finalize_test(ip_data);
194
195 if self.reset_last {
198 self.delta_x_last = self.delta_x_curr;
199 self.delta_s_last = self.delta_s_curr;
200 self.delta_c_last = self.delta_c_curr;
201 self.delta_d_last = self.delta_d_curr;
202 } else {
203 if self.delta_x_curr > 0.0 {
204 self.delta_x_last = self.delta_x_curr;
205 }
206 if self.delta_s_curr > 0.0 {
207 self.delta_s_last = self.delta_s_curr;
208 }
209 if self.delta_c_curr > 0.0 {
210 self.delta_c_last = self.delta_c_curr;
211 }
212 if self.delta_d_curr > 0.0 {
213 self.delta_d_last = self.delta_d_curr;
214 }
215 }
216
217 let undet = matches!(self.hess_degenerate, DegenType::NotYetDetermined)
218 || matches!(self.jac_degenerate, DegenType::NotYetDetermined);
219 self.test_status = if undet {
220 if self.perturb_always_cd {
221 TrialStatus::DcGt0DxEq0
222 } else {
223 TrialStatus::DcEq0DxEq0
224 }
225 } else {
226 TrialStatus::NoTest
227 };
228
229 let mut delta_c = if matches!(self.jac_degenerate, DegenType::Degenerate) {
230 let v = self.delta_cd(mu);
231 self.delta_c_curr = v;
232 append_info(ip_data, "l");
233 v
234 } else if self.perturb_always_cd {
235 let v = self.delta_cd(mu);
236 self.delta_c_curr = v;
237 v
238 } else {
239 self.delta_c_curr = 0.0;
240 0.0
241 };
242 let mut delta_d = delta_c;
243 self.delta_d_curr = delta_d;
244
245 let mut delta_x = 0.0;
246 let mut delta_s = 0.0;
247
248 if matches!(self.hess_degenerate, DegenType::Degenerate) {
249 self.delta_x_curr = 0.0;
250 self.delta_s_curr = 0.0;
251 if !self.get_deltas_for_wrong_inertia(
252 &mut delta_x,
253 &mut delta_s,
254 &mut delta_c,
255 &mut delta_d,
256 ip_data,
257 ) {
258 return None;
259 }
260 }
261
262 self.delta_x_curr = delta_x;
263 self.delta_s_curr = delta_s;
264 self.delta_c_curr = delta_c;
265 self.delta_d_curr = delta_d;
266 set_info_regu_x(ip_data, delta_x);
267 self.get_deltas_for_wrong_inertia_called = false;
268 self.delta_c_rungs = 0;
270 self.delta_c_abandoned = false;
271
272 Some(Deltas {
273 delta_x,
274 delta_s,
275 delta_c,
276 delta_d,
277 })
278 }
279
280 pub fn perturb_for_singular(
283 &mut self,
284 mu: Number,
285 ip_data: Option<&dyn PerturbationSink>,
286 ) -> Option<Deltas> {
287 let mut delta_x = 0.0;
288 let mut delta_s = 0.0;
289 let mut delta_c = 0.0;
290 let mut delta_d = 0.0;
291
292 if self.delta_c_abandoned {
299 self.test_status = TrialStatus::NoTest;
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 set_info_regu_x(ip_data, self.delta_x_curr);
310 return Some(Deltas {
311 delta_x: self.delta_x_curr,
312 delta_s: self.delta_s_curr,
313 delta_c: self.delta_c_curr,
314 delta_d: self.delta_d_curr,
315 });
316 }
317
318 let probe_intact = match self.test_status {
331 TrialStatus::DcEq0DxEq0 => self.delta_x_curr == 0.0 && self.delta_c_curr == 0.0,
332 TrialStatus::DcGt0DxEq0 => self.delta_x_curr == 0.0 && self.delta_c_curr > 0.0,
333 TrialStatus::DcEq0DxGt0 => self.delta_x_curr > 0.0 && self.delta_c_curr == 0.0,
334 TrialStatus::DcGt0DxGt0 | TrialStatus::NoTest => true,
335 };
336 if !probe_intact {
337 self.test_status = TrialStatus::NoTest;
338 }
339
340 let undet = probe_intact
341 && (matches!(self.hess_degenerate, DegenType::NotYetDetermined)
342 || matches!(self.jac_degenerate, DegenType::NotYetDetermined));
343 if undet {
344 match self.test_status {
345 TrialStatus::DcEq0DxEq0 => {
346 debug_assert!(self.delta_x_curr == 0.0 && self.delta_c_curr == 0.0);
347 if matches!(self.jac_degenerate, DegenType::NotYetDetermined) {
348 let v = self.delta_cd(mu);
349 self.delta_c_curr = v;
350 self.delta_d_curr = v;
351 self.test_status = TrialStatus::DcGt0DxEq0;
352 } else {
353 debug_assert!(matches!(self.hess_degenerate, DegenType::NotYetDetermined));
354 if !self.get_deltas_for_wrong_inertia(
355 &mut delta_x,
356 &mut delta_s,
357 &mut delta_c,
358 &mut delta_d,
359 ip_data,
360 ) {
361 return None;
362 }
363 self.test_status = TrialStatus::DcEq0DxGt0;
364 }
365 }
366 TrialStatus::DcGt0DxEq0 => {
367 debug_assert!(self.delta_x_curr == 0.0 && self.delta_c_curr > 0.0);
368 debug_assert!(matches!(self.jac_degenerate, DegenType::NotYetDetermined));
369 if !self.perturb_always_cd {
370 self.delta_c_curr = 0.0;
371 self.delta_d_curr = 0.0;
372 if !self.get_deltas_for_wrong_inertia(
373 &mut delta_x,
374 &mut delta_s,
375 &mut delta_c,
376 &mut delta_d,
377 ip_data,
378 ) {
379 return None;
380 }
381 self.test_status = TrialStatus::DcEq0DxGt0;
382 } else 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 } else {
391 self.test_status = TrialStatus::DcGt0DxGt0;
392 }
393 }
394 TrialStatus::DcEq0DxGt0 => {
395 debug_assert!(self.delta_x_curr > 0.0 && self.delta_c_curr == 0.0);
396 let v = self.delta_cd(mu);
397 self.delta_c_curr = v;
398 self.delta_d_curr = v;
399 if !self.get_deltas_for_wrong_inertia(
400 &mut delta_x,
401 &mut delta_s,
402 &mut delta_c,
403 &mut delta_d,
404 ip_data,
405 ) {
406 return None;
407 }
408 self.test_status = TrialStatus::DcGt0DxGt0;
409 }
410 TrialStatus::DcGt0DxGt0 => {
411 if !self.get_deltas_for_wrong_inertia(
412 &mut delta_x,
413 &mut delta_s,
414 &mut delta_c,
415 &mut delta_d,
416 ip_data,
417 ) {
418 return None;
419 }
420 }
421 TrialStatus::NoTest => {
422 debug_assert!(false, "perturb_for_singular: NoTest in undetermined branch");
423 }
424 }
425 } else if self.delta_c_curr > 0.0 {
426 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 } else {
437 let v = self.delta_cd(mu);
438 self.delta_c_curr = v;
439 self.delta_d_curr = v;
440 append_info(ip_data, "L");
441 }
442
443 let out = Deltas {
444 delta_x: self.delta_x_curr,
445 delta_s: self.delta_s_curr,
446 delta_c: self.delta_c_curr,
447 delta_d: self.delta_d_curr,
448 };
449 set_info_regu_x(ip_data, out.delta_x);
450 Some(out)
451 }
452
453 pub fn perturb_for_wrong_inertia(
456 &mut self,
457 mu: Number,
458 ip_data: Option<&dyn PerturbationSink>,
459 ) -> Option<Deltas> {
460 if std::env::var_os("POUNCE_DBG_PERT").is_some() {
461 if let Some(d) = ip_data {
462 d.debug(&format!(
463 "[PERT] iter={} WRONG_INERTIA mu={:.2e} dx_last={:.2e} dx_curr={:.2e}",
464 d.iter_count(),
465 mu,
466 self.delta_x_last,
467 self.delta_x_curr
468 ));
469 }
470 }
471 self.finalize_test(ip_data);
472 self.maybe_withdraw_delta_c(ip_data);
473
474 let mut delta_x = 0.0;
475 let mut delta_s = 0.0;
476 let mut delta_c = 0.0;
477 let mut delta_d = 0.0;
478 let mut ok = self.get_deltas_for_wrong_inertia(
479 &mut delta_x,
480 &mut delta_s,
481 &mut delta_c,
482 &mut delta_d,
483 ip_data,
484 );
485 if !ok && self.delta_c_curr == 0.0 {
492 debug_assert_eq!(self.delta_d_curr, 0.0);
493 let v = self.delta_cd(mu);
494 self.delta_c_curr = v;
495 self.delta_d_curr = v;
496 self.delta_x_curr = 0.0;
497 self.delta_s_curr = 0.0;
498 self.test_status = TrialStatus::NoTest;
499 if matches!(self.hess_degenerate, DegenType::Degenerate) {
500 self.hess_degenerate = DegenType::NotYetDetermined;
501 }
502 ok = self.get_deltas_for_wrong_inertia(
503 &mut delta_x,
504 &mut delta_s,
505 &mut delta_c,
506 &mut delta_d,
507 ip_data,
508 );
509 }
510 if !ok {
511 return None;
512 }
513 Some(Deltas {
514 delta_x,
515 delta_s,
516 delta_c,
517 delta_d,
518 })
519 }
520
521 fn maybe_withdraw_delta_c(&mut self, ip_data: Option<&dyn PerturbationSink>) {
547 if self.delta_c_max_rungs <= 0 || self.delta_c_abandoned || self.delta_c_curr <= 0.0 {
548 return;
549 }
550 self.delta_c_rungs += 1;
551 if self.delta_c_rungs < self.delta_c_max_rungs {
552 return;
553 }
554 self.delta_c_curr = 0.0;
555 self.delta_d_curr = 0.0;
556 self.delta_x_curr = 0.0;
557 self.delta_s_curr = 0.0;
558 self.delta_c_abandoned = true;
559 self.test_status = TrialStatus::NoTest;
560 append_info(ip_data, "w");
561 }
562
563 pub fn current_perturbation(&self) -> Deltas {
565 Deltas {
566 delta_x: self.delta_x_curr,
567 delta_s: self.delta_s_curr,
568 delta_c: self.delta_c_curr,
569 delta_d: self.delta_d_curr,
570 }
571 }
572
573 fn get_deltas_for_wrong_inertia(
577 &mut self,
578 delta_x: &mut Number,
579 delta_s: &mut Number,
580 delta_c: &mut Number,
581 delta_d: &mut Number,
582 ip_data: Option<&dyn PerturbationSink>,
583 ) -> bool {
584 if self.delta_x_curr == 0.0 {
585 self.delta_x_curr = if self.delta_x_last == 0.0 {
586 self.delta_xs_init
587 } else {
588 self.delta_xs_min
589 .max(self.delta_x_last * self.delta_xs_dec_fact)
590 };
591 } else if self.delta_x_last == 0.0 || 1e5 * self.delta_x_last < self.delta_x_curr {
592 self.delta_x_curr *= self.delta_xs_first_inc_fact;
593 } else {
594 self.delta_x_curr *= self.delta_xs_inc_fact;
595 }
596 if self.delta_x_curr > self.delta_xs_max {
597 self.delta_x_last = 0.0;
598 self.delta_s_last = 0.0;
599 append_info(ip_data, "dx");
600 return false;
601 }
602 self.delta_s_curr = self.delta_x_curr;
603
604 *delta_x = self.delta_x_curr;
605 *delta_s = self.delta_s_curr;
606 *delta_c = self.delta_c_curr;
607 *delta_d = self.delta_d_curr;
608 set_info_regu_x(ip_data, *delta_x);
609 self.get_deltas_for_wrong_inertia_called = true;
610 true
611 }
612
613 fn delta_cd(&self, mu: Number) -> Number {
614 self.delta_cd_val * mu.powf(self.delta_cd_exp)
615 }
616
617 fn finalize_test(&mut self, ip_data: Option<&dyn PerturbationSink>) {
621 match self.test_status {
622 TrialStatus::NoTest => (),
623 TrialStatus::DcEq0DxEq0 => {
624 if matches!(self.hess_degenerate, DegenType::NotYetDetermined)
625 && matches!(self.jac_degenerate, DegenType::NotYetDetermined)
626 {
627 self.hess_degenerate = DegenType::NotDegenerate;
628 self.jac_degenerate = DegenType::NotDegenerate;
629 append_info(ip_data, "Nhj ");
630 } else if matches!(self.hess_degenerate, DegenType::NotYetDetermined) {
631 self.hess_degenerate = DegenType::NotDegenerate;
632 append_info(ip_data, "Nh ");
633 } else if matches!(self.jac_degenerate, DegenType::NotYetDetermined) {
634 self.jac_degenerate = DegenType::NotDegenerate;
635 append_info(ip_data, "Nj ");
636 }
637 }
638 TrialStatus::DcGt0DxEq0 => {
639 if matches!(self.hess_degenerate, DegenType::NotYetDetermined) {
640 self.hess_degenerate = DegenType::NotDegenerate;
641 append_info(ip_data, "Nh ");
642 }
643 if matches!(self.jac_degenerate, DegenType::NotYetDetermined) {
644 self.degen_iters += 1;
645 if self.degen_iters >= self.degen_iters_max {
646 self.jac_degenerate = DegenType::Degenerate;
647 append_info(ip_data, "Dj ");
648 }
649 append_info(ip_data, "L");
650 }
651 }
652 TrialStatus::DcEq0DxGt0 => {
653 if matches!(self.jac_degenerate, DegenType::NotYetDetermined) {
654 self.jac_degenerate = DegenType::NotDegenerate;
655 append_info(ip_data, "Nj ");
656 }
657 if matches!(self.hess_degenerate, DegenType::NotYetDetermined) {
658 self.degen_iters += 1;
659 if self.degen_iters >= self.degen_iters_max {
660 self.hess_degenerate = DegenType::Degenerate;
661 append_info(ip_data, "Dh ");
662 }
663 }
664 }
665 TrialStatus::DcGt0DxGt0 => {
666 self.degen_iters += 1;
667 if self.degen_iters >= self.degen_iters_max {
668 self.hess_degenerate = DegenType::Degenerate;
669 self.jac_degenerate = DegenType::Degenerate;
670 append_info(ip_data, "Dhj ");
671 }
672 append_info(ip_data, "L");
673 }
674 }
675 }
676}
677
678fn append_info(sink: Option<&dyn PerturbationSink>, s: &str) {
679 if let Some(h) = sink {
680 h.append_info(s);
681 }
682}
683
684fn set_info_regu_x(sink: Option<&dyn PerturbationSink>, v: Number) {
685 if let Some(h) = sink {
686 h.set_regu_x(v);
687 }
688}
689
690#[cfg(test)]
691mod tests {
692 use super::*;
693
694 #[test]
698 fn delta_c_is_withdrawn_after_the_ladder_has_climbed_without_it() {
699 let mut h = PdPerturbationHandler::new();
700 h.consider_new_system(0.1, None).unwrap();
701
702 let d = h.perturb_for_singular(0.1, None).unwrap();
704 assert!(d.delta_c > 0.0, "δ_c should be the first response");
705 assert_eq!(d.delta_x, 0.0);
706
707 for rung in 1..=2 {
709 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
710 assert!(d.delta_c > 0.0, "δ_c withdrawn early, at rung {rung}");
711 assert!(d.delta_x > 0.0);
712 }
713
714 let before = h.delta_x_curr;
717 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
718 assert_eq!(d.delta_c, 0.0, "δ_c was not withdrawn at the third rung");
719 assert_eq!(d.delta_d, 0.0);
720 assert!(
721 d.delta_x > 0.0 && d.delta_x < before,
722 "the ladder did not restart"
723 );
724 }
725
726 #[test]
731 fn a_withdrawn_delta_c_is_not_raised_again_until_the_next_iterate() {
732 let mut h = PdPerturbationHandler::new();
733 h.consider_new_system(0.1, None).unwrap();
734 h.perturb_for_singular(0.1, None).unwrap();
735 for _ in 0..3 {
736 h.perturb_for_wrong_inertia(0.1, None).unwrap();
737 }
738 assert!(h.delta_c_abandoned);
739
740 let d = h.perturb_for_singular(0.1, None).unwrap();
741 assert_eq!(d.delta_c, 0.0, "δ_c came back inside the same aug-system");
742 assert!(d.delta_x > 0.0, "the `Singular` was not answered at all");
743
744 h.consider_new_system(0.1, None).unwrap();
747 assert!(!h.delta_c_abandoned);
748 assert_eq!(h.delta_c_rungs, 0);
749 let d = h.perturb_for_singular(0.1, None).unwrap();
750 assert!(d.delta_c > 0.0, "δ_c is still latched off a new aug-system");
751 }
752
753 #[test]
757 fn one_rung_under_delta_c_leaves_it_alone() {
758 let mut h = PdPerturbationHandler::new();
759 h.consider_new_system(0.1, None).unwrap();
760 h.perturb_for_singular(0.1, None).unwrap();
761 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
762 assert!(d.delta_c > 0.0);
763 h.consider_new_system(0.1, None).unwrap();
765 assert_eq!(h.delta_c_rungs, 0);
766 }
767
768 #[test]
771 fn zero_max_rungs_disables_the_walkback() {
772 let mut h = PdPerturbationHandler::new();
773 h.delta_c_max_rungs = 0;
774 h.consider_new_system(0.1, None).unwrap();
775 h.perturb_for_singular(0.1, None).unwrap();
776 let mut last = 0.0;
777 for _ in 0..6 {
778 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
779 assert!(d.delta_c > 0.0, "δ_c was withdrawn with the walk-back off");
780 assert!(d.delta_x > last, "the ladder stopped climbing");
781 last = d.delta_x;
782 }
783 assert!(!h.delta_c_abandoned);
784 }
785
786 #[test]
787 fn first_wrong_inertia_perturbation_is_delta_xs_init() {
788 let mut h = PdPerturbationHandler::new();
789 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
790 assert!((d.delta_x - 1e-4).abs() < 1e-20);
792 assert_eq!(d.delta_x, d.delta_s);
793 assert_eq!(d.delta_c, 0.0);
794 assert_eq!(d.delta_d, 0.0);
795 }
796
797 #[test]
798 fn second_perturbation_uses_first_inc_fact() {
799 let mut h = PdPerturbationHandler::new();
803 let d1 = h.perturb_for_wrong_inertia(0.1, None).unwrap();
804 let d2 = h.perturb_for_wrong_inertia(0.1, None).unwrap();
805 assert!((d2.delta_x - d1.delta_x * 100.0).abs() < 1e-15);
806 }
807
808 #[test]
809 fn third_perturbation_uses_inc_fact() {
810 let mut h = PdPerturbationHandler::new();
813 h.delta_x_curr = 1e-2;
814 h.delta_x_last = 1e-2;
815 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
816 assert!((d.delta_x - 1e-2 * 8.0).abs() < 1e-15);
817 }
818
819 #[test]
820 fn perturbation_caps_at_max_when_dcd_already_active() {
821 let mut h = PdPerturbationHandler::new();
826 h.delta_x_curr = h.delta_xs_max;
827 h.delta_c_curr = 1e-4;
828 h.delta_d_curr = 1e-4;
829 assert!(h.perturb_for_wrong_inertia(0.1, None).is_none());
830 }
831
832 #[test]
833 fn consider_new_system_with_perturb_always_cd_seeds_dcd() {
834 let mut h = PdPerturbationHandler::new();
835 h.set_perturb_always_cd(true);
836 let mu = 0.1;
837 let d = h.consider_new_system(mu, None).unwrap();
838 let expected = h.delta_cd_val * mu.powf(h.delta_cd_exp);
839 assert!((d.delta_c - expected).abs() < 1e-15);
840 assert!((d.delta_d - expected).abs() < 1e-15);
841 assert_eq!(d.delta_x, 0.0);
842 assert_eq!(d.delta_s, 0.0);
843 }
844
845 #[test]
846 fn consider_new_system_default_zeros_dcd() {
847 let mut h = PdPerturbationHandler::new();
848 let d = h.consider_new_system(0.1, None).unwrap();
849 assert_eq!(
850 d,
851 Deltas {
852 delta_x: 0.0,
853 delta_s: 0.0,
854 delta_c: 0.0,
855 delta_d: 0.0
856 }
857 );
858 }
859
860 #[test]
861 fn singular_in_test_dc_eq0_dx_eq0_seeds_dcd() {
862 let mut h = PdPerturbationHandler::new();
863 let _ = h.consider_new_system(0.1, None).unwrap();
864 assert_eq!(h.test_status, TrialStatus::DcEq0DxEq0);
868 let d = h.perturb_for_singular(0.1, None).unwrap();
869 let expected = h.delta_cd_val * (0.1_f64).powf(h.delta_cd_exp);
870 assert!((d.delta_c - expected).abs() < 1e-15);
871 assert!((d.delta_d - expected).abs() < 1e-15);
872 assert_eq!(d.delta_x, 0.0);
873 assert_eq!(h.test_status, TrialStatus::DcGt0DxEq0);
874 }
875
876 #[test]
877 fn singular_when_determined_with_dc_zero_seeds_dcd() {
878 let mut h = PdPerturbationHandler::new();
879 h.hess_degenerate = DegenType::NotDegenerate;
880 h.jac_degenerate = DegenType::NotDegenerate;
881 h.test_status = TrialStatus::NoTest;
882 let d = h.perturb_for_singular(0.1, None).unwrap();
883 let expected = h.delta_cd_val * (0.1_f64).powf(h.delta_cd_exp);
884 assert!((d.delta_c - expected).abs() < 1e-15);
885 }
886
887 #[test]
898 fn a_second_singular_verdict_from_the_ladder_does_not_trip_the_probe() {
899 let mut h = PdPerturbationHandler::new();
900 let _ = h.consider_new_system(0.1, None).unwrap();
901 let _ = h.perturb_for_singular(0.1, None).unwrap();
904 assert_eq!(h.test_status, TrialStatus::DcGt0DxEq0);
905 assert_eq!(h.jac_degenerate, DegenType::NotYetDetermined);
906 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
910 assert!(d.delta_x > 0.0);
911 assert_eq!(h.jac_degenerate, DegenType::NotYetDetermined);
912 assert_eq!(h.test_status, TrialStatus::DcGt0DxEq0);
913 let d = h
916 .perturb_for_singular(0.1, None)
917 .expect("a singular verdict from the ladder must not be fatal");
918 assert!(
919 d.delta_c > 0.0,
920 "δ_c was dropped by the abandoned probe: {}",
921 d.delta_c,
922 );
923 assert_eq!(
924 h.test_status,
925 TrialStatus::NoTest,
926 "a probe whose precondition no longer holds must be abandoned",
927 );
928 }
929
930 #[test]
934 fn singular_after_a_delta_x_step_abandons_the_probe() {
935 let mut h = PdPerturbationHandler::new();
936 let _ = h.consider_new_system(0.1, None).unwrap();
939 assert_eq!(h.test_status, TrialStatus::DcEq0DxEq0);
940 let d = h.perturb_for_wrong_inertia(0.1, None).unwrap();
943 assert!(d.delta_x > 0.0);
944 let d = h.perturb_for_singular(0.1, None).unwrap();
946 let expected_cd = h.delta_cd_val * (0.1_f64).powf(h.delta_cd_exp);
947 assert!(
948 (d.delta_c - expected_cd).abs() < 1e-20,
949 "δ_c was not raised: {}",
950 d.delta_c,
951 );
952 assert_eq!(d.delta_c, d.delta_d);
953 assert_eq!(
954 d.delta_x, h.delta_xs_init,
955 "the δ_x ladder lost the rung it had already paid for",
956 );
957 assert_eq!(
958 h.test_status,
959 TrialStatus::NoTest,
960 "a probe whose precondition no longer holds must be abandoned, \
961 not carried into finalize_test",
962 );
963 }
964
965 #[test]
966 fn finalize_test_sets_not_degenerate_after_dc_eq0_dx_eq0_pass() {
967 let mut h = PdPerturbationHandler::new();
968 let _ = h.consider_new_system(0.1, None).unwrap();
969 let _ = h.consider_new_system(0.1, None).unwrap();
973 assert_eq!(h.hess_degenerate, DegenType::NotDegenerate);
974 assert_eq!(h.jac_degenerate, DegenType::NotDegenerate);
975 }
976
977 #[test]
978 fn current_perturbation_returns_committed_values() {
979 let mut h = PdPerturbationHandler::new();
980 h.delta_x_curr = 1.0;
981 h.delta_s_curr = 2.0;
982 h.delta_c_curr = 3.0;
983 h.delta_d_curr = 4.0;
984 let d = h.current_perturbation();
985 assert_eq!(d.delta_x, 1.0);
986 assert_eq!(d.delta_s, 2.0);
987 assert_eq!(d.delta_c, 3.0);
988 assert_eq!(d.delta_d, 4.0);
989 }
990}