1use std::collections::{HashMap, HashSet, VecDeque};
6
7use super::types::{
8 AbstractState, AbstractSystem, BmcResult, CegarConfig, CegarResult, Concreteness,
9 IntervalDomain, Predicate, RefinementStrategy, SafetyProperty, TransitionSystem,
10};
11
12pub type StateIdx = usize;
14pub type TracePredicate = Box<dyn Fn(&[Vec<f64>]) -> bool>;
16pub fn verify_energy_conservation(states: &[[f64; 6]], tol: f64) -> bool {
23 if states.len() < 2 {
24 return true;
25 }
26 let energies: Vec<f64> = states
27 .iter()
28 .map(|s| 0.5 * (s[3] * s[3] + s[4] * s[4] + s[5] * s[5]))
29 .collect();
30 let e0 = energies[0];
31 energies.iter().all(|&e| (e - e0).abs() <= tol)
32}
33pub fn verify_momentum_conservation(states: &[[f64; 6]], tol: f64) -> bool {
39 if states.len() < 2 {
40 return true;
41 }
42 let (px0, py0, pz0) = (states[0][3], states[0][4], states[0][5]);
43 states.iter().all(|s| {
44 (s[3] - px0).abs() <= tol && (s[4] - py0).abs() <= tol && (s[5] - pz0).abs() <= tol
45 })
46}
47pub fn verify_angular_momentum_conservation(states: &[[f64; 6]], tol: f64) -> bool {
52 if states.len() < 2 {
53 return true;
54 }
55 let lz0 = states[0][0] * states[0][4] - states[0][1] * states[0][3];
56 states
57 .iter()
58 .all(|s| ((s[0] * s[4] - s[1] * s[3]) - lz0).abs() <= tol)
59}
60pub fn check_lyapunov_stability(states: &[Vec<f64>]) -> bool {
65 if states.len() < 2 {
66 return true;
67 }
68 let norm = |s: &Vec<f64>| s.iter().map(|x| x * x).sum::<f64>().sqrt();
69 let first_norm = norm(&states[0]);
70 let last_norm = norm(states.last().expect("states has at least 2 entries"));
71 last_norm <= first_norm * 1.001
72}
73pub type SymbolicState = HashMap<String, IntervalDomain>;
75pub fn symbolic_step(
78 state: &SymbolicState,
79 var: &str,
80 delta: f64,
81 bounds: &IntervalDomain,
82) -> Option<SymbolicState> {
83 let iv = state.get(var).copied().unwrap_or_else(IntervalDomain::top);
84 let next_iv = IntervalDomain::new(iv.lo + delta, iv.hi + delta);
85 if next_iv.meet(bounds).is_non_empty() {
86 let mut next = state.clone();
87 next.insert(var.to_string(), next_iv.meet(bounds));
88 Some(next)
89 } else {
90 None
91 }
92}
93pub type ConcreteState = Vec<f64>;
95fn predicate_abstraction(system: &TransitionSystem, predicates: &[Predicate]) -> AbstractSystem {
101 let mut abstract_states: Vec<AbstractState> = Vec::new();
102 let mut concrete_to_abstract: HashMap<StateIdx, usize> = HashMap::new();
103 let mut state_to_idx: HashMap<Vec<bool>, usize> = HashMap::new();
104 for (ci, concrete) in system.model.states.iter().enumerate() {
105 let bits: Vec<bool> = predicates.iter().map(|p| p.eval(concrete)).collect();
106 let abs_idx = if let Some(&existing) = state_to_idx.get(&bits) {
107 existing
108 } else {
109 let idx = abstract_states.len();
110 abstract_states.push(AbstractState {
111 bits: bits.clone(),
112 concrete_idx: Some(ci),
113 });
114 state_to_idx.insert(bits, idx);
115 idx
116 };
117 concrete_to_abstract.insert(ci, abs_idx);
118 }
119 let mut transitions_set: HashSet<(usize, usize)> = HashSet::new();
120 for &(from, to) in &system.model.transitions {
121 if let (Some(&af), Some(&at)) = (
122 concrete_to_abstract.get(&from),
123 concrete_to_abstract.get(&to),
124 ) {
125 transitions_set.insert((af, at));
126 }
127 }
128 AbstractSystem {
129 states: abstract_states,
130 transitions: transitions_set.into_iter().collect(),
131 concrete_to_abstract,
132 }
133}
134fn bounded_model_check(
140 abs_sys: &AbstractSystem,
141 system: &TransitionSystem,
142 property: &SafetyProperty,
143 bound: usize,
144) -> BmcResult {
145 let init_abs = match abs_sys.concrete_to_abstract.get(&system.initial_idx) {
146 Some(&i) => i,
147 None => return BmcResult::NoCounterexample,
148 };
149 let mut abs_violates: HashMap<usize, bool> = HashMap::new();
150 for (&ci, &ai) in &abs_sys.concrete_to_abstract {
151 if system
152 .model
153 .states
154 .get(ci)
155 .is_some_and(|concrete| !property.check(concrete))
156 {
157 abs_violates.insert(ai, true);
158 }
159 }
160 let mut queue: VecDeque<(usize, Vec<usize>)> = VecDeque::new();
161 queue.push_back((init_abs, vec![init_abs]));
162 let mut visited_at_depth: HashMap<(usize, usize), bool> = HashMap::new();
163 while let Some((abs_idx, path)) = queue.pop_front() {
164 let depth = path.len() - 1;
165 let key = (abs_idx, depth);
166 if visited_at_depth.contains_key(&key) {
167 continue;
168 }
169 visited_at_depth.insert(key, true);
170 if abs_violates.get(&abs_idx).copied().unwrap_or(false) {
171 return BmcResult::AbstractCex(path);
172 }
173 if depth < bound {
174 for succ in abs_sys.successors(abs_idx) {
175 let mut new_path = path.clone();
176 new_path.push(succ);
177 queue.push_back((succ, new_path));
178 }
179 }
180 }
181 BmcResult::NoCounterexample
182}
183fn check_concreteness(
196 system: &TransitionSystem,
197 abs_sys: &AbstractSystem,
198 abstract_trace: &[usize],
199) -> Concreteness {
200 if abstract_trace.is_empty() {
201 return Concreteness::Spurious;
202 }
203 let mut abs_to_concretes: HashMap<usize, Vec<StateIdx>> = HashMap::new();
204 for (&ci, &ai) in &abs_sys.concrete_to_abstract {
205 abs_to_concretes.entry(ai).or_default().push(ci);
206 }
207 let expand_within = |reachable: &[StateIdx], class_abs: usize| -> Vec<StateIdx> {
208 let class_set: HashSet<StateIdx> = abs_to_concretes
209 .get(&class_abs)
210 .cloned()
211 .unwrap_or_default()
212 .into_iter()
213 .collect();
214 let mut frontier: Vec<StateIdx> = reachable.to_vec();
215 let mut visited: HashSet<StateIdx> = reachable.iter().copied().collect();
216 loop {
217 let mut added = false;
218 for &ci in frontier.clone().iter() {
219 for succ in system.successors(ci) {
220 if class_set.contains(&succ) && !visited.contains(&succ) {
221 visited.insert(succ);
222 frontier.push(succ);
223 added = true;
224 }
225 }
226 }
227 if !added {
228 break;
229 }
230 }
231 frontier
232 };
233 let first_abs = abstract_trace[0];
234 let mut reachable: Vec<StateIdx> = abs_to_concretes
235 .get(&first_abs)
236 .cloned()
237 .unwrap_or_default()
238 .into_iter()
239 .filter(|&ci| ci == system.initial_idx)
240 .collect();
241 reachable = expand_within(&reachable, first_abs);
242 let mut witness_path: Vec<StateIdx> = reachable
243 .first()
244 .copied()
245 .map(|ci| vec![ci])
246 .unwrap_or_default();
247 if reachable.is_empty() {
248 return Concreteness::Spurious;
249 }
250 for &next_abs in abstract_trace.iter().skip(1) {
251 let next_concretes_in_abs: HashSet<StateIdx> = abs_to_concretes
252 .get(&next_abs)
253 .cloned()
254 .unwrap_or_default()
255 .into_iter()
256 .collect();
257 let mut next_reachable: Vec<StateIdx> = Vec::new();
258 for &prev in &reachable {
259 for succ in system.successors(prev) {
260 if next_concretes_in_abs.contains(&succ) && !next_reachable.contains(&succ) {
261 next_reachable.push(succ);
262 }
263 }
264 }
265 if next_reachable.is_empty() {
266 return Concreteness::Spurious;
267 }
268 next_reachable = expand_within(&next_reachable, next_abs);
269 if let Some(&rep) = next_reachable.first() {
270 witness_path.push(rep);
271 }
272 reachable = next_reachable;
273 }
274 let concrete_path: Vec<ConcreteState> = witness_path
275 .iter()
276 .filter_map(|&ci| system.model.states.get(ci).cloned())
277 .collect();
278 if concrete_path.is_empty() {
279 Concreteness::Spurious
280 } else {
281 Concreteness::Concrete(concrete_path)
282 }
283}
284fn refine_predicates(
291 system: &TransitionSystem,
292 abs_sys: &AbstractSystem,
293 abstract_trace: &[usize],
294 strategy: &RefinementStrategy,
295) -> Vec<Predicate> {
296 let mut infeasible_step = abstract_trace.len().saturating_sub(1);
297 let mut abs_to_concretes: HashMap<usize, Vec<StateIdx>> = HashMap::new();
298 for (&ci, &ai) in &abs_sys.concrete_to_abstract {
299 abs_to_concretes.entry(ai).or_default().push(ci);
300 }
301 let mut reachable: Vec<StateIdx> = abs_to_concretes
302 .get(&abstract_trace[0])
303 .cloned()
304 .unwrap_or_default()
305 .into_iter()
306 .filter(|&ci| ci == system.initial_idx)
307 .collect();
308 for (step, &next_abs) in abstract_trace.iter().enumerate().skip(1) {
309 let next_in_abs: HashSet<StateIdx> = abs_to_concretes
310 .get(&next_abs)
311 .cloned()
312 .unwrap_or_default()
313 .into_iter()
314 .collect();
315 let mut next_reachable: Vec<StateIdx> = Vec::new();
316 for &prev in &reachable {
317 for succ in system.successors(prev) {
318 if next_in_abs.contains(&succ) && !next_reachable.contains(&succ) {
319 next_reachable.push(succ);
320 }
321 }
322 }
323 if next_reachable.is_empty() {
324 infeasible_step = step;
325 break;
326 }
327 reachable = next_reachable;
328 }
329 let before_abs = if infeasible_step > 0 {
330 abstract_trace[infeasible_step - 1]
331 } else {
332 abstract_trace[0]
333 };
334 let after_abs = abstract_trace[infeasible_step];
335 let before_concretes: Vec<&Vec<f64>> = abs_to_concretes
336 .get(&before_abs)
337 .cloned()
338 .unwrap_or_default()
339 .into_iter()
340 .filter_map(|ci| system.model.states.get(ci))
341 .collect();
342 let after_concretes: Vec<&Vec<f64>> = abs_to_concretes
343 .get(&after_abs)
344 .cloned()
345 .unwrap_or_default()
346 .into_iter()
347 .filter_map(|ci| system.model.states.get(ci))
348 .collect();
349 if before_concretes.is_empty() || after_concretes.is_empty() {
350 return Vec::new();
351 }
352 let n_vars = before_concretes[0].len().min(after_concretes[0].len());
353 let mut new_predicates: Vec<Predicate> = Vec::new();
354 match strategy {
355 RefinementStrategy::SyntaxGuided | RefinementStrategy::InterpolationBased => {
356 for var_idx in 0..n_vars {
357 let mean_before: f64 = before_concretes
358 .iter()
359 .map(|s| s.get(var_idx).copied().unwrap_or(0.0))
360 .sum::<f64>()
361 / before_concretes.len() as f64;
362 let mean_after: f64 = after_concretes
363 .iter()
364 .map(|s| s.get(var_idx).copied().unwrap_or(0.0))
365 .sum::<f64>()
366 / after_concretes.len() as f64;
367 if (mean_before - mean_after).abs() > 1e-12 {
368 let threshold = (mean_before + mean_after) / 2.0;
369 let name_ge = format!("refine_v{}>={}_{:.4}", var_idx, var_idx, threshold);
370 let name_lt = format!("refine_v{}<{}_{:.4}", var_idx, var_idx, threshold);
371 new_predicates.push(Predicate::ge(name_ge, var_idx, threshold));
372 new_predicates.push(Predicate::lt(name_lt, var_idx, threshold));
373 }
374 }
375 }
376 RefinementStrategy::CraigInterpolant => {
377 let before_bits = &abs_sys.states[before_abs].bits;
378 let after_bits = &abs_sys.states[after_abs].bits;
379 for var_idx in 0..n_vars {
380 let val_before = before_concretes[0].get(var_idx).copied().unwrap_or(0.0);
381 let val_after = after_concretes[0].get(var_idx).copied().unwrap_or(0.0);
382 let bit_idx = var_idx.min(before_bits.len().saturating_sub(1));
383 let bits_differ = if bit_idx < before_bits.len() && bit_idx < after_bits.len() {
384 before_bits[bit_idx] != after_bits[bit_idx]
385 } else {
386 true
387 };
388 if bits_differ && (val_before - val_after).abs() > 1e-12 {
389 let threshold = (val_before + val_after) / 2.0;
390 new_predicates.push(Predicate::ge(
391 format!("craig_v{}_ge_{:.4}", var_idx, threshold),
392 var_idx,
393 threshold,
394 ));
395 }
396 }
397 }
398 }
399 new_predicates
400}
401fn initial_predicates(system: &TransitionSystem, property: &SafetyProperty) -> Vec<Predicate> {
407 let mut predicates: Vec<Predicate> = Vec::new();
408 let (satisfying, violating): (Vec<&Vec<f64>>, Vec<&Vec<f64>>) =
409 system.model.states.iter().partition(|s| property.check(s));
410 if satisfying.is_empty() || violating.is_empty() {
411 let n_vars = system.model.states.first().map(|s| s.len()).unwrap_or(1);
412 for var_idx in 0..n_vars {
413 let vals: Vec<f64> = system
414 .model
415 .states
416 .iter()
417 .map(|s| s.get(var_idx).copied().unwrap_or(0.0))
418 .collect();
419 if let (Some(&lo), Some(&hi)) = (
420 vals.iter()
421 .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)),
422 vals.iter()
423 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)),
424 ) && (hi - lo).abs() > 1e-12
425 {
426 let mid = (lo + hi) / 2.0;
427 predicates.push(Predicate::ge(
428 format!("init_v{}_ge_{:.4}", var_idx, mid),
429 var_idx,
430 mid,
431 ));
432 }
433 }
434 return predicates;
435 }
436 let n_vars = system.model.states.first().map(|s| s.len()).unwrap_or(1);
437 for var_idx in 0..n_vars {
438 let max_sat = satisfying
439 .iter()
440 .map(|s| s.get(var_idx).copied().unwrap_or(0.0))
441 .fold(f64::NEG_INFINITY, f64::max);
442 let min_sat = satisfying
443 .iter()
444 .map(|s| s.get(var_idx).copied().unwrap_or(0.0))
445 .fold(f64::INFINITY, f64::min);
446 let max_vio = violating
447 .iter()
448 .map(|s| s.get(var_idx).copied().unwrap_or(0.0))
449 .fold(f64::NEG_INFINITY, f64::max);
450 let min_vio = violating
451 .iter()
452 .map(|s| s.get(var_idx).copied().unwrap_or(0.0))
453 .fold(f64::INFINITY, f64::min);
454 if min_sat > max_vio {
455 let threshold = (min_sat + max_vio) / 2.0;
456 predicates.push(Predicate::ge(
457 format!("init_v{}_ge_{:.4}", var_idx, threshold),
458 var_idx,
459 threshold,
460 ));
461 predicates.push(Predicate::lt(
462 format!("init_v{}_lt_{:.4}", var_idx, threshold),
463 var_idx,
464 threshold,
465 ));
466 } else if max_sat < min_vio {
467 let threshold = (max_sat + min_vio) / 2.0;
468 predicates.push(Predicate::ge(
469 format!("init_v{}_ge_{:.4}", var_idx, threshold),
470 var_idx,
471 threshold,
472 ));
473 predicates.push(Predicate::lt(
474 format!("init_v{}_lt_{:.4}", var_idx, threshold),
475 var_idx,
476 threshold,
477 ));
478 } else {
479 let all_vals: Vec<f64> = satisfying
480 .iter()
481 .chain(violating.iter())
482 .map(|s| s.get(var_idx).copied().unwrap_or(0.0))
483 .collect();
484 let lo = all_vals.iter().cloned().fold(f64::INFINITY, f64::min);
485 let hi = all_vals.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
486 if (hi - lo).abs() > 1e-12 {
487 let mid = (lo + hi) / 2.0;
488 predicates.push(Predicate::ge(
489 format!("init_v{}_ge_{:.4}", var_idx, mid),
490 var_idx,
491 mid,
492 ));
493 predicates.push(Predicate::lt(
494 format!("init_v{}_lt_{:.4}", var_idx, mid),
495 var_idx,
496 mid,
497 ));
498 }
499 }
500 }
501 predicates
502}
503pub fn cegar_verify(
513 system: &TransitionSystem,
514 property: &SafetyProperty,
515 config: &CegarConfig,
516) -> CegarResult {
517 let mut predicates = initial_predicates(system, property);
518 for iteration in 0..config.max_iterations {
519 let abstract_sys = predicate_abstraction(system, &predicates);
520 if abstract_sys.states.is_empty() {
521 return CegarResult::Verified;
522 }
523 let bmc_result = bounded_model_check(&abstract_sys, system, property, config.bmc_bound);
524 match bmc_result {
525 BmcResult::NoCounterexample => return CegarResult::Verified,
526 BmcResult::AbstractCex(abstract_trace) => {
527 match check_concreteness(system, &abstract_sys, &abstract_trace) {
528 Concreteness::Concrete(concrete_trace) => {
529 return CegarResult::Violated {
530 trace: concrete_trace,
531 };
532 }
533 Concreteness::Spurious => {
534 let new_preds = refine_predicates(
535 system,
536 &abstract_sys,
537 &abstract_trace,
538 &config.refinement_strategy,
539 );
540 if new_preds.is_empty() {
541 return CegarResult::Unknown {
542 reason: format!(
543 "Refinement stalled at iteration {} (no new predicates)",
544 iteration
545 ),
546 };
547 }
548 predicates.extend(new_preds);
549 predicates.sort_unstable();
550 predicates.dedup();
551 }
552 }
553 }
554 }
555 }
556 CegarResult::Unknown {
557 reason: format!(
558 "Exceeded {} iterations without convergence",
559 config.max_iterations
560 ),
561 }
562}
563pub fn verify_trace_properties(
567 trace: &[Vec<f64>],
568 properties: &[(&str, TracePredicate)],
569) -> HashMap<String, bool> {
570 properties
571 .iter()
572 .map(|(name, predicate)| (name.to_string(), predicate(trace)))
573 .collect()
574}
575pub fn always<S, F: Fn(&S) -> bool>(trace: &[S], predicate: F) -> bool {
577 trace.iter().all(predicate)
578}
579pub fn eventually<S, F: Fn(&S) -> bool>(trace: &[S], predicate: F) -> bool {
581 trace.iter().any(predicate)
582}
583pub fn globally_after<S, F: Fn(&S) -> bool, G: Fn(&S) -> bool>(
586 trace: &[S],
587 trigger: F,
588 predicate: G,
589) -> bool {
590 let mut triggered = false;
591 for s in trace {
592 if triggered && !predicate(s) {
593 return false;
594 }
595 if trigger(s) {
596 triggered = true;
597 }
598 }
599 true
600}
601#[cfg(test)]
602mod tests {
603 use super::super::types::*;
604 use super::*;
605 #[test]
606 fn test_interval_contains() {
607 let iv = IntervalDomain::new(1.0, 3.0);
608 assert!(iv.contains(2.0));
609 assert!(!iv.contains(4.0));
610 }
611 #[test]
612 fn test_interval_join() {
613 let a = IntervalDomain::new(1.0, 3.0);
614 let b = IntervalDomain::new(2.0, 5.0);
615 let j = a.join(&b);
616 assert_eq!(j.lo, 1.0);
617 assert_eq!(j.hi, 5.0);
618 }
619 #[test]
620 fn test_interval_meet() {
621 let a = IntervalDomain::new(1.0, 4.0);
622 let b = IntervalDomain::new(2.0, 6.0);
623 let m = a.meet(&b);
624 assert_eq!(m.lo, 2.0);
625 assert_eq!(m.hi, 4.0);
626 }
627 #[test]
628 fn test_interval_add() {
629 let a = IntervalDomain::new(1.0, 2.0);
630 let b = IntervalDomain::new(3.0, 4.0);
631 let c = a.add(&b);
632 assert_eq!(c.lo, 4.0);
633 assert_eq!(c.hi, 6.0);
634 }
635 #[test]
636 fn test_interval_sub() {
637 let a = IntervalDomain::new(3.0, 5.0);
638 let b = IntervalDomain::new(1.0, 2.0);
639 let c = a.sub(&b);
640 assert_eq!(c.lo, 1.0);
641 assert_eq!(c.hi, 4.0);
642 }
643 #[test]
644 fn test_interval_mul() {
645 let a = IntervalDomain::new(2.0, 3.0);
646 let b = IntervalDomain::new(4.0, 5.0);
647 let c = a.mul(&b);
648 assert_eq!(c.lo, 8.0);
649 assert_eq!(c.hi, 15.0);
650 }
651 #[test]
652 fn test_interval_div_no_zero() {
653 let a = IntervalDomain::new(6.0, 8.0);
654 let b = IntervalDomain::new(2.0, 4.0);
655 let c = a.div(&b);
656 assert!(c.lo >= 1.5 - 1e-9);
657 assert!(c.hi <= 4.0 + 1e-9);
658 }
659 #[test]
660 fn test_interval_div_contains_zero() {
661 let a = IntervalDomain::new(1.0, 2.0);
662 let b = IntervalDomain::new(-1.0, 1.0);
663 let c = a.div(&b);
664 assert_eq!(c, IntervalDomain::top());
665 }
666 #[test]
667 fn test_interval_width() {
668 let iv = IntervalDomain::new(2.0, 5.0);
669 assert!((iv.width() - 3.0).abs() < 1e-10);
670 }
671 #[test]
672 fn test_ltl_check_safety() {
673 let ltl = LinearTemporalLogic::new("always x >= 0");
674 let trace = vec![1.0_f64, 2.0, 3.0];
675 assert!(ltl.check_safety(&trace, |&x| x >= 0.0));
676 let bad_trace = vec![1.0_f64, -1.0, 3.0];
677 assert!(!ltl.check_safety(&bad_trace, |&x| x >= 0.0));
678 }
679 #[test]
680 fn test_ltl_check_liveness() {
681 let ltl = LinearTemporalLogic::new("eventually x > 10");
682 let trace = vec![1.0_f64, 5.0, 11.0];
683 assert!(ltl.check_liveness(&trace, |&x| x > 10.0));
684 let bad = vec![1.0_f64, 2.0, 3.0];
685 assert!(!ltl.check_liveness(&bad, |&x| x > 10.0));
686 }
687 #[test]
688 fn test_ltl_check_next() {
689 let ltl = LinearTemporalLogic::new("next");
690 let trace = vec![true, true, false];
691 assert!(!ltl.check_next(&trace, |&x| x, |&x| x));
692 }
693 #[test]
694 fn test_ltl_check_until() {
695 let ltl = LinearTemporalLogic::new("until");
696 let trace = vec![1i32, 1, 1, 2];
697 assert!(ltl.check_until(&trace, |&x| x == 1, |&x| x == 2));
698 let bad = vec![1i32, 0, 2];
699 assert!(!ltl.check_until(&bad, |&x| x == 1, |&x| x == 2));
700 }
701 fn simple_model() -> ModelChecker {
702 ModelChecker::new(vec![vec![0.0], vec![1.0], vec![2.0]], vec![(0, 1), (1, 2)])
703 }
704 #[test]
705 fn test_model_reachable_states() {
706 let mc = simple_model();
707 let r = mc.reachable_states(0);
708 assert!(r.contains(&0));
709 assert!(r.contains(&1));
710 assert!(r.contains(&2));
711 }
712 #[test]
713 fn test_model_satisfies_invariant() {
714 let mc = simple_model();
715 assert!(mc.satisfies_invariant(0, |s| s[0] >= 0.0));
716 assert!(!mc.satisfies_invariant(0, |s| s[0] > 1.5));
717 }
718 #[test]
719 fn test_model_find_counterexample() {
720 let mc = simple_model();
721 let cex = mc.find_counterexample(0, |s| s[0] < 2.0);
722 assert_eq!(cex, Some(2));
723 }
724 #[test]
725 fn test_model_shortest_path() {
726 let mc = simple_model();
727 let path = mc.shortest_path(0, 2).unwrap();
728 assert_eq!(path, vec![0, 1, 2]);
729 }
730 #[test]
731 fn test_abstract_interp_bind_get() {
732 let mut ai = AbstractInterpretation::new();
733 ai.bind("x", 3.0);
734 assert_eq!(ai.get("x"), IntervalDomain::new(3.0, 3.0));
735 }
736 #[test]
737 fn test_abstract_interp_widening() {
738 let ai = AbstractInterpretation::new();
739 let prev = IntervalDomain::new(0.0, 1.0);
740 let next = IntervalDomain::new(0.0, 2.0);
741 let w = ai.widening(&prev, &next);
742 assert_eq!(w.hi, f64::INFINITY);
743 }
744 #[test]
745 fn test_abstract_interp_narrowing() {
746 let ai = AbstractInterpretation::new();
747 let prev = IntervalDomain::new(f64::NEG_INFINITY, f64::INFINITY);
748 let next = IntervalDomain::new(-5.0, 5.0);
749 let n = ai.narrowing(&prev, &next);
750 assert_eq!(n.lo, -5.0);
751 assert_eq!(n.hi, 5.0);
752 }
753 #[test]
754 fn test_abstract_interp_is_non_negative() {
755 let mut ai = AbstractInterpretation::new();
756 ai.bind_interval("x", IntervalDomain::new(0.0, 10.0));
757 assert!(ai.is_non_negative("x"));
758 ai.bind_interval("y", IntervalDomain::new(-1.0, 10.0));
759 assert!(!ai.is_non_negative("y"));
760 }
761 #[test]
762 fn test_bisimulation_same_state() {
763 let mut lts = LabeledTransitionSystem::new(2);
764 lts.add_label(0, "a");
765 lts.add_label(1, "a");
766 lts.add_transition(0, "x", 0);
767 lts.add_transition(1, "x", 1);
768 let checker = BisimulationChecker::new(lts);
769 assert!(checker.are_bisimilar(0, 1));
770 }
771 #[test]
772 fn test_bisimulation_different_labels() {
773 let mut lts = LabeledTransitionSystem::new(2);
774 lts.add_label(0, "a");
775 lts.add_label(1, "b");
776 let checker = BisimulationChecker::new(lts);
777 assert!(!checker.are_bisimilar(0, 1));
778 }
779 #[test]
780 fn test_bisimulation_quotient_size() {
781 let mut lts = LabeledTransitionSystem::new(3);
782 lts.add_label(0, "a");
783 lts.add_label(1, "a");
784 lts.add_label(2, "b");
785 let checker = BisimulationChecker::new(lts);
786 assert_eq!(checker.quotient_size(), 2);
787 }
788 #[test]
789 fn test_verify_energy_conservation_ok() {
790 let states = vec![[0.0, 0.0, 0.0, 1.0, 0.0, 0.0]; 5];
791 assert!(verify_energy_conservation(&states, 1e-9));
792 }
793 #[test]
794 fn test_verify_energy_conservation_fail() {
795 let s1 = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0];
796 let s2 = [0.0, 0.0, 0.0, 2.0, 0.0, 0.0];
797 assert!(!verify_energy_conservation(&[s1, s2], 1e-9));
798 }
799 #[test]
800 fn test_verify_momentum_conservation_ok() {
801 let states = vec![[0.0, 0.0, 0.0, 1.0, 2.0, 3.0]; 4];
802 assert!(verify_momentum_conservation(&states, 1e-9));
803 }
804 #[test]
805 fn test_verify_momentum_conservation_fail() {
806 let s1 = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0];
807 let s2 = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
808 assert!(!verify_momentum_conservation(&[s1, s2], 1e-9));
809 }
810 #[test]
811 fn test_sat_trivial_sat() {
812 let mut sat = SatisfiabilityChecker::new(1);
813 sat.add_clause(vec![Literal::pos(0)]);
814 let result = sat.solve();
815 assert!(result.is_some());
816 let assignment = result.unwrap();
817 assert!(sat.check_assignment(&assignment));
818 }
819 #[test]
820 fn test_sat_trivial_unsat() {
821 let mut sat = SatisfiabilityChecker::new(1);
822 sat.add_clause(vec![Literal::pos(0)]);
823 sat.add_clause(vec![Literal::neg(0)]);
824 assert!(sat.solve().is_none());
825 }
826 #[test]
827 fn test_sat_two_vars() {
828 let mut sat = SatisfiabilityChecker::new(2);
829 sat.add_clause(vec![Literal::pos(0), Literal::pos(1)]);
830 sat.add_clause(vec![Literal::neg(0), Literal::pos(1)]);
831 let result = sat.solve();
832 assert!(result.is_some());
833 assert!(sat.check_assignment(&result.unwrap()));
834 }
835 #[test]
836 fn test_sat_check_assignment() {
837 let mut sat = SatisfiabilityChecker::new(2);
838 sat.add_clause(vec![Literal::pos(0), Literal::pos(1)]);
839 assert!(sat.check_assignment(&[false, true]));
840 assert!(!sat.check_assignment(&[false, false]));
841 }
842 #[test]
843 fn test_type_state_fire_ok() {
844 let mut proto = TypeStateProtocol::new("idle");
845 proto.add_transition("idle", "start", "running");
846 proto.add_transition("running", "stop", "idle");
847 assert!(proto.fire("start").is_ok());
848 assert_eq!(proto.current_state, "running");
849 }
850 #[test]
851 fn test_type_state_fire_err() {
852 let mut proto = TypeStateProtocol::new("idle");
853 proto.add_transition("idle", "start", "running");
854 assert!(proto.fire("stop").is_err());
855 }
856 #[test]
857 fn test_type_state_is_reachable() {
858 let mut proto = TypeStateProtocol::new("idle");
859 proto.add_transition("idle", "start", "running");
860 proto.add_transition("running", "pause", "paused");
861 assert!(proto.is_reachable("idle", "paused"));
862 assert!(!proto.is_reachable("idle", "done"));
863 }
864 #[test]
865 fn test_always() {
866 assert!(always(&[1, 2, 3], |&x| x > 0));
867 assert!(!always(&[1, -1, 3], |&x| x > 0));
868 }
869 #[test]
870 fn test_eventually() {
871 assert!(eventually(&[0, 0, 5], |&x| x > 4));
872 assert!(!eventually(&[0, 0, 0], |&x| x > 4));
873 }
874 #[test]
875 fn test_globally_after() {
876 let trace = vec![0i32, 1, 2, 3];
877 assert!(globally_after(&trace, |&x| x == 1, |&x| x > 0));
878 }
879 #[test]
880 fn test_lyapunov_stable() {
881 let trace: Vec<Vec<f64>> = (0..5).map(|i| vec![1.0 / (i as f64 + 1.0)]).collect();
882 assert!(check_lyapunov_stability(&trace));
883 }
884 fn simple_safe_system() -> TransitionSystem {
885 let mc = ModelChecker::new(vec![vec![0.0], vec![1.0], vec![2.0]], vec![(0, 1), (1, 2)]);
886 TransitionSystem::new(mc, 0)
887 }
888 fn simple_violated_system() -> TransitionSystem {
889 let mc = ModelChecker::new(vec![vec![0.0], vec![-1.0]], vec![(0, 1)]);
890 TransitionSystem::new(mc, 0)
891 }
892 #[test]
893 fn test_cegar_simple_safe_property() {
894 let system = simple_safe_system();
895 let property = SafetyProperty::new("x >= 0", |s| s[0] >= 0.0);
896 let config = CegarConfig::default();
897 let result = cegar_verify(&system, &property, &config);
898 assert!(
899 matches!(result, CegarResult::Verified),
900 "Expected Verified, got {:?}",
901 result
902 );
903 }
904 #[test]
905 fn test_cegar_simple_violated_property() {
906 let system = simple_violated_system();
907 let property = SafetyProperty::new("x >= 0", |s| s[0] >= 0.0);
908 let config = CegarConfig::default();
909 let result = cegar_verify(&system, &property, &config);
910 assert!(
911 matches!(result, CegarResult::Violated { .. }),
912 "Expected Violated, got {:?}",
913 result
914 );
915 if let CegarResult::Violated { trace } = &result {
916 assert!(!trace.is_empty(), "Trace should not be empty");
917 }
918 }
919 #[test]
920 fn test_cegar_refinement_adds_predicates() {
921 let mc = ModelChecker::new(
922 vec![vec![5.0], vec![10.0], vec![-1.0]],
923 vec![(0, 1), (1, 2)],
924 );
925 let system = TransitionSystem::new(mc, 0);
926 let property = SafetyProperty::new("x >= 0", |s| s[0] >= 0.0);
927 let config = CegarConfig {
928 max_iterations: 10,
929 bmc_bound: 5,
930 ..CegarConfig::default()
931 };
932 let result = cegar_verify(&system, &property, &config);
933 assert!(
934 matches!(result, CegarResult::Violated { .. } | CegarResult::Verified),
935 "Got unexpected Unknown: {:?}",
936 result
937 );
938 }
939 #[test]
940 fn test_cegar_timeout_returns_unknown() {
941 let mc = ModelChecker::new(vec![vec![1.0], vec![-1.0]], vec![(0, 1)]);
942 let system = TransitionSystem::new(mc, 0);
943 let property = SafetyProperty::new("x >= 0", |s| s[0] >= 0.0);
944 let config = CegarConfig {
945 max_iterations: 0,
946 bmc_bound: 10,
947 ..CegarConfig::default()
948 };
949 let result = cegar_verify(&system, &property, &config);
950 assert!(
951 matches!(result, CegarResult::Unknown { .. }),
952 "Expected Unknown with max_iterations=0, got {:?}",
953 result
954 );
955 }
956 #[test]
957 fn test_cegar_trace_length_matches_bound() {
958 let n = 6usize;
959 let states: Vec<Vec<f64>> = (0..n).map(|i| vec![i as f64]).collect();
960 let transitions: Vec<(StateIdx, StateIdx)> = (0..(n - 1)).map(|i| (i, i + 1)).collect();
961 let mc = ModelChecker::new(states, transitions);
962 let system = TransitionSystem::new(mc, 0);
963 let property = SafetyProperty::new("x < 5", |s| s[0] < 5.0);
964 let config = CegarConfig {
965 bmc_bound: 10,
966 ..CegarConfig::default()
967 };
968 let result = cegar_verify(&system, &property, &config);
969 if let CegarResult::Violated { trace } = result {
970 assert!(
971 trace.len() <= config.bmc_bound + 1,
972 "Trace length {} exceeds bound {}",
973 trace.len(),
974 config.bmc_bound + 1
975 );
976 let last = trace.last().expect("trace non-empty");
977 assert!(
978 !property.check(last),
979 "Last trace state should violate property"
980 );
981 } else {
982 panic!("Expected Violated result");
983 }
984 }
985 #[test]
986 fn test_cegar_craig_interpolant_strategy() {
987 let system = simple_violated_system();
988 let property = SafetyProperty::new("x >= 0", |s| s[0] >= 0.0);
989 let config = CegarConfig {
990 refinement_strategy: RefinementStrategy::CraigInterpolant,
991 ..CegarConfig::default()
992 };
993 let result = cegar_verify(&system, &property, &config);
994 assert!(
995 matches!(result, CegarResult::Violated { .. }),
996 "Expected Violated with CraigInterpolant strategy, got {:?}",
997 result
998 );
999 }
1000 #[test]
1001 fn test_symbolic_step_ok() {
1002 let mut state: SymbolicState = HashMap::new();
1003 state.insert("x".to_string(), IntervalDomain::new(0.0, 5.0));
1004 let bounds = IntervalDomain::new(0.0, 10.0);
1005 let next = symbolic_step(&state, "x", 1.0, &bounds);
1006 assert!(next.is_some());
1007 }
1008 #[test]
1009 fn test_symbolic_step_out_of_bounds() {
1010 let mut state: SymbolicState = HashMap::new();
1011 state.insert("x".to_string(), IntervalDomain::new(9.0, 10.0));
1012 let bounds = IntervalDomain::new(0.0, 5.0);
1013 let next = symbolic_step(&state, "x", 3.0, &bounds);
1014 assert!(next.is_none());
1015 }
1016}