Skip to main content

oxiphysics_core/
formal_verification.rs

1#![allow(clippy::type_complexity)]
2// Copyright 2026 COOLJAPAN OU (Team KitaSan)
3// SPDX-License-Identifier: Apache-2.0
4
5//! Formal verification methods for physics simulation correctness.
6//!
7//! This module provides model checking, abstract interpretation, bisimulation,
8//! propositional SAT solving, and conservation-law verifiers for physical
9//! simulation states.
10
11#![allow(dead_code)]
12
13use std::collections::{HashMap, HashSet, VecDeque};
14
15// ---------------------------------------------------------------------------
16// IntervalDomain
17// ---------------------------------------------------------------------------
18
19/// Closed interval `[lo, hi]` used as the abstract domain for variables.
20#[derive(Debug, Clone, Copy, PartialEq)]
21pub struct IntervalDomain {
22    /// Lower bound of the interval.
23    pub lo: f64,
24    /// Upper bound of the interval.
25    pub hi: f64,
26}
27
28impl IntervalDomain {
29    /// Create a new interval `[lo, hi]`.
30    ///
31    /// Panics in debug mode if `lo > hi`.
32    pub fn new(lo: f64, hi: f64) -> Self {
33        debug_assert!(lo <= hi, "invalid interval: {} > {}", lo, hi);
34        Self { lo, hi }
35    }
36
37    /// The "top" element — represents the universe of all reals.
38    pub fn top() -> Self {
39        Self {
40            lo: f64::NEG_INFINITY,
41            hi: f64::INFINITY,
42        }
43    }
44
45    /// The "bottom" element — represents the empty set (empty interval).
46    pub fn bottom() -> Self {
47        Self {
48            lo: f64::INFINITY,
49            hi: f64::NEG_INFINITY,
50        }
51    }
52
53    /// Returns `true` if the interval is non-empty.
54    pub fn is_non_empty(&self) -> bool {
55        self.lo <= self.hi
56    }
57
58    /// Returns `true` if `v` lies within this interval.
59    pub fn contains(&self, v: f64) -> bool {
60        v >= self.lo && v <= self.hi
61    }
62
63    /// Least upper bound (join): the smallest interval containing both.
64    pub fn join(&self, other: &Self) -> Self {
65        Self {
66            lo: self.lo.min(other.lo),
67            hi: self.hi.max(other.hi),
68        }
69    }
70
71    /// Greatest lower bound (meet): the intersection.
72    pub fn meet(&self, other: &Self) -> Self {
73        Self {
74            lo: self.lo.max(other.lo),
75            hi: self.hi.min(other.hi),
76        }
77    }
78
79    /// Returns `true` if `self ⊆ other`.
80    pub fn is_subset_of(&self, other: &Self) -> bool {
81        self.lo >= other.lo && self.hi <= other.hi
82    }
83
84    /// Interval addition.
85    pub fn add(&self, other: &Self) -> Self {
86        Self::new(self.lo + other.lo, self.hi + other.hi)
87    }
88
89    /// Interval subtraction.
90    pub fn sub(&self, other: &Self) -> Self {
91        Self::new(self.lo - other.hi, self.hi - other.lo)
92    }
93
94    /// Interval multiplication.
95    pub fn mul(&self, other: &Self) -> Self {
96        let products = [
97            self.lo * other.lo,
98            self.lo * other.hi,
99            self.hi * other.lo,
100            self.hi * other.hi,
101        ];
102        Self::new(
103            products.iter().cloned().fold(f64::INFINITY, f64::min),
104            products.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
105        )
106    }
107
108    /// Interval division (returns `top()` when divisor contains zero).
109    pub fn div(&self, other: &Self) -> Self {
110        if other.lo <= 0.0 && other.hi >= 0.0 {
111            Self::top()
112        } else {
113            let quotients = [
114                self.lo / other.lo,
115                self.lo / other.hi,
116                self.hi / other.lo,
117                self.hi / other.hi,
118            ];
119            Self::new(
120                quotients.iter().cloned().fold(f64::INFINITY, f64::min),
121                quotients.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
122            )
123        }
124    }
125
126    /// Width of the interval.
127    pub fn width(&self) -> f64 {
128        (self.hi - self.lo).max(0.0)
129    }
130
131    /// Midpoint of the interval.
132    pub fn midpoint(&self) -> f64 {
133        (self.lo + self.hi) / 2.0
134    }
135}
136
137impl std::fmt::Display for IntervalDomain {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        write!(f, "[{}, {}]", self.lo, self.hi)
140    }
141}
142
143// ---------------------------------------------------------------------------
144// LinearTemporalLogic
145// ---------------------------------------------------------------------------
146
147/// A Linear Temporal Logic (LTL) formula represented as a string.
148///
149/// The current implementation uses a simplified embedded DSL rather than a
150/// full LTL parser.
151#[derive(Debug, Clone)]
152pub struct LinearTemporalLogic {
153    /// The LTL formula expressed in textual form.
154    pub formula: String,
155}
156
157impl LinearTemporalLogic {
158    /// Create a new LTL formula.
159    pub fn new(formula: impl Into<String>) -> Self {
160        Self {
161            formula: formula.into(),
162        }
163    }
164
165    /// Check a safety property: returns `true` if the formula is satisfied
166    /// for all positions in `trace`.
167    ///
168    /// Safety properties have the form "always P" – here `predicate` is
169    /// evaluated on every state.
170    pub fn check_safety<S, F>(&self, trace: &[S], predicate: F) -> bool
171    where
172        F: Fn(&S) -> bool,
173    {
174        trace.iter().all(predicate)
175    }
176
177    /// Check a liveness property: returns `true` if `predicate` is `true` at
178    /// least once in `trace`.
179    ///
180    /// Liveness properties have the form "eventually P".
181    pub fn check_liveness<S, F>(&self, trace: &[S], predicate: F) -> bool
182    where
183        F: Fn(&S) -> bool,
184    {
185        trace.iter().any(predicate)
186    }
187
188    /// Check a "next" property: returns `true` if `predicate` holds at every
189    /// state immediately following a state where `trigger` holds.
190    pub fn check_next<S, F, G>(&self, trace: &[S], trigger: F, predicate: G) -> bool
191    where
192        F: Fn(&S) -> bool,
193        G: Fn(&S) -> bool,
194    {
195        for i in 0..trace.len().saturating_sub(1) {
196            if trigger(&trace[i]) && !predicate(&trace[i + 1]) {
197                return false;
198            }
199        }
200        true
201    }
202
203    /// Check an "until" property: `left` must hold until `right` holds.
204    pub fn check_until<S, F, G>(&self, trace: &[S], left: F, right: G) -> bool
205    where
206        F: Fn(&S) -> bool,
207        G: Fn(&S) -> bool,
208    {
209        let mut holding = false;
210        for s in trace {
211            if right(s) {
212                holding = true;
213                break;
214            }
215            if !left(s) {
216                return false;
217            }
218        }
219        holding
220    }
221}
222
223// ---------------------------------------------------------------------------
224// ModelChecker
225// ---------------------------------------------------------------------------
226
227/// State index type alias.
228pub type StateIdx = usize;
229
230/// A simple explicit-state model checker.
231///
232/// States are represented as `Vec`f64` (e.g., phase-space coordinates).
233/// Transitions are labeled pairs `(from, to)`.
234#[derive(Debug, Clone)]
235pub struct ModelChecker {
236    /// All states in the model.
237    pub states: Vec<Vec<f64>>,
238    /// Transitions as `(from_idx, to_idx)` pairs.
239    pub transitions: Vec<(StateIdx, StateIdx)>,
240}
241
242impl ModelChecker {
243    /// Create a new model checker from a set of states and transitions.
244    pub fn new(states: Vec<Vec<f64>>, transitions: Vec<(StateIdx, StateIdx)>) -> Self {
245        Self {
246            states,
247            transitions,
248        }
249    }
250
251    /// Return all states reachable from `start_idx` via BFS.
252    pub fn reachable_states(&self, start_idx: StateIdx) -> HashSet<StateIdx> {
253        let mut visited = HashSet::new();
254        let mut queue = VecDeque::new();
255        queue.push_back(start_idx);
256        visited.insert(start_idx);
257        while let Some(s) = queue.pop_front() {
258            for &(from, to) in &self.transitions {
259                if from == s && !visited.contains(&to) {
260                    visited.insert(to);
261                    queue.push_back(to);
262                }
263            }
264        }
265        visited
266    }
267
268    /// Check whether all reachable states from `start_idx` satisfy `invariant`.
269    pub fn satisfies_invariant<F>(&self, start_idx: StateIdx, invariant: F) -> bool
270    where
271        F: Fn(&[f64]) -> bool,
272    {
273        let reachable = self.reachable_states(start_idx);
274        reachable.iter().all(|&s| invariant(&self.states[s]))
275    }
276
277    /// Find a counterexample state (as index) that violates `invariant`.
278    ///
279    /// Returns `None` if all reachable states satisfy the invariant.
280    pub fn find_counterexample<F>(&self, start_idx: StateIdx, invariant: F) -> Option<StateIdx>
281    where
282        F: Fn(&[f64]) -> bool,
283    {
284        let reachable = self.reachable_states(start_idx);
285        reachable.into_iter().find(|&s| !invariant(&self.states[s]))
286    }
287
288    /// Return the shortest path (as a sequence of state indices) from
289    /// `start_idx` to `target_idx`, or `None` if unreachable.
290    pub fn shortest_path(
291        &self,
292        start_idx: StateIdx,
293        target_idx: StateIdx,
294    ) -> Option<Vec<StateIdx>> {
295        let mut parent: HashMap<StateIdx, StateIdx> = HashMap::new();
296        let mut queue = VecDeque::new();
297        queue.push_back(start_idx);
298        parent.insert(start_idx, start_idx);
299        while let Some(s) = queue.pop_front() {
300            if s == target_idx {
301                // Reconstruct path.
302                let mut path = Vec::new();
303                let mut cur = target_idx;
304                loop {
305                    path.push(cur);
306                    let prev = parent[&cur];
307                    if prev == cur {
308                        break;
309                    }
310                    cur = prev;
311                }
312                path.reverse();
313                return Some(path);
314            }
315            for &(from, to) in &self.transitions {
316                if from == s && !parent.contains_key(&to) {
317                    parent.insert(to, s);
318                    queue.push_back(to);
319                }
320            }
321        }
322        None
323    }
324
325    /// Count the number of reachable states from `start_idx`.
326    pub fn reachable_count(&self, start_idx: StateIdx) -> usize {
327        self.reachable_states(start_idx).len()
328    }
329}
330
331// ---------------------------------------------------------------------------
332// AbstractInterpretation
333// ---------------------------------------------------------------------------
334
335/// Abstract interpretation engine using the interval domain.
336#[derive(Debug, Clone)]
337pub struct AbstractInterpretation {
338    /// Abstract domain for each tracked variable.
339    pub domain: HashMap<String, IntervalDomain>,
340}
341
342impl AbstractInterpretation {
343    /// Create a new abstract interpreter with no variable bindings.
344    pub fn new() -> Self {
345        Self {
346            domain: HashMap::new(),
347        }
348    }
349
350    /// Bind a variable to a concrete value (singleton interval).
351    pub fn bind(&mut self, var: impl Into<String>, value: f64) {
352        let name = var.into();
353        self.domain.insert(name, IntervalDomain::new(value, value));
354    }
355
356    /// Bind a variable to an abstract interval.
357    pub fn bind_interval(&mut self, var: impl Into<String>, interval: IntervalDomain) {
358        self.domain.insert(var.into(), interval);
359    }
360
361    /// Return the abstract value of a variable, or `top()` if unknown.
362    pub fn get(&self, var: &str) -> IntervalDomain {
363        self.domain
364            .get(var)
365            .copied()
366            .unwrap_or_else(IntervalDomain::top)
367    }
368
369    /// Analyze a simple loop that adds `delta` to `var` each iteration for at
370    /// most `max_iter` iterations, using widening to ensure termination.
371    ///
372    /// Returns the post-loop interval for `var`.
373    pub fn analyze_loop(
374        &mut self,
375        var: &str,
376        delta: IntervalDomain,
377        max_iter: usize,
378    ) -> IntervalDomain {
379        let mut current = self.get(var);
380        for _i in 0..max_iter {
381            let next = current.add(&delta);
382            let joined = current.join(&next);
383            let widened = self.widening(&current, &joined);
384            if widened == current {
385                return current;
386            }
387            current = widened;
388        }
389        current
390    }
391
392    /// Widening operator: extends the interval to ensure convergence.
393    ///
394    /// If the lower bound decreases it goes to `-∞`; if the upper bound
395    /// increases it goes to `+∞`.
396    pub fn widening(&self, prev: &IntervalDomain, next: &IntervalDomain) -> IntervalDomain {
397        let lo = if next.lo < prev.lo {
398            f64::NEG_INFINITY
399        } else {
400            prev.lo
401        };
402        let hi = if next.hi > prev.hi {
403            f64::INFINITY
404        } else {
405            prev.hi
406        };
407        IntervalDomain { lo, hi }
408    }
409
410    /// Narrowing operator: refines an over-approximation using additional
411    /// concrete information.
412    pub fn narrowing(&self, prev: &IntervalDomain, next: &IntervalDomain) -> IntervalDomain {
413        let lo = if prev.lo == f64::NEG_INFINITY {
414            next.lo
415        } else {
416            prev.lo
417        };
418        let hi = if prev.hi == f64::INFINITY {
419            next.hi
420        } else {
421            prev.hi
422        };
423        IntervalDomain { lo, hi }
424    }
425
426    /// Check that `var` is always non-negative under the current abstraction.
427    pub fn is_non_negative(&self, var: &str) -> bool {
428        self.get(var).lo >= 0.0
429    }
430
431    /// Check that `var` is bounded (finite interval bounds).
432    pub fn is_bounded(&self, var: &str) -> bool {
433        let iv = self.get(var);
434        iv.lo.is_finite() && iv.hi.is_finite()
435    }
436}
437
438impl Default for AbstractInterpretation {
439    fn default() -> Self {
440        Self::new()
441    }
442}
443
444// ---------------------------------------------------------------------------
445// BisimulationChecker
446// ---------------------------------------------------------------------------
447
448/// Labeled transition system used for bisimulation checking.
449///
450/// Labels on transitions are `String`s.
451#[derive(Debug, Clone)]
452pub struct LabeledTransitionSystem {
453    /// Number of states.
454    pub n_states: usize,
455    /// Transitions: `(from, label, to)`.
456    pub transitions: Vec<(usize, String, usize)>,
457    /// Optional state labels (atomic propositions).
458    pub labels: Vec<Vec<String>>,
459}
460
461impl LabeledTransitionSystem {
462    /// Create a new LTS with `n_states` states and no transitions.
463    pub fn new(n_states: usize) -> Self {
464        Self {
465            n_states,
466            transitions: Vec::new(),
467            labels: vec![Vec::new(); n_states],
468        }
469    }
470
471    /// Add a labeled transition `from --label--> to`.
472    pub fn add_transition(&mut self, from: usize, label: impl Into<String>, to: usize) {
473        self.transitions.push((from, label.into(), to));
474    }
475
476    /// Add an atomic proposition to a state.
477    pub fn add_label(&mut self, state: usize, label: impl Into<String>) {
478        if state < self.labels.len() {
479            self.labels[state].push(label.into());
480        }
481    }
482}
483
484/// Bisimulation checker for labeled transition systems.
485#[derive(Debug)]
486pub struct BisimulationChecker {
487    /// The LTS to analyze.
488    pub lts: LabeledTransitionSystem,
489}
490
491impl BisimulationChecker {
492    /// Create a new bisimulation checker for `lts`.
493    pub fn new(lts: LabeledTransitionSystem) -> Self {
494        Self { lts }
495    }
496
497    /// Check whether states `s` and `t` are bisimilar in the LTS.
498    ///
499    /// Uses a partition-refinement approximation (up to `max_rounds` rounds).
500    pub fn are_bisimilar(&self, s: usize, t: usize) -> bool {
501        let partition = self.compute_quotient();
502        let s_class = partition.get(&s).copied().unwrap_or(s);
503        let t_class = partition.get(&t).copied().unwrap_or(t);
504        s_class == t_class
505    }
506
507    /// Compute the bisimulation quotient using Paige-Tarjan partition
508    /// refinement (simplified version).
509    ///
510    /// Returns a map from state index to equivalence class index.
511    pub fn compute_quotient(&self) -> HashMap<usize, usize> {
512        let n = self.lts.n_states;
513        // Initial partition: group by atomic propositions.
514        let mut partition: Vec<usize> = (0..n).map(|_| 0).collect();
515        // Assign initial classes based on label sets.
516        let mut label_to_class: HashMap<Vec<String>, usize> = HashMap::new();
517        let mut next_class = 0usize;
518        for (i, labels) in self.lts.labels.iter().enumerate() {
519            let mut sorted_labels = labels.clone();
520            sorted_labels.sort();
521            let class = *label_to_class.entry(sorted_labels).or_insert_with(|| {
522                let c = next_class;
523                next_class += 1;
524                c
525            });
526            partition[i] = class;
527        }
528
529        // Refinement loop.
530        loop {
531            let old_partition = partition.clone();
532            // For each state, compute a signature: (class, sorted list of (label, target_class)).
533            let mut signatures: Vec<(usize, Vec<(String, usize)>)> = Vec::new();
534            for i in 0..n {
535                let mut sig: Vec<(String, usize)> = self
536                    .lts
537                    .transitions
538                    .iter()
539                    .filter(|(from, _, _)| *from == i)
540                    .map(|(_, label, to)| (label.clone(), partition[*to]))
541                    .collect();
542                sig.sort();
543                signatures.push((partition[i], sig));
544            }
545            // Re-assign classes.
546            let mut sig_to_class: HashMap<(usize, Vec<(String, usize)>), usize> = HashMap::new();
547            next_class = 0;
548            for (i, sig) in signatures.iter().enumerate() {
549                let class = *sig_to_class.entry(sig.clone()).or_insert_with(|| {
550                    let c = next_class;
551                    next_class += 1;
552                    c
553                });
554                partition[i] = class;
555            }
556            if partition == old_partition {
557                break;
558            }
559        }
560        partition.into_iter().enumerate().collect()
561    }
562
563    /// Return the number of equivalence classes in the quotient.
564    pub fn quotient_size(&self) -> usize {
565        let q = self.compute_quotient();
566        let classes: HashSet<usize> = q.values().copied().collect();
567        classes.len()
568    }
569}
570
571// ---------------------------------------------------------------------------
572// Conservation law verifiers
573// ---------------------------------------------------------------------------
574
575/// Verify energy conservation over a trajectory of states.
576///
577/// Each state is a `\[f64; 6\]` array of the form `\[x, y, z, vx, vy, vz\]`.
578/// Energy is estimated as kinetic energy `0.5 * (vx² + vy² + vz²)` (unit mass).
579///
580/// Returns `true` if the energy variation across all states is within `tol`.
581pub fn verify_energy_conservation(states: &[[f64; 6]], tol: f64) -> bool {
582    if states.len() < 2 {
583        return true;
584    }
585    let energies: Vec<f64> = states
586        .iter()
587        .map(|s| 0.5 * (s[3] * s[3] + s[4] * s[4] + s[5] * s[5]))
588        .collect();
589    let e0 = energies[0];
590    energies.iter().all(|&e| (e - e0).abs() <= tol)
591}
592
593/// Verify linear momentum conservation over a trajectory of states.
594///
595/// Each state is `\[x, y, z, vx, vy, vz\]` (unit mass).
596/// Returns `true` if the momentum vector changes by less than `tol` per
597/// component.
598pub fn verify_momentum_conservation(states: &[[f64; 6]], tol: f64) -> bool {
599    if states.len() < 2 {
600        return true;
601    }
602    let (px0, py0, pz0) = (states[0][3], states[0][4], states[0][5]);
603    states.iter().all(|s| {
604        (s[3] - px0).abs() <= tol && (s[4] - py0).abs() <= tol && (s[5] - pz0).abs() <= tol
605    })
606}
607
608/// Verify angular momentum conservation.
609///
610/// Each state is `\[x, y, z, vx, vy, vz\]`.  Returns `true` if the z-component
611/// of `r × v` changes by less than `tol`.
612pub fn verify_angular_momentum_conservation(states: &[[f64; 6]], tol: f64) -> bool {
613    if states.len() < 2 {
614        return true;
615    }
616    // Lz = x*vy - y*vx
617    let lz0 = states[0][0] * states[0][4] - states[0][1] * states[0][3];
618    states
619        .iter()
620        .all(|s| ((s[0] * s[4] - s[1] * s[3]) - lz0).abs() <= tol)
621}
622
623// ---------------------------------------------------------------------------
624// SatisfiabilityChecker (DPLL)
625// ---------------------------------------------------------------------------
626
627/// A literal in a propositional clause.
628#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
629pub struct Literal {
630    /// Variable index (0-based).
631    pub var: usize,
632    /// Polarity: `true` = positive, `false` = negative.
633    pub positive: bool,
634}
635
636impl Literal {
637    /// Positive literal for variable `var`.
638    pub fn pos(var: usize) -> Self {
639        Self {
640            var,
641            positive: true,
642        }
643    }
644
645    /// Negative literal for variable `var`.
646    pub fn neg(var: usize) -> Self {
647        Self {
648            var,
649            positive: false,
650        }
651    }
652
653    /// Return the negation of this literal.
654    pub fn negate(&self) -> Self {
655        Self {
656            var: self.var,
657            positive: !self.positive,
658        }
659    }
660}
661
662/// A propositional formula in Conjunctive Normal Form (CNF).
663///
664/// Clauses are disjunctions; the formula is the conjunction of all clauses.
665#[derive(Debug, Clone)]
666pub struct SatisfiabilityChecker {
667    /// Number of propositional variables.
668    pub n_vars: usize,
669    /// CNF clauses.
670    pub clauses: Vec<Vec<Literal>>,
671}
672
673impl SatisfiabilityChecker {
674    /// Create an empty SAT checker with `n_vars` variables.
675    pub fn new(n_vars: usize) -> Self {
676        Self {
677            n_vars,
678            clauses: Vec::new(),
679        }
680    }
681
682    /// Add a clause (disjunction of literals).
683    pub fn add_clause(&mut self, clause: Vec<Literal>) {
684        self.clauses.push(clause);
685    }
686
687    /// Check satisfiability using the DPLL algorithm.
688    ///
689    /// Returns `Some(assignment)` where `assignment\[i\]` is the truth value
690    /// for variable `i`, or `None` if unsatisfiable.
691    pub fn solve(&self) -> Option<Vec<bool>> {
692        let mut assignment = vec![None::<bool>; self.n_vars];
693        if self.dpll(&mut assignment) {
694            Some(assignment.iter().map(|a| a.unwrap_or(true)).collect())
695        } else {
696            None
697        }
698    }
699
700    fn dpll(&self, assignment: &mut Vec<Option<bool>>) -> bool {
701        // Unit propagation.
702        loop {
703            let mut propagated = false;
704            for clause in &self.clauses {
705                let (unassigned, satisfied) = self.clause_status(clause, assignment);
706                if satisfied {
707                    continue;
708                }
709                if unassigned.is_empty() {
710                    return false; // Conflict.
711                }
712                if unassigned.len() == 1 {
713                    // Unit clause: force the literal.
714                    let lit = unassigned[0];
715                    assignment[lit.var] = Some(lit.positive);
716                    propagated = true;
717                }
718            }
719            if !propagated {
720                break;
721            }
722        }
723
724        // Check if all clauses are satisfied.
725        if self.all_satisfied(assignment) {
726            return true;
727        }
728        // Check for conflict.
729        if self.has_conflict(assignment) {
730            return false;
731        }
732
733        // Choose an unassigned variable and branch.
734        if let Some(var) = (0..self.n_vars).find(|&v| assignment[v].is_none()) {
735            for &value in &[true, false] {
736                let mut child = assignment.clone();
737                child[var] = Some(value);
738                if self.dpll(&mut child) {
739                    *assignment = child;
740                    return true;
741                }
742            }
743        }
744        false
745    }
746
747    fn clause_status<'a>(
748        &self,
749        clause: &'a [Literal],
750        assignment: &[Option<bool>],
751    ) -> (Vec<&'a Literal>, bool) {
752        let mut unassigned = Vec::new();
753        for lit in clause {
754            match assignment[lit.var] {
755                Some(val) if val == lit.positive => return (vec![], true),
756                None => unassigned.push(lit),
757                _ => {}
758            }
759        }
760        (unassigned, false)
761    }
762
763    fn all_satisfied(&self, assignment: &[Option<bool>]) -> bool {
764        self.clauses.iter().all(|clause| {
765            clause
766                .iter()
767                .any(|lit| assignment[lit.var] == Some(lit.positive))
768        })
769    }
770
771    fn has_conflict(&self, assignment: &[Option<bool>]) -> bool {
772        self.clauses.iter().any(|clause| {
773            clause
774                .iter()
775                .all(|lit| assignment[lit.var].is_some_and(|v| v != lit.positive))
776        })
777    }
778
779    /// Check whether the given assignment satisfies all clauses.
780    pub fn check_assignment(&self, assignment: &[bool]) -> bool {
781        self.clauses
782            .iter()
783            .all(|clause| clause.iter().any(|lit| assignment[lit.var] == lit.positive))
784    }
785
786    /// Return the number of clauses.
787    pub fn clause_count(&self) -> usize {
788        self.clauses.len()
789    }
790}
791
792// ---------------------------------------------------------------------------
793// Lyapunov stability checker
794// ---------------------------------------------------------------------------
795
796/// Check Lyapunov stability of a fixed-point under a discrete map.
797///
798/// `states` is a trajectory; the function checks whether `‖state‖₂` is
799/// non-increasing (on average) — a sufficient condition for Lyapunov stability.
800pub fn check_lyapunov_stability(states: &[Vec<f64>]) -> bool {
801    if states.len() < 2 {
802        return true;
803    }
804    let norm = |s: &Vec<f64>| s.iter().map(|x| x * x).sum::<f64>().sqrt();
805    let first_norm = norm(&states[0]);
806    let last_norm = norm(states.last().expect("states has at least 2 entries"));
807    last_norm <= first_norm * 1.001 // 0.1% tolerance for numerical drift
808}
809
810// ---------------------------------------------------------------------------
811// Invariant specification helpers
812// ---------------------------------------------------------------------------
813
814/// A named invariant with a predicate over phase-space vectors.
815pub struct Invariant {
816    /// Name of the invariant.
817    pub name: String,
818    /// Predicate: returns `true` if the state satisfies the invariant.
819    pub predicate: Box<dyn Fn(&[f64]) -> bool + Send + Sync>,
820}
821
822impl Invariant {
823    /// Create a new invariant.
824    pub fn new(
825        name: impl Into<String>,
826        predicate: impl Fn(&[f64]) -> bool + Send + Sync + 'static,
827    ) -> Self {
828        Self {
829            name: name.into(),
830            predicate: Box::new(predicate),
831        }
832    }
833
834    /// Evaluate the invariant on a state.
835    pub fn check(&self, state: &[f64]) -> bool {
836        (self.predicate)(state)
837    }
838
839    /// Verify the invariant holds for an entire trajectory.
840    pub fn verify_trajectory(&self, trajectory: &[Vec<f64>]) -> bool {
841        trajectory.iter().all(|s| self.check(s))
842    }
843}
844
845impl std::fmt::Debug for Invariant {
846    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
847        f.debug_struct("Invariant")
848            .field("name", &self.name)
849            .finish()
850    }
851}
852
853// ---------------------------------------------------------------------------
854// Symbolic execution engine (simple numeric version)
855// ---------------------------------------------------------------------------
856
857/// A symbolic state: each variable holds an interval.
858pub type SymbolicState = HashMap<String, IntervalDomain>;
859
860/// Symbolic execution step: apply a numeric update `x := x + delta` to the
861/// symbolic state, and check whether the result remains within `bounds`.
862pub fn symbolic_step(
863    state: &SymbolicState,
864    var: &str,
865    delta: f64,
866    bounds: &IntervalDomain,
867) -> Option<SymbolicState> {
868    let iv = state.get(var).copied().unwrap_or_else(IntervalDomain::top);
869    let next_iv = IntervalDomain::new(iv.lo + delta, iv.hi + delta);
870    if next_iv.meet(bounds).is_non_empty() {
871        let mut next = state.clone();
872        next.insert(var.to_string(), next_iv.meet(bounds));
873        Some(next)
874    } else {
875        None
876    }
877}
878
879// ---------------------------------------------------------------------------
880// Counterexample-guided abstraction refinement (CEGAR) stub
881// ---------------------------------------------------------------------------
882
883/// A simple CEGAR loop result.
884#[derive(Debug, Clone)]
885pub enum CegarResult {
886    /// The property was verified (no counterexample exists).
887    Verified,
888    /// A concrete counterexample was found (sequence of state indices).
889    Counterexample(Vec<StateIdx>),
890    /// The analysis exceeded the iteration budget without a definitive answer.
891    Unknown,
892}
893
894/// Run a simplified CEGAR loop on `checker`.
895///
896/// Checks `invariant` starting from `start_idx`; returns the result.
897pub fn cegar_verify<F>(
898    checker: &ModelChecker,
899    start_idx: StateIdx,
900    invariant: F,
901    _max_iterations: usize,
902) -> CegarResult
903where
904    F: Fn(&[f64]) -> bool,
905{
906    match checker.find_counterexample(start_idx, &invariant) {
907        None => CegarResult::Verified,
908        Some(cex_state) => {
909            // Build a simple path to the counterexample.
910            let path = checker
911                .shortest_path(start_idx, cex_state)
912                .unwrap_or_else(|| vec![cex_state]);
913            CegarResult::Counterexample(path)
914        }
915    }
916}
917
918// ---------------------------------------------------------------------------
919// Simulation trace verifier
920// ---------------------------------------------------------------------------
921
922/// Verify a collection of named properties against a simulation trace.
923///
924/// Returns a map from property name to `true`/`false`.
925pub fn verify_trace_properties(
926    trace: &[Vec<f64>],
927    properties: &[(&str, Box<dyn Fn(&[Vec<f64>]) -> bool>)],
928) -> HashMap<String, bool> {
929    properties
930        .iter()
931        .map(|(name, predicate)| (name.to_string(), predicate(trace)))
932        .collect()
933}
934
935// ---------------------------------------------------------------------------
936// Type-state protocol checker
937// ---------------------------------------------------------------------------
938
939/// Protocol state machine for type-state verification.
940#[derive(Debug, Clone)]
941pub struct TypeStateProtocol {
942    /// Current state of the protocol.
943    pub current_state: String,
944    /// Allowed transitions: `(from, event, to)`.
945    pub transitions: Vec<(String, String, String)>,
946}
947
948impl TypeStateProtocol {
949    /// Create a new protocol starting in `initial_state`.
950    pub fn new(initial_state: impl Into<String>) -> Self {
951        Self {
952            current_state: initial_state.into(),
953            transitions: Vec::new(),
954        }
955    }
956
957    /// Register an allowed transition.
958    pub fn add_transition(
959        &mut self,
960        from: impl Into<String>,
961        event: impl Into<String>,
962        to: impl Into<String>,
963    ) {
964        self.transitions
965            .push((from.into(), event.into(), to.into()));
966    }
967
968    /// Attempt to fire `event`.  Returns `Ok(new_state)` or `Err(msg)`.
969    pub fn fire(&mut self, event: &str) -> Result<String, String> {
970        for (from, ev, to) in &self.transitions {
971            if from == &self.current_state && ev == event {
972                self.current_state = to.clone();
973                return Ok(to.clone());
974            }
975        }
976        Err(format!(
977            "no transition from '{}' on event '{}'",
978            self.current_state, event
979        ))
980    }
981
982    /// Return `true` if `state` is reachable from the initial state.
983    pub fn is_reachable(&self, initial: &str, target: &str) -> bool {
984        let mut visited = HashSet::new();
985        let mut queue = VecDeque::new();
986        queue.push_back(initial.to_string());
987        visited.insert(initial.to_string());
988        while let Some(s) = queue.pop_front() {
989            if s == target {
990                return true;
991            }
992            for (from, _, to) in &self.transitions {
993                if from == &s && !visited.contains(to) {
994                    visited.insert(to.clone());
995                    queue.push_back(to.clone());
996                }
997            }
998        }
999        false
1000    }
1001}
1002
1003// ---------------------------------------------------------------------------
1004// Temporal property macros (as free functions)
1005// ---------------------------------------------------------------------------
1006
1007/// Return `true` if `predicate` holds for every element of `trace`.
1008pub fn always<S, F: Fn(&S) -> bool>(trace: &[S], predicate: F) -> bool {
1009    trace.iter().all(predicate)
1010}
1011
1012/// Return `true` if `predicate` holds for at least one element of `trace`.
1013pub fn eventually<S, F: Fn(&S) -> bool>(trace: &[S], predicate: F) -> bool {
1014    trace.iter().any(predicate)
1015}
1016
1017/// Return `true` if `predicate` holds at every position strictly after
1018/// a position where `trigger` holds.
1019pub fn globally_after<S, F: Fn(&S) -> bool, G: Fn(&S) -> bool>(
1020    trace: &[S],
1021    trigger: F,
1022    predicate: G,
1023) -> bool {
1024    let mut triggered = false;
1025    for s in trace {
1026        if triggered && !predicate(s) {
1027            return false;
1028        }
1029        if trigger(s) {
1030            triggered = true;
1031        }
1032    }
1033    true
1034}
1035
1036// ---------------------------------------------------------------------------
1037// Tests
1038// ---------------------------------------------------------------------------
1039
1040#[cfg(test)]
1041mod tests {
1042    use super::*;
1043
1044    // -- IntervalDomain --
1045
1046    #[test]
1047    fn test_interval_contains() {
1048        let iv = IntervalDomain::new(1.0, 3.0);
1049        assert!(iv.contains(2.0));
1050        assert!(!iv.contains(4.0));
1051    }
1052
1053    #[test]
1054    fn test_interval_join() {
1055        let a = IntervalDomain::new(1.0, 3.0);
1056        let b = IntervalDomain::new(2.0, 5.0);
1057        let j = a.join(&b);
1058        assert_eq!(j.lo, 1.0);
1059        assert_eq!(j.hi, 5.0);
1060    }
1061
1062    #[test]
1063    fn test_interval_meet() {
1064        let a = IntervalDomain::new(1.0, 4.0);
1065        let b = IntervalDomain::new(2.0, 6.0);
1066        let m = a.meet(&b);
1067        assert_eq!(m.lo, 2.0);
1068        assert_eq!(m.hi, 4.0);
1069    }
1070
1071    #[test]
1072    fn test_interval_add() {
1073        let a = IntervalDomain::new(1.0, 2.0);
1074        let b = IntervalDomain::new(3.0, 4.0);
1075        let c = a.add(&b);
1076        assert_eq!(c.lo, 4.0);
1077        assert_eq!(c.hi, 6.0);
1078    }
1079
1080    #[test]
1081    fn test_interval_sub() {
1082        let a = IntervalDomain::new(3.0, 5.0);
1083        let b = IntervalDomain::new(1.0, 2.0);
1084        let c = a.sub(&b);
1085        assert_eq!(c.lo, 1.0);
1086        assert_eq!(c.hi, 4.0);
1087    }
1088
1089    #[test]
1090    fn test_interval_mul() {
1091        let a = IntervalDomain::new(2.0, 3.0);
1092        let b = IntervalDomain::new(4.0, 5.0);
1093        let c = a.mul(&b);
1094        assert_eq!(c.lo, 8.0);
1095        assert_eq!(c.hi, 15.0);
1096    }
1097
1098    #[test]
1099    fn test_interval_div_no_zero() {
1100        let a = IntervalDomain::new(6.0, 8.0);
1101        let b = IntervalDomain::new(2.0, 4.0);
1102        let c = a.div(&b);
1103        assert!(c.lo >= 1.5 - 1e-9);
1104        assert!(c.hi <= 4.0 + 1e-9);
1105    }
1106
1107    #[test]
1108    fn test_interval_div_contains_zero() {
1109        let a = IntervalDomain::new(1.0, 2.0);
1110        let b = IntervalDomain::new(-1.0, 1.0);
1111        let c = a.div(&b);
1112        assert_eq!(c, IntervalDomain::top());
1113    }
1114
1115    #[test]
1116    fn test_interval_width() {
1117        let iv = IntervalDomain::new(2.0, 5.0);
1118        assert!((iv.width() - 3.0).abs() < 1e-10);
1119    }
1120
1121    // -- LinearTemporalLogic --
1122
1123    #[test]
1124    fn test_ltl_check_safety() {
1125        let ltl = LinearTemporalLogic::new("always x >= 0");
1126        let trace = vec![1.0_f64, 2.0, 3.0];
1127        assert!(ltl.check_safety(&trace, |&x| x >= 0.0));
1128        let bad_trace = vec![1.0_f64, -1.0, 3.0];
1129        assert!(!ltl.check_safety(&bad_trace, |&x| x >= 0.0));
1130    }
1131
1132    #[test]
1133    fn test_ltl_check_liveness() {
1134        let ltl = LinearTemporalLogic::new("eventually x > 10");
1135        let trace = vec![1.0_f64, 5.0, 11.0];
1136        assert!(ltl.check_liveness(&trace, |&x| x > 10.0));
1137        let bad = vec![1.0_f64, 2.0, 3.0];
1138        assert!(!ltl.check_liveness(&bad, |&x| x > 10.0));
1139    }
1140
1141    #[test]
1142    fn test_ltl_check_next() {
1143        let ltl = LinearTemporalLogic::new("next");
1144        let trace = vec![true, true, false];
1145        // trigger = identity; next must also be true
1146        assert!(!ltl.check_next(&trace, |&x| x, |&x| x));
1147    }
1148
1149    #[test]
1150    fn test_ltl_check_until() {
1151        let ltl = LinearTemporalLogic::new("until");
1152        let trace = vec![1i32, 1, 1, 2];
1153        assert!(ltl.check_until(&trace, |&x| x == 1, |&x| x == 2));
1154        let bad = vec![1i32, 0, 2];
1155        assert!(!ltl.check_until(&bad, |&x| x == 1, |&x| x == 2));
1156    }
1157
1158    // -- ModelChecker --
1159
1160    fn simple_model() -> ModelChecker {
1161        // States: 0 → 1 → 2
1162        ModelChecker::new(vec![vec![0.0], vec![1.0], vec![2.0]], vec![(0, 1), (1, 2)])
1163    }
1164
1165    #[test]
1166    fn test_model_reachable_states() {
1167        let mc = simple_model();
1168        let r = mc.reachable_states(0);
1169        assert!(r.contains(&0));
1170        assert!(r.contains(&1));
1171        assert!(r.contains(&2));
1172    }
1173
1174    #[test]
1175    fn test_model_satisfies_invariant() {
1176        let mc = simple_model();
1177        assert!(mc.satisfies_invariant(0, |s| s[0] >= 0.0));
1178        assert!(!mc.satisfies_invariant(0, |s| s[0] > 1.5));
1179    }
1180
1181    #[test]
1182    fn test_model_find_counterexample() {
1183        let mc = simple_model();
1184        let cex = mc.find_counterexample(0, |s| s[0] < 2.0);
1185        assert_eq!(cex, Some(2));
1186    }
1187
1188    #[test]
1189    fn test_model_shortest_path() {
1190        let mc = simple_model();
1191        let path = mc.shortest_path(0, 2).unwrap();
1192        assert_eq!(path, vec![0, 1, 2]);
1193    }
1194
1195    // -- AbstractInterpretation --
1196
1197    #[test]
1198    fn test_abstract_interp_bind_get() {
1199        let mut ai = AbstractInterpretation::new();
1200        ai.bind("x", 3.0);
1201        assert_eq!(ai.get("x"), IntervalDomain::new(3.0, 3.0));
1202    }
1203
1204    #[test]
1205    fn test_abstract_interp_widening() {
1206        let ai = AbstractInterpretation::new();
1207        let prev = IntervalDomain::new(0.0, 1.0);
1208        let next = IntervalDomain::new(0.0, 2.0);
1209        let w = ai.widening(&prev, &next);
1210        assert_eq!(w.hi, f64::INFINITY);
1211    }
1212
1213    #[test]
1214    fn test_abstract_interp_narrowing() {
1215        let ai = AbstractInterpretation::new();
1216        let prev = IntervalDomain::new(f64::NEG_INFINITY, f64::INFINITY);
1217        let next = IntervalDomain::new(-5.0, 5.0);
1218        let n = ai.narrowing(&prev, &next);
1219        assert_eq!(n.lo, -5.0);
1220        assert_eq!(n.hi, 5.0);
1221    }
1222
1223    #[test]
1224    fn test_abstract_interp_is_non_negative() {
1225        let mut ai = AbstractInterpretation::new();
1226        ai.bind_interval("x", IntervalDomain::new(0.0, 10.0));
1227        assert!(ai.is_non_negative("x"));
1228        ai.bind_interval("y", IntervalDomain::new(-1.0, 10.0));
1229        assert!(!ai.is_non_negative("y"));
1230    }
1231
1232    // -- BisimulationChecker --
1233
1234    #[test]
1235    fn test_bisimulation_same_state() {
1236        let mut lts = LabeledTransitionSystem::new(2);
1237        lts.add_label(0, "a");
1238        lts.add_label(1, "a");
1239        lts.add_transition(0, "x", 0);
1240        lts.add_transition(1, "x", 1);
1241        let checker = BisimulationChecker::new(lts);
1242        assert!(checker.are_bisimilar(0, 1));
1243    }
1244
1245    #[test]
1246    fn test_bisimulation_different_labels() {
1247        let mut lts = LabeledTransitionSystem::new(2);
1248        lts.add_label(0, "a");
1249        lts.add_label(1, "b");
1250        let checker = BisimulationChecker::new(lts);
1251        assert!(!checker.are_bisimilar(0, 1));
1252    }
1253
1254    #[test]
1255    fn test_bisimulation_quotient_size() {
1256        let mut lts = LabeledTransitionSystem::new(3);
1257        lts.add_label(0, "a");
1258        lts.add_label(1, "a");
1259        lts.add_label(2, "b");
1260        let checker = BisimulationChecker::new(lts);
1261        assert_eq!(checker.quotient_size(), 2);
1262    }
1263
1264    // -- Conservation verifiers --
1265
1266    #[test]
1267    fn test_verify_energy_conservation_ok() {
1268        let states = vec![[0.0, 0.0, 0.0, 1.0, 0.0, 0.0]; 5];
1269        assert!(verify_energy_conservation(&states, 1e-9));
1270    }
1271
1272    #[test]
1273    fn test_verify_energy_conservation_fail() {
1274        let s1 = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0];
1275        let s2 = [0.0, 0.0, 0.0, 2.0, 0.0, 0.0];
1276        assert!(!verify_energy_conservation(&[s1, s2], 1e-9));
1277    }
1278
1279    #[test]
1280    fn test_verify_momentum_conservation_ok() {
1281        let states = vec![[0.0, 0.0, 0.0, 1.0, 2.0, 3.0]; 4];
1282        assert!(verify_momentum_conservation(&states, 1e-9));
1283    }
1284
1285    #[test]
1286    fn test_verify_momentum_conservation_fail() {
1287        let s1 = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0];
1288        let s2 = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
1289        assert!(!verify_momentum_conservation(&[s1, s2], 1e-9));
1290    }
1291
1292    // -- SatisfiabilityChecker --
1293
1294    #[test]
1295    fn test_sat_trivial_sat() {
1296        // (x0) — always satisfiable.
1297        let mut sat = SatisfiabilityChecker::new(1);
1298        sat.add_clause(vec![Literal::pos(0)]);
1299        let result = sat.solve();
1300        assert!(result.is_some());
1301        let assignment = result.unwrap();
1302        assert!(sat.check_assignment(&assignment));
1303    }
1304
1305    #[test]
1306    fn test_sat_trivial_unsat() {
1307        // (x0) ∧ (¬x0) — unsatisfiable.
1308        let mut sat = SatisfiabilityChecker::new(1);
1309        sat.add_clause(vec![Literal::pos(0)]);
1310        sat.add_clause(vec![Literal::neg(0)]);
1311        assert!(sat.solve().is_none());
1312    }
1313
1314    #[test]
1315    fn test_sat_two_vars() {
1316        // (x0 ∨ x1) ∧ (¬x0 ∨ x1) — satisfiable with x1=true.
1317        let mut sat = SatisfiabilityChecker::new(2);
1318        sat.add_clause(vec![Literal::pos(0), Literal::pos(1)]);
1319        sat.add_clause(vec![Literal::neg(0), Literal::pos(1)]);
1320        let result = sat.solve();
1321        assert!(result.is_some());
1322        assert!(sat.check_assignment(&result.unwrap()));
1323    }
1324
1325    #[test]
1326    fn test_sat_check_assignment() {
1327        let mut sat = SatisfiabilityChecker::new(2);
1328        sat.add_clause(vec![Literal::pos(0), Literal::pos(1)]);
1329        assert!(sat.check_assignment(&[false, true]));
1330        assert!(!sat.check_assignment(&[false, false]));
1331    }
1332
1333    // -- TypeStateProtocol --
1334
1335    #[test]
1336    fn test_type_state_fire_ok() {
1337        let mut proto = TypeStateProtocol::new("idle");
1338        proto.add_transition("idle", "start", "running");
1339        proto.add_transition("running", "stop", "idle");
1340        assert!(proto.fire("start").is_ok());
1341        assert_eq!(proto.current_state, "running");
1342    }
1343
1344    #[test]
1345    fn test_type_state_fire_err() {
1346        let mut proto = TypeStateProtocol::new("idle");
1347        proto.add_transition("idle", "start", "running");
1348        assert!(proto.fire("stop").is_err());
1349    }
1350
1351    #[test]
1352    fn test_type_state_is_reachable() {
1353        let mut proto = TypeStateProtocol::new("idle");
1354        proto.add_transition("idle", "start", "running");
1355        proto.add_transition("running", "pause", "paused");
1356        assert!(proto.is_reachable("idle", "paused"));
1357        assert!(!proto.is_reachable("idle", "done"));
1358    }
1359
1360    // -- Temporal helpers --
1361
1362    #[test]
1363    fn test_always() {
1364        assert!(always(&[1, 2, 3], |&x| x > 0));
1365        assert!(!always(&[1, -1, 3], |&x| x > 0));
1366    }
1367
1368    #[test]
1369    fn test_eventually() {
1370        assert!(eventually(&[0, 0, 5], |&x| x > 4));
1371        assert!(!eventually(&[0, 0, 0], |&x| x > 4));
1372    }
1373
1374    #[test]
1375    fn test_globally_after() {
1376        // After seeing 1, every element must be positive.
1377        let trace = vec![0i32, 1, 2, 3];
1378        assert!(globally_after(&trace, |&x| x == 1, |&x| x > 0));
1379    }
1380
1381    // -- Lyapunov stability --
1382
1383    #[test]
1384    fn test_lyapunov_stable() {
1385        let trace: Vec<Vec<f64>> = (0..5).map(|i| vec![1.0 / (i as f64 + 1.0)]).collect();
1386        assert!(check_lyapunov_stability(&trace));
1387    }
1388
1389    // -- CEGAR --
1390
1391    #[test]
1392    fn test_cegar_verified() {
1393        let mc = simple_model();
1394        let result = cegar_verify(&mc, 0, |s| s[0] >= 0.0, 10);
1395        assert!(matches!(result, CegarResult::Verified));
1396    }
1397
1398    #[test]
1399    fn test_cegar_counterexample() {
1400        let mc = simple_model();
1401        let result = cegar_verify(&mc, 0, |s| s[0] < 2.0, 10);
1402        assert!(matches!(result, CegarResult::Counterexample(_)));
1403    }
1404
1405    // -- Symbolic execution --
1406
1407    #[test]
1408    fn test_symbolic_step_ok() {
1409        let mut state: SymbolicState = HashMap::new();
1410        state.insert("x".to_string(), IntervalDomain::new(0.0, 5.0));
1411        let bounds = IntervalDomain::new(0.0, 10.0);
1412        let next = symbolic_step(&state, "x", 1.0, &bounds);
1413        assert!(next.is_some());
1414    }
1415
1416    #[test]
1417    fn test_symbolic_step_out_of_bounds() {
1418        let mut state: SymbolicState = HashMap::new();
1419        state.insert("x".to_string(), IntervalDomain::new(9.0, 10.0));
1420        let bounds = IntervalDomain::new(0.0, 5.0);
1421        let next = symbolic_step(&state, "x", 3.0, &bounds);
1422        assert!(next.is_none());
1423    }
1424}