1use crate::ipopt_cq::IpoptCqHandle;
28use crate::ipopt_data::IpoptDataHandle;
29use pounce_common::types::Number;
30use pounce_linalg::{Matrix, Vector};
31use pounce_nlp::ipopt_nlp::SplitNames;
32
33pub use pounce_common::debug::{
34 Checkpoint, DebugAction, DebugHook, DebugState, IterSnapshot, KktReport, KktTriplets, LFactor,
35 ResidKind, Residual,
36};
37
38pub const BLOCK_NAMES: [&str; 8] = ["x", "s", "y_c", "y_d", "z_l", "z_u", "v_l", "v_u"];
40
41pub struct DebugCtx {
46 data: IpoptDataHandle,
47 cq: Option<IpoptCqHandle>,
51 cp: Checkpoint,
52 status: Option<String>,
54 pending_tol: Vec<(String, Number)>,
59}
60
61pub const LIVE_TOLERANCE_OPTS: &[&str] = &[
66 "tol",
67 "dual_inf_tol",
68 "constr_viol_tol",
69 "compl_inf_tol",
70 "acceptable_tol",
71 "acceptable_dual_inf_tol",
72 "acceptable_constr_viol_tol",
73 "acceptable_compl_inf_tol",
74 "acceptable_obj_change_tol",
75];
76
77pub fn is_live_tolerance(name: &str) -> bool {
80 LIVE_TOLERANCE_OPTS.contains(&name)
81}
82
83#[derive(Clone)]
91pub struct IterateSnapshot {
92 pub iter: i32,
93 pub mu: Number,
94 pub tau: Number,
95 curr: crate::iterates_vector::IteratesVector,
96}
97
98impl IterateSnapshot {
99 pub fn iter(&self) -> i32 {
100 self.iter
101 }
102
103 pub fn mu(&self) -> Number {
104 self.mu
105 }
106
107 pub fn tau(&self) -> Number {
108 self.tau
109 }
110
111 pub(crate) fn iterates(&self) -> &crate::iterates_vector::IteratesVector {
114 &self.curr
115 }
116
117 pub fn block(&self, name: &str) -> Option<Vec<Number>> {
119 let v = block_ref(&self.curr, name)?;
120 Some(crate::ipopt_alg::flat_read_owned(v.as_ref()))
121 }
122}
123
124impl IterSnapshot for IterateSnapshot {
125 fn iter(&self) -> i32 {
126 IterateSnapshot::iter(self)
127 }
128 fn mu(&self) -> Number {
129 IterateSnapshot::mu(self)
130 }
131 fn block(&self, name: &str) -> Option<Vec<Number>> {
132 IterateSnapshot::block(self, name)
133 }
134 fn as_any(&self) -> &dyn std::any::Any {
135 self
136 }
137}
138
139impl DebugCtx {
140 pub fn new(data: IpoptDataHandle, cq: IpoptCqHandle, cp: Checkpoint) -> Self {
141 Self {
142 data,
143 cq: Some(cq),
144 cp,
145 status: None,
146 pending_tol: Vec::new(),
147 }
148 }
149
150 pub fn set_live_tolerance(&mut self, name: &str, value: Number) {
156 self.pending_tol.push((name.to_string(), value));
157 }
158
159 pub fn take_live_tolerances(&mut self) -> Vec<(String, Number)> {
161 std::mem::take(&mut self.pending_tol)
162 }
163
164 pub fn with_status(mut self, status: String) -> Self {
166 self.status = Some(status);
167 self
168 }
169
170 pub fn status(&self) -> Option<&str> {
172 self.status.as_deref()
173 }
174
175 #[cfg(test)]
177 fn new_data_only(data: IpoptDataHandle, cp: Checkpoint) -> Self {
178 Self {
179 data,
180 cq: None,
181 cp,
182 status: None,
183 pending_tol: Vec::new(),
184 }
185 }
186
187 pub fn snapshot(&self) -> Option<IterateSnapshot> {
190 let d = self.data.borrow();
191 let curr = d.curr.as_ref()?.clone();
192 Some(IterateSnapshot {
193 iter: d.iter_count,
194 mu: d.curr_mu,
195 tau: d.curr_tau,
196 curr,
197 })
198 }
199
200 pub fn restore(&mut self, snap: &IterateSnapshot) {
204 let mut d = self.data.borrow_mut();
205 d.set_curr(snap.curr.clone());
206 d.curr_mu = snap.mu;
207 d.curr_tau = snap.tau;
208 d.iter_count = snap.iter;
209 }
210
211 fn cq_scalar(
212 &self,
213 f: impl FnOnce(&crate::ipopt_cq::IpoptCalculatedQuantities) -> Number,
214 ) -> Number {
215 match self.cq.as_ref() {
216 Some(cq) => f(&cq.borrow()),
217 None => Number::NAN,
218 }
219 }
220
221 pub fn checkpoint(&self) -> Checkpoint {
223 self.cp
224 }
225
226 pub fn iter(&self) -> i32 {
230 self.data.borrow().iter_count
231 }
232
233 pub fn mu(&self) -> Number {
235 self.data.borrow().curr_mu
236 }
237
238 pub fn objective(&self) -> Number {
240 self.cq_scalar(|c| c.unscaled_curr_f())
241 }
242
243 pub fn inf_pr(&self) -> Number {
245 self.cq_scalar(|c| c.curr_primal_infeasibility_max())
246 }
247
248 pub fn inf_du(&self) -> Number {
250 self.cq_scalar(|c| c.curr_dual_infeasibility_max())
251 }
252
253 pub fn nlp_error(&self) -> Number {
255 self.cq_scalar(|c| c.curr_nlp_error())
256 }
257
258 pub fn complementarity(&self) -> Number {
261 self.cq_scalar(|c| c.curr_avrg_compl())
262 }
263
264 pub fn bound_slack(&self, which: &str) -> Option<Vec<Number>> {
268 let c = self.cq.as_ref()?.borrow();
269 let v = match which {
270 "x_l" => c.curr_slack_x_l(),
271 "x_u" => c.curr_slack_x_u(),
272 "s_l" => c.curr_slack_s_l(),
273 "s_u" => c.curr_slack_s_u(),
274 _ => return None,
275 };
276 Some(crate::ipopt_alg::flat_read_owned(v.as_ref()))
277 }
278
279 pub fn var_bounds(&self) -> Option<(Vec<Number>, Vec<Number>)> {
291 let cq = self.cq.as_ref()?.borrow();
292 let nlp = cq.nlp().borrow();
293 let d = self.data.borrow();
294 let x = &d.curr.as_ref()?.x; let lower = expand_bound(&*nlp.px_l(), nlp.x_l(), &**x, Number::NEG_INFINITY);
296 let upper = expand_bound(&*nlp.px_u(), nlp.x_u(), &**x, Number::INFINITY);
297 Some((lower, upper))
298 }
299
300 pub fn constraint_residuals(&self) -> Option<Vec<Residual>> {
307 let cq = self.cq.as_ref()?.borrow();
308 let c = crate::ipopt_alg::flat_read_owned(cq.curr_c().as_ref());
309 let dms = crate::ipopt_alg::flat_read_owned(cq.curr_d_minus_s().as_ref());
310 let mut out = Vec::with_capacity(c.len() + dms.len());
311 out.extend(c.iter().enumerate().map(|(index, &value)| Residual {
312 kind: ResidKind::Eq,
313 index,
314 value,
315 }));
316 out.extend(dms.iter().enumerate().map(|(index, &value)| Residual {
317 kind: ResidKind::Ineq,
318 index,
319 value,
320 }));
321 Some(out)
322 }
323
324 pub fn dual_residuals(&self) -> Option<Vec<Residual>> {
331 let cq = self.cq.as_ref()?.borrow();
332 let gx = crate::ipopt_alg::flat_read_owned(cq.curr_grad_lag_x().as_ref());
333 let gs = crate::ipopt_alg::flat_read_owned(cq.curr_grad_lag_s().as_ref());
334 let mut out = Vec::with_capacity(gx.len() + gs.len());
335 out.extend(gx.iter().enumerate().map(|(index, &value)| Residual {
336 kind: ResidKind::DualX,
337 index,
338 value,
339 }));
340 out.extend(gs.iter().enumerate().map(|(index, &value)| Residual {
341 kind: ResidKind::DualS,
342 index,
343 value,
344 }));
345 Some(out)
346 }
347
348 pub fn split_names(&self) -> Option<SplitNames> {
357 let cq = self.cq.as_ref()?.borrow();
358 let names = cq.nlp().borrow().split_space_names();
359 names
360 }
361
362 pub fn regularization(&self) -> Number {
369 self.data.borrow().info_regu_x
370 }
371
372 pub fn ls_count(&self) -> i32 {
375 self.data.borrow().info_ls_count
376 }
377
378 pub fn alpha(&self) -> (Number, Number) {
380 let d = self.data.borrow();
381 (d.info_alpha_primal, d.info_alpha_dual)
382 }
383
384 pub fn kkt(&self) -> Option<KktReport> {
393 let d = self.data.borrow();
394 let k = d.kkt_debug.as_ref()?;
395 let curr = d.curr.as_ref();
396 let expected_neg = curr.map(|c| c.y_c.dim() + c.y_d.dim()).unwrap_or(0);
397 let n_pos = if k.n_neg >= 0 { k.dim - k.n_neg } else { -1 };
399 let inertia_correct = k.provides_inertia && k.n_neg == expected_neg;
400 Some(KktReport {
401 iter: k.iter,
402 dim: k.dim,
403 n_neg: k.n_neg,
404 n_pos,
405 expected_neg,
406 provides_inertia: k.provides_inertia,
407 inertia_correct,
408 delta_w: d.perturbations.delta_x,
409 delta_c: d.perturbations.delta_c,
410 status: k.status.clone(),
411 })
412 }
413
414 pub fn kkt_matrix(&self) -> Option<(i32, Vec<i32>, Vec<i32>, Vec<Number>)> {
417 self.data.borrow().kkt_debug.as_ref()?.matrix.clone()
418 }
419
420 pub fn rank_report(&self) -> Option<crate::debug_rank::RankReport> {
434 use crate::debug_rank::RankRow;
435 use pounce_linalg::triplet::GenTMatrix;
436 let cq = self.cq.as_ref()?.borrow();
437 let jac = cq.curr_jac_c();
438 let g = jac.as_any().downcast_ref::<GenTMatrix>()?;
439 let m = g.n_rows() as usize;
440 let n = g.n_cols() as usize;
441 if m == 0 || n == 0 {
442 return None;
443 }
444 let mut dense = vec![0.0; m * n];
448 for ((&ir, &jc), &v) in g.irows().iter().zip(g.jcols()).zip(g.values()) {
449 dense[(ir - 1) as usize * n + (jc - 1) as usize] += v;
450 }
451 let rows: Vec<RankRow> = (0..m)
452 .map(|index| RankRow {
453 kind: ResidKind::Eq,
454 index,
455 })
456 .collect();
457 crate::debug_rank::svd_rank(m, n, &dense, rows)
458 }
459
460 pub fn kkt_captured_iter(&self) -> Option<i32> {
464 Some(self.data.borrow().kkt_debug.as_ref()?.iter)
465 }
466
467 #[allow(clippy::type_complexity)]
471 pub fn kkt_l_factor(
472 &self,
473 ) -> Option<(usize, Vec<usize>, Vec<i32>, Vec<i32>, Option<Vec<Number>>)> {
474 let d = self.data.borrow();
475 let f = d.kkt_debug.as_ref()?.l_factor.as_ref()?;
476 Some((
477 f.n,
478 f.perm.clone(),
479 f.l_irn.clone(),
480 f.l_jcn.clone(),
481 f.l_vals.clone(),
482 ))
483 }
484
485 pub fn block_dims(&self) -> Vec<(&'static str, usize)> {
489 let d = self.data.borrow();
490 let Some(curr) = d.curr.as_ref() else {
491 return BLOCK_NAMES.iter().map(|&n| (n, 0)).collect();
492 };
493 BLOCK_NAMES
494 .iter()
495 .map(|&n| (n, block_ref(curr, n).map(|v| v.dim() as usize).unwrap_or(0)))
496 .collect()
497 }
498
499 pub fn block(&self, name: &str) -> Option<Vec<Number>> {
502 let d = self.data.borrow();
503 let curr = d.curr.as_ref()?;
504 let v = block_ref(curr, name)?;
505 Some(crate::ipopt_alg::flat_read_owned(v.as_ref()))
506 }
507
508 pub fn delta_block(&self, name: &str) -> Option<Vec<Number>> {
510 let d = self.data.borrow();
511 let delta = d.delta.as_ref()?;
512 let v = block_ref(delta, name)?;
513 Some(crate::ipopt_alg::flat_read_owned(v.as_ref()))
514 }
515
516 pub fn set_mu(&mut self, mu: Number) -> Result<(), String> {
522 if !mu.is_finite() || mu <= 0.0 {
523 return Err(format!("mu must be finite and positive, got {mu}"));
524 }
525 self.data.borrow_mut().curr_mu = mu;
526 Ok(())
527 }
528
529 pub fn set_block(&mut self, name: &str, vals: &[Number]) -> Result<(), String> {
547 if !BLOCK_NAMES.contains(&name) {
548 return Err(format!(
549 "unknown block `{name}` (expected one of {BLOCK_NAMES:?})"
550 ));
551 }
552 let mut d = self.data.borrow_mut();
553 let curr = d.curr.as_ref().ok_or("no current iterate yet")?;
554 let mut m = curr.deep_copy();
555 let blk = block_ref_mut(&mut m, name).expect("name checked above");
556 let dim = blk.dim() as usize;
557 if vals.len() != dim {
558 return Err(format!(
559 "block `{name}` has dimension {dim}, got {} value(s)",
560 vals.len()
561 ));
562 }
563 crate::ipopt_alg::flat_write_into(blk.as_mut(), vals);
564 let frozen = m.freeze();
565 d.set_curr(frozen);
566 Ok(())
567 }
568
569 pub fn set_component(&mut self, name: &str, idx: usize, val: Number) -> Result<(), String> {
571 let mut vals = self
572 .block(name)
573 .ok_or_else(|| format!("unknown block `{name}` or no iterate yet"))?;
574 if idx >= vals.len() {
575 return Err(format!(
576 "index {idx} out of range for block `{name}` (dimension {})",
577 vals.len()
578 ));
579 }
580 vals[idx] = val;
581 self.set_block(name, &vals)
582 }
583}
584
585fn expand_bound(
594 p: &dyn Matrix,
595 reduced: &dyn Vector,
596 template: &dyn Vector,
597 absent: Number,
598) -> Vec<Number> {
599 let mut ones = reduced.make_new();
600 ones.set(1.0);
601 let mut mask = template.make_new();
602 p.mult_vector(1.0, &*ones, 0.0, &mut *mask);
603 let mut vals = template.make_new();
604 p.mult_vector(1.0, reduced, 0.0, &mut *vals);
605 let mask = crate::ipopt_alg::flat_read_owned(&*mask);
606 let vals = crate::ipopt_alg::flat_read_owned(&*vals);
607 mask.iter()
608 .zip(vals)
609 .map(|(&m, v)| if m > 0.5 { v } else { absent })
610 .collect()
611}
612
613fn block_ref<'a>(
615 iv: &'a crate::iterates_vector::IteratesVector,
616 name: &str,
617) -> Option<&'a std::rc::Rc<dyn pounce_linalg::Vector>> {
618 Some(match name {
619 "x" => &iv.x,
620 "s" => &iv.s,
621 "y_c" => &iv.y_c,
622 "y_d" => &iv.y_d,
623 "z_l" => &iv.z_l,
624 "z_u" => &iv.z_u,
625 "v_l" => &iv.v_l,
626 "v_u" => &iv.v_u,
627 _ => return None,
628 })
629}
630
631fn block_ref_mut<'a>(
633 iv: &'a mut crate::iterates_vector::IteratesVectorMut,
634 name: &str,
635) -> Option<&'a mut Box<dyn pounce_linalg::Vector>> {
636 Some(match name {
637 "x" => &mut iv.x,
638 "s" => &mut iv.s,
639 "y_c" => &mut iv.y_c,
640 "y_d" => &mut iv.y_d,
641 "z_l" => &mut iv.z_l,
642 "z_u" => &mut iv.z_u,
643 "v_l" => &mut iv.v_l,
644 "v_u" => &mut iv.v_u,
645 _ => return None,
646 })
647}
648
649impl DebugState for DebugCtx {
653 fn as_any(&self) -> Option<&dyn std::any::Any> {
654 Some(self)
655 }
656 fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
657 Some(self)
658 }
659 fn checkpoint(&self) -> Checkpoint {
660 DebugCtx::checkpoint(self)
661 }
662 fn iter(&self) -> i32 {
663 DebugCtx::iter(self)
664 }
665 fn mu(&self) -> Number {
666 DebugCtx::mu(self)
667 }
668 fn objective(&self) -> Number {
669 DebugCtx::objective(self)
670 }
671 fn inf_pr(&self) -> Number {
672 DebugCtx::inf_pr(self)
673 }
674 fn inf_du(&self) -> Number {
675 DebugCtx::inf_du(self)
676 }
677 fn complementarity(&self) -> Number {
678 DebugCtx::complementarity(self)
679 }
680 fn alpha(&self) -> (Number, Number) {
681 DebugCtx::alpha(self)
682 }
683 fn block_dims(&self) -> Vec<(&'static str, usize)> {
684 DebugCtx::block_dims(self)
685 }
686 fn block(&self, name: &str) -> Option<Vec<Number>> {
687 DebugCtx::block(self, name)
688 }
689 fn delta_block(&self, name: &str) -> Option<Vec<Number>> {
690 DebugCtx::delta_block(self, name)
691 }
692 fn status(&self) -> Option<&str> {
693 DebugCtx::status(self)
694 }
695 fn nlp_error(&self) -> Number {
696 DebugCtx::nlp_error(self)
697 }
698 fn bound_slack(&self, which: &str) -> Option<Vec<Number>> {
699 DebugCtx::bound_slack(self, which)
700 }
701 fn regularization(&self) -> Number {
702 DebugCtx::regularization(self)
703 }
704 fn ls_count(&self) -> i32 {
705 DebugCtx::ls_count(self)
706 }
707 fn kkt(&self) -> Option<KktReport> {
708 DebugCtx::kkt(self)
709 }
710 fn kkt_matrix(&self) -> Option<KktTriplets> {
711 DebugCtx::kkt_matrix(self)
712 }
713 fn kkt_l_factor(&self) -> Option<LFactor> {
714 DebugCtx::kkt_l_factor(self)
715 }
716 fn kkt_captured_iter(&self) -> Option<i32> {
717 DebugCtx::kkt_captured_iter(self)
718 }
719 fn request_l_factor(&mut self) -> bool {
720 DebugCtx::kkt_l_factor(self).is_some()
724 }
725 fn request_kkt_matrix(&mut self) -> bool {
726 DebugCtx::kkt_matrix(self).is_some()
727 }
728 fn set_mu(&mut self, mu: Number) -> Result<(), String> {
729 DebugCtx::set_mu(self, mu)
730 }
731 fn set_block(&mut self, name: &str, vals: &[Number]) -> Result<(), String> {
732 DebugCtx::set_block(self, name, vals)
733 }
734 fn set_component(&mut self, name: &str, idx: usize, val: Number) -> Result<(), String> {
735 DebugCtx::set_component(self, name, idx, val)
736 }
737 fn snapshot(&self) -> Option<Box<dyn IterSnapshot>> {
738 DebugCtx::snapshot(self).map(|s| Box::new(s) as Box<dyn IterSnapshot>)
739 }
740 fn restore(&mut self, snap: &dyn IterSnapshot) -> bool {
741 match snap.as_any().downcast_ref::<IterateSnapshot>() {
742 Some(s) => {
743 DebugCtx::restore(self, s);
744 true
745 }
746 None => false,
747 }
748 }
749 fn constraint_residuals(&self) -> Option<Vec<Residual>> {
750 DebugCtx::constraint_residuals(self)
751 }
752 fn dual_residuals(&self) -> Option<Vec<Residual>> {
753 DebugCtx::dual_residuals(self)
754 }
755}
756
757#[cfg(test)]
758mod tests {
759 use super::*;
760 use crate::ipopt_data::IpoptData;
761 use crate::iterates_vector::IteratesVector;
762 use pounce_linalg::dense_vector::DenseVectorSpace;
763 use pounce_linalg::Vector;
764 use std::cell::RefCell;
765 use std::rc::Rc;
766
767 fn iv(xvals: &[f64]) -> IteratesVector {
768 let dense = |vals: &[f64]| {
769 let mut v = DenseVectorSpace::new(vals.len() as i32).make_new_dense();
770 v.set_values(vals);
771 Rc::new(v) as Rc<dyn Vector>
772 };
773 let z = |n| dense(&vec![0.0; n]);
774 IteratesVector::new(dense(xvals), z(1), z(1), z(1), z(2), z(2), z(1), z(1))
775 }
776
777 fn ctx_with(xvals: &[f64]) -> DebugCtx {
778 let mut data = IpoptData::new();
779 data.set_curr(iv(xvals));
780 data.curr_mu = 0.1;
781 let data = Rc::new(RefCell::new(data));
782 DebugCtx::new_data_only(data, Checkpoint::IterStart)
783 }
784
785 #[test]
786 fn reads_block_and_mu() {
787 let ctx = ctx_with(&[1.0, 2.0]);
788 assert_eq!(ctx.mu(), 0.1);
789 assert_eq!(ctx.block("x"), Some(vec![1.0, 2.0]));
790 assert_eq!(ctx.block("nope"), None);
791 }
792
793 #[test]
794 fn set_component_rebuilds_iterate_with_fresh_tag() {
795 let mut ctx = ctx_with(&[1.0, 2.0]);
796 let before = ctx
797 .data
798 .borrow()
799 .curr
800 .as_ref()
801 .unwrap()
802 .x
803 .as_tagged()
804 .get_tag();
805 ctx.set_component("x", 1, 9.0).unwrap();
806 let after = ctx
807 .data
808 .borrow()
809 .curr
810 .as_ref()
811 .unwrap()
812 .x
813 .as_tagged()
814 .get_tag();
815 assert_eq!(ctx.block("x"), Some(vec![1.0, 9.0]));
816 assert_ne!(before, after, "mutating the iterate must mint a new tag");
817 }
818
819 #[test]
820 fn set_block_dim_mismatch_is_rejected() {
821 let mut ctx = ctx_with(&[1.0, 2.0]);
822 assert!(ctx.set_block("x", &[1.0]).is_err());
823 assert!(ctx.set_block("x", &[1.0, 2.0, 3.0]).is_err());
824 assert!(ctx.set_block("x", &[3.0, 4.0]).is_ok());
825 assert_eq!(ctx.block("x"), Some(vec![3.0, 4.0]));
826 }
827
828 #[test]
829 fn block_names_all_resolve_in_block_ref() {
830 let mut ctx = ctx_with(&[1.0, 2.0]);
835 for name in BLOCK_NAMES {
836 let cur = ctx
837 .block(name)
838 .unwrap_or_else(|| panic!("block_ref does not resolve `{name}`"));
839 ctx.set_block(name, &cur)
841 .unwrap_or_else(|e| panic!("block_ref_mut does not resolve `{name}`: {e}"));
842 }
843 }
844
845 #[test]
846 fn residuals_are_none_without_cq() {
847 let ctx = ctx_with(&[1.0, 2.0]);
850 assert!(ctx.constraint_residuals().is_none());
851 assert!(ctx.dual_residuals().is_none());
852 }
853
854 #[test]
855 fn resid_kind_tags_and_primal_classification_are_stable() {
856 assert_eq!(ResidKind::Eq.tag(), "c");
857 assert_eq!(ResidKind::Ineq.tag(), "d-s");
858 assert_eq!(ResidKind::DualX.tag(), "grad_x_L");
859 assert_eq!(ResidKind::DualS.tag(), "grad_s_L");
860 assert!(ResidKind::Eq.is_primal());
861 assert!(ResidKind::Ineq.is_primal());
862 assert!(!ResidKind::DualX.is_primal());
863 assert!(!ResidKind::DualS.is_primal());
864 }
865
866 #[test]
867 fn checkpoint_as_str_is_stable() {
868 assert_eq!(Checkpoint::IterStart.as_str(), "iter_start");
873 assert_eq!(Checkpoint::AfterBarrierUpdate.as_str(), "after_mu");
874 assert_eq!(
875 Checkpoint::AfterSearchDirection.as_str(),
876 "after_search_dir"
877 );
878 assert_eq!(Checkpoint::AfterStep.as_str(), "after_step");
879 assert_eq!(Checkpoint::StepRejected.as_str(), "step_rejected");
880 assert_eq!(Checkpoint::PreRestoration.as_str(), "pre_restoration_entry");
881 assert_eq!(
882 Checkpoint::PostRestoration.as_str(),
883 "post_restoration_exit"
884 );
885 assert_eq!(Checkpoint::Terminated.as_str(), "terminated");
886 }
887
888 #[test]
889 fn snapshot_then_restore_round_trips_iterate_and_mu() {
890 let mut ctx = ctx_with(&[1.0, 2.0]);
891 let snap = ctx.snapshot().expect("snapshot");
892 assert_eq!(snap.iter(), 0);
893 ctx.set_component("x", 0, 99.0).unwrap();
895 ctx.set_mu(0.5).unwrap();
896 assert_eq!(ctx.block("x"), Some(vec![99.0, 2.0]));
897 assert_eq!(ctx.mu(), 0.5);
898 ctx.restore(&snap);
900 assert_eq!(ctx.block("x"), Some(vec![1.0, 2.0]));
901 assert_eq!(ctx.mu(), 0.1);
902 assert_eq!(ctx.iter(), 0);
903 }
904
905 #[test]
906 fn set_mu_rejects_nonpositive() {
907 let mut ctx = ctx_with(&[1.0]);
908 assert!(ctx.set_mu(-1.0).is_err());
909 assert!(ctx.set_mu(0.0).is_err());
910 assert!(ctx.set_mu(1e-3).is_ok());
911 assert_eq!(ctx.mu(), 1e-3);
912 }
913}