Skip to main content

oxiphysics_core/formal_verification/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use std::collections::{HashMap, HashSet, VecDeque};
6
7use super::types::{
8    AbstractState, AbstractSystem, BmcResult, CegarConfig, CegarResult, Concreteness,
9    IntervalDomain, Predicate, RefinementStrategy, SafetyProperty, TransitionSystem,
10};
11
12/// State index type alias.
13pub type StateIdx = usize;
14/// Type alias for a boxed trace-level property predicate.
15pub type TracePredicate = Box<dyn Fn(&[Vec<f64>]) -> bool>;
16/// Verify energy conservation over a trajectory of states.
17///
18/// Each state is a `\[f64; 6\]` array of the form `\[x, y, z, vx, vy, vz\]`.
19/// Energy is estimated as kinetic energy `0.5 * (vx² + vy² + vz²)` (unit mass).
20///
21/// Returns `true` if the energy variation across all states is within `tol`.
22pub 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}
33/// Verify linear momentum conservation over a trajectory of states.
34///
35/// Each state is `\[x, y, z, vx, vy, vz\]` (unit mass).
36/// Returns `true` if the momentum vector changes by less than `tol` per
37/// component.
38pub 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}
47/// Verify angular momentum conservation.
48///
49/// Each state is `\[x, y, z, vx, vy, vz\]`.  Returns `true` if the z-component
50/// of `r × v` changes by less than `tol`.
51pub 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}
60/// Check Lyapunov stability of a fixed-point under a discrete map.
61///
62/// `states` is a trajectory; the function checks whether `‖state‖₂` is
63/// non-increasing (on average) — a sufficient condition for Lyapunov stability.
64pub 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}
73/// A symbolic state: each variable holds an interval.
74pub type SymbolicState = HashMap<String, IntervalDomain>;
75/// Symbolic execution step: apply a numeric update `x := x + delta` to the
76/// symbolic state, and check whether the result remains within `bounds`.
77pub 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}
93/// A concrete state in the transition system — the raw phase-space vector.
94pub type ConcreteState = Vec<f64>;
95/// A predicate-abstraction of `system` using `predicates`.
96///
97/// Each concrete state is mapped to an abstract state (a predicate bit-vector).
98/// An abstract transition exists whenever a concrete transition exists between
99/// the corresponding abstraction classes.
100fn 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}
134/// Run Bounded Model Checking on the abstract system for `bound` steps.
135///
136/// Starting from the abstract initial state, we do a bounded BFS.
137/// An abstract state "violates" the property if ANY of the concrete states it
138/// represents violates the property (sound over-approximation).
139fn 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}
183/// Check whether the abstract trace `abstract_trace` corresponds to a real
184/// concrete execution path in `system`.
185///
186/// The abstract trace is a sequence of abstract-state indices. Because the
187/// abstract system may merge multiple concrete states into one abstract state,
188/// an abstract step `A → B` can correspond to *multiple* concrete steps that
189/// stay within A before finally transitioning to B.
190///
191/// We therefore perform a **bounded forward reachability** check: starting
192/// from the initial concrete state, we expand reachable concrete states by
193/// following transitions that stay within the current abstract state, then
194/// cross into the next abstract state, and repeat for each abstract step.
195fn 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}
284/// Derive new predicates to eliminate a spurious abstract trace.
285///
286/// We identify the first "infeasible step" in `abstract_trace` — the transition
287/// `abs[k-1] → abs[k]` that has no concrete witness — and generate separating
288/// predicates from the coordinate differences between representative concrete
289/// states on each side of that gap.
290fn 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}
401/// Compute initial predicates from the safety property by sampling state-space
402/// boundaries implied by the property's structure.
403///
404/// Since `SafetyProperty` wraps an opaque closure, we derive boundary predicates
405/// for each state variable based on the representative concrete states in `system`.
406fn 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}
503/// Run the full CEGAR loop on `system`, verifying `property`.
504///
505/// Iterates:
506/// 1. Compute predicate abstraction from the current predicate set.
507/// 2. Run bounded model checking on the abstract system.
508/// 3. If an abstract counterexample is found, check its concreteness.
509/// 4. If spurious, refine predicates and repeat.
510/// 5. Terminate when verified, a concrete violation is found, or the
511///    iteration budget is exhausted.
512pub 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}
563/// Verify a collection of named properties against a simulation trace.
564///
565/// Returns a map from property name to `true`/`false`.
566pub 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}
575/// Return `true` if `predicate` holds for every element of `trace`.
576pub fn always<S, F: Fn(&S) -> bool>(trace: &[S], predicate: F) -> bool {
577    trace.iter().all(predicate)
578}
579/// Return `true` if `predicate` holds for at least one element of `trace`.
580pub fn eventually<S, F: Fn(&S) -> bool>(trace: &[S], predicate: F) -> bool {
581    trace.iter().any(predicate)
582}
583/// Return `true` if `predicate` holds at every position strictly after
584/// a position where `trigger` holds.
585pub 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}