Skip to main content

libpetri_verification/
dbm.rs

1const EPSILON: f64 = 1e-9;
2
3/// Difference Bound Matrix for encoding firing domains in Time Petri Nets.
4///
5/// Float64 matrix representation. Reference clock at index 0,
6/// transition clocks at indices 1..n.
7///
8/// bounds[i][j] represents the upper bound on (x_i - x_j).
9#[derive(Debug, Clone)]
10pub struct Dbm {
11    dim: usize,
12    data: Vec<f64>,
13    clock_names: Vec<String>,
14    empty: bool,
15}
16
17impl Dbm {
18    /// Creates a new DBM of the given size (n clocks + reference clock 0).
19    pub fn new(size: usize) -> Self {
20        let data = vec![f64::INFINITY; size * size];
21        let mut dbm = Self {
22            dim: size,
23            data,
24            clock_names: Vec::new(),
25            empty: false,
26        };
27        for i in 0..size {
28            dbm.set(i, i, 0.0);
29        }
30        dbm
31    }
32
33    /// Creates an initial firing domain for enabled transitions.
34    pub fn create(clock_names: Vec<String>, lower_bounds: &[f64], upper_bounds: &[f64]) -> Self {
35        let n = clock_names.len();
36        let dim = n + 1;
37        let mut dbm = Self::new(dim);
38        dbm.clock_names = clock_names;
39
40        for i in 0..n {
41            dbm.set(0, i + 1, -lower_bounds[i]); // -lower bound
42            dbm.set(i + 1, 0, upper_bounds[i]); // upper bound
43        }
44
45        dbm.canonicalize();
46        dbm
47    }
48
49    /// Creates an empty (unsatisfiable) DBM.
50    pub fn empty_dbm() -> Self {
51        Self {
52            dim: 1,
53            data: vec![0.0],
54            clock_names: Vec::new(),
55            empty: true,
56        }
57    }
58
59    /// Returns the matrix size (dim x dim).
60    pub fn size(&self) -> usize {
61        self.dim
62    }
63
64    /// Returns the clock names.
65    pub fn clock_names(&self) -> &[String] {
66        &self.clock_names
67    }
68
69    /// Returns true if the DBM represents an empty zone.
70    pub fn is_empty(&self) -> bool {
71        self.empty
72    }
73
74    /// Gets the bound D[i][j] (clock_i - clock_j <= D[i][j]).
75    pub fn get(&self, i: usize, j: usize) -> f64 {
76        self.data[i * self.dim + j]
77    }
78
79    /// Sets the bound D[i][j].
80    pub fn set(&mut self, i: usize, j: usize, value: f64) {
81        self.data[i * self.dim + j] = value;
82    }
83
84    /// Returns the lower bound for clock i: -D[0][i+1].
85    pub fn lower_bound(&self, i: usize) -> f64 {
86        if self.empty || i >= self.clock_names.len() {
87            return 0.0;
88        }
89        let val = -self.get(0, i + 1);
90        if val == 0.0 { 0.0 } else { val } // normalize -0
91    }
92
93    /// Returns the upper bound for clock i: D[i+1][0].
94    pub fn upper_bound(&self, i: usize) -> f64 {
95        if self.empty || i >= self.clock_names.len() {
96            return f64::INFINITY;
97        }
98        self.get(i + 1, 0)
99    }
100
101    /// Returns true if clock i can fire (lower bound <= epsilon after time passage).
102    pub fn can_fire(&self, i: usize) -> bool {
103        !self.empty && self.lower_bound(i) <= EPSILON
104    }
105
106    /// Canonicalizes the DBM using Floyd-Warshall algorithm.
107    pub fn canonicalize(&mut self) {
108        if self.empty {
109            return;
110        }
111        let n = self.dim;
112        for k in 0..n {
113            for i in 0..n {
114                for j in 0..n {
115                    let ik = self.get(i, k);
116                    let kj = self.get(k, j);
117                    if ik < f64::INFINITY && kj < f64::INFINITY {
118                        let via = ik + kj;
119                        if via < self.get(i, j) {
120                            self.set(i, j, via);
121                        }
122                    }
123                }
124            }
125        }
126        // Check for negative diagonal (empty zone)
127        for i in 0..n {
128            if self.get(i, i) < -EPSILON {
129                self.empty = true;
130                return;
131            }
132        }
133    }
134
135    /// Lets time pass: set all lower bounds to 0 (minimum firing time reached).
136    pub fn let_time_pass(&self) -> Dbm {
137        if self.empty {
138            return self.clone();
139        }
140        let mut result = self.clone();
141        for i in 1..self.dim {
142            result.set(0, i, 0.0);
143        }
144        result.canonicalize();
145        result
146    }
147
148    /// Computes successor firing domain after firing transition at `fired_clock`.
149    ///
150    /// Implements the Berthomieu-Diaz successor formula:
151    /// intersect constraints, canonicalize, substitute/eliminate fired clock,
152    /// add fresh intervals for newly enabled, then final canonicalization.
153    pub fn fire_transition(
154        &self,
155        fired_clock: usize,
156        new_clock_names: &[String],
157        new_lower_bounds: &[f64],
158        new_upper_bounds: &[f64],
159        persistent_clocks: &[usize],
160    ) -> Dbm {
161        if self.empty {
162            return self.clone();
163        }
164
165        let n = self.clock_names.len();
166        if fired_clock >= n {
167            return Dbm::empty_dbm();
168        }
169
170        // Step 1: Intersect with "fired fires first" constraint
171        let mut constrained = self.data.clone();
172        let dim = self.dim;
173        let f = fired_clock + 1;
174
175        for i in 0..n {
176            if i != fired_clock {
177                let idx = i + 1;
178                let pos = f * dim + idx;
179                constrained[pos] = constrained[pos].min(0.0);
180            }
181        }
182
183        // Step 2: Canonicalize
184        if !canonicalize_in_place(&mut constrained, dim) {
185            return Dbm::empty_dbm();
186        }
187
188        // Steps 3 & 4: Build new DBM with persistent + newly enabled clocks
189        let new_n = persistent_clocks.len() + new_clock_names.len();
190        let new_dim = new_n + 1;
191        let mut new_data = vec![f64::INFINITY; new_dim * new_dim];
192        for i in 0..new_dim {
193            new_data[i * new_dim + i] = 0.0;
194        }
195
196        // Copy persistent clocks with transformed bounds
197        for (pi, &old_clock) in persistent_clocks.iter().enumerate() {
198            let old_idx = old_clock + 1;
199            let new_idx = pi + 1;
200
201            let upper = constrained[old_idx * dim + f];
202            let lower = (-constrained[f * dim + old_idx]).max(0.0);
203
204            new_data[new_idx] = -lower; // row 0, col new_idx
205            new_data[new_idx * new_dim] = upper; // row new_idx, col 0
206
207            // Inter-clock constraints between persistent transitions
208            for (pj, &old_j) in persistent_clocks.iter().enumerate() {
209                let old_j_idx = old_j + 1;
210                let new_j = pj + 1;
211                new_data[new_idx * new_dim + new_j] = constrained[old_idx * dim + old_j_idx];
212            }
213        }
214
215        // Step 5: Add fresh intervals for newly enabled transitions
216        let offset = persistent_clocks.len();
217        for (k, _) in new_clock_names.iter().enumerate() {
218            let idx = offset + k + 1;
219            new_data[idx] = -new_lower_bounds[k]; // row 0, col idx
220            new_data[idx * new_dim] = new_upper_bounds[k]; // row idx, col 0
221        }
222
223        // Build new clock names
224        let mut all_names = Vec::with_capacity(new_n);
225        for &idx in persistent_clocks {
226            all_names.push(self.clock_names[idx].clone());
227        }
228        all_names.extend(new_clock_names.iter().cloned());
229
230        let mut result = Dbm {
231            dim: new_dim,
232            data: new_data,
233            clock_names: all_names,
234            empty: false,
235        };
236        result.canonicalize();
237        result
238    }
239
240    /// Generates a canonical string representation for deduplication.
241    pub fn canonical_string(&self) -> String {
242        if self.empty {
243            return "DBM[empty]".to_string();
244        }
245        let mut parts = Vec::new();
246        for i in 0..self.clock_names.len() {
247            let lo = self.lower_bound(i);
248            let hi = self.upper_bound(i);
249            parts.push(format!(
250                "{}:[{},{}]",
251                self.clock_names[i],
252                format_bound(lo),
253                format_bound(hi)
254            ));
255        }
256        format!("DBM{{{}}}", parts.join(", "))
257    }
258}
259
260fn canonicalize_in_place(data: &mut [f64], dim: usize) -> bool {
261    for k in 0..dim {
262        for i in 0..dim {
263            for j in 0..dim {
264                let ik = data[i * dim + k];
265                let kj = data[k * dim + j];
266                if ik < f64::INFINITY && kj < f64::INFINITY {
267                    let via = ik + kj;
268                    if via < data[i * dim + j] {
269                        data[i * dim + j] = via;
270                    }
271                }
272            }
273        }
274    }
275    for i in 0..dim {
276        if data[i * dim + i] < -EPSILON {
277            return false;
278        }
279    }
280    true
281}
282
283fn format_bound(b: f64) -> String {
284    if b >= f64::INFINITY / 2.0 {
285        "\u{221e}".to_string()
286    } else if b == b.trunc() {
287        format!("{}", b as i64)
288    } else {
289        format!("{:.3}", b)
290    }
291}
292
293impl PartialEq for Dbm {
294    fn eq(&self, other: &Self) -> bool {
295        if self.empty && other.empty {
296            return true;
297        }
298        if self.empty || other.empty {
299            return false;
300        }
301        if self.clock_names.len() != other.clock_names.len() {
302            return false;
303        }
304        if self.clock_names != other.clock_names {
305            return false;
306        }
307        if self.data.len() != other.data.len() {
308            return false;
309        }
310        for i in 0..self.data.len() {
311            if (self.data[i] - other.data[i]).abs() > EPSILON {
312                return false;
313            }
314        }
315        true
316    }
317}
318
319impl Eq for Dbm {}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn dbm_new() {
327        let dbm = Dbm::new(3);
328        assert_eq!(dbm.size(), 3);
329        assert_eq!(dbm.get(0, 0), 0.0);
330        assert_eq!(dbm.get(1, 1), 0.0);
331        assert_eq!(dbm.get(0, 1), f64::INFINITY);
332    }
333
334    #[test]
335    fn dbm_create_with_bounds() {
336        let dbm = Dbm::create(vec!["t1".into(), "t2".into()], &[5.0, 3.0], &[10.0, 8.0]);
337        assert_eq!(dbm.clock_names().len(), 2);
338        assert_eq!(dbm.lower_bound(0), 5.0);
339        assert_eq!(dbm.upper_bound(0), 10.0);
340        assert_eq!(dbm.lower_bound(1), 3.0);
341        assert_eq!(dbm.upper_bound(1), 8.0);
342    }
343
344    #[test]
345    fn dbm_bounds() {
346        let mut dbm = Dbm::new(3);
347        dbm.clock_names = vec!["t1".into(), "t2".into()];
348        dbm.set(0, 1, -5.0);
349        dbm.set(1, 0, 10.0);
350        assert_eq!(dbm.lower_bound(0), 5.0);
351        assert_eq!(dbm.upper_bound(0), 10.0);
352        assert!(dbm.can_fire(0) || !dbm.can_fire(0)); // depends on canonicalization
353    }
354
355    #[test]
356    fn dbm_canonicalize() {
357        let mut dbm = Dbm::new(3);
358        dbm.set(1, 0, 10.0);
359        dbm.set(0, 1, -5.0);
360        dbm.set(2, 0, 8.0);
361        dbm.set(0, 2, -3.0);
362        dbm.canonicalize();
363        assert!(dbm.get(1, 2) <= 10.0 + 8.0);
364    }
365
366    #[test]
367    fn dbm_empty() {
368        let mut dbm = Dbm::new(2);
369        dbm.set(0, 1, -10.0);
370        dbm.set(1, 0, 5.0);
371        dbm.canonicalize();
372        assert!(dbm.is_empty());
373    }
374
375    #[test]
376    fn dbm_let_time_pass() {
377        let dbm = Dbm::create(vec!["t1".into()], &[5.0], &[10.0]);
378        let passed = dbm.let_time_pass();
379        // After time passage, lower bound should be 0
380        assert_eq!(passed.lower_bound(0), 0.0);
381        // Upper bound preserved
382        assert_eq!(passed.upper_bound(0), 10.0);
383    }
384
385    #[test]
386    fn dbm_equality() {
387        let a = Dbm::create(vec!["t1".into()], &[5.0], &[10.0]);
388        let b = Dbm::create(vec!["t1".into()], &[5.0], &[10.0]);
389        assert_eq!(a, b);
390    }
391
392    #[test]
393    fn dbm_inequality() {
394        let a = Dbm::create(vec!["t1".into()], &[5.0], &[10.0]);
395        let b = Dbm::create(vec!["t1".into()], &[3.0], &[10.0]);
396        assert_ne!(a, b);
397    }
398
399    #[test]
400    fn dbm_fire_transition() {
401        let dbm = Dbm::create(vec!["t1".into(), "t2".into()], &[5.0, 3.0], &[10.0, 8.0]);
402        let passed = dbm.let_time_pass();
403
404        // Fire t1 (clock 0), t2 persists (clock 1)
405        let result = passed.fire_transition(
406            0,
407            &[],
408            &[],
409            &[],
410            &[1], // t2 is persistent
411        );
412        assert!(!result.is_empty());
413        assert_eq!(result.clock_names().len(), 1);
414        assert_eq!(result.clock_names()[0], "t2");
415    }
416
417    #[test]
418    fn dbm_fire_with_newly_enabled() {
419        let dbm = Dbm::create(vec!["t1".into()], &[0.0], &[f64::INFINITY]);
420        let passed = dbm.let_time_pass();
421
422        // Fire t1, enable t2 and t3
423        let result = passed.fire_transition(
424            0,
425            &["t2".into(), "t3".into()],
426            &[2.0, 0.0],
427            &[5.0, 3.0],
428            &[],
429        );
430        assert!(!result.is_empty());
431        assert_eq!(result.clock_names().len(), 2);
432        assert_eq!(result.clock_names()[0], "t2");
433        assert_eq!(result.clock_names()[1], "t3");
434    }
435
436    #[test]
437    fn dbm_can_fire() {
438        let dbm = Dbm::create(vec!["t1".into()], &[0.0], &[10.0]);
439        let passed = dbm.let_time_pass();
440        assert!(passed.can_fire(0));
441
442        // With high lower bound and no time passage
443        let dbm2 = Dbm::create(vec!["t1".into()], &[5.0], &[10.0]);
444        assert!(!dbm2.can_fire(0)); // hasn't waited long enough
445    }
446
447    #[test]
448    fn dbm_canonical_string() {
449        let dbm = Dbm::create(vec!["t1".into()], &[5.0], &[10.0]);
450        let s = dbm.canonical_string();
451        assert!(s.contains("t1"));
452        assert!(s.contains("5"));
453        assert!(s.contains("10"));
454    }
455
456    #[test]
457    fn dbm_empty_dbm() {
458        let dbm = Dbm::empty_dbm();
459        assert!(dbm.is_empty());
460        assert_eq!(dbm.canonical_string(), "DBM[empty]");
461    }
462
463    #[test]
464    fn dbm_fire_persistent_bounds_adjusted() {
465        // After firing t1 with t2 persistent, t2's bounds should be relative to fire time
466        let dbm = Dbm::create(vec!["t1".into(), "t2".into()], &[0.0, 0.0], &[5.0, 10.0]);
467        let passed = dbm.let_time_pass();
468
469        let result = passed.fire_transition(
470            0, // fire t1
471            &[],
472            &[],
473            &[],
474            &[1], // t2 persists
475        );
476
477        assert!(!result.is_empty());
478        // t2 upper bound should still be finite
479        assert!(result.upper_bound(0) < f64::INFINITY);
480    }
481
482    #[test]
483    fn dbm_fire_replaces_fired_clock() {
484        let dbm = Dbm::create(vec!["t1".into()], &[0.0], &[5.0]);
485        let passed = dbm.let_time_pass();
486
487        // Fire t1, enable t2 with different bounds
488        let result = passed.fire_transition(0, &["t2".into()], &[1.0], &[3.0], &[]);
489
490        assert_eq!(result.clock_names().len(), 1);
491        assert_eq!(result.clock_names()[0], "t2");
492        assert_eq!(result.lower_bound(0), 1.0); // fresh interval lower bound
493        assert_eq!(result.upper_bound(0), 3.0);
494
495        // After let_time_pass, lower bound drops to 0
496        let passed = result.let_time_pass();
497        assert_eq!(passed.lower_bound(0), 0.0);
498    }
499
500    #[test]
501    fn dbm_infeasible_constraints_produce_empty() {
502        // lower > upper should produce empty DBM
503        let dbm = Dbm::create(
504            vec!["t1".into()],
505            &[10.0],
506            &[5.0], // upper < lower
507        );
508        assert!(dbm.is_empty());
509    }
510
511    #[test]
512    fn dbm_multiple_clocks_inter_constraints() {
513        // Two clocks with tight constraints
514        let dbm = Dbm::create(vec!["t1".into(), "t2".into()], &[1.0, 2.0], &[3.0, 4.0]);
515        assert!(!dbm.is_empty());
516
517        // After canonicalization, inter-clock constraints should be tightened
518        // t1 - t2 <= upper_t1 - lower_t2 = 3 - 2 = 1
519        assert!(dbm.get(1, 2) <= 1.0 + 1e-9);
520    }
521
522    #[test]
523    fn dbm_lower_bound_out_of_range() {
524        let dbm = Dbm::create(vec!["t1".into()], &[5.0], &[10.0]);
525        // Out of range index
526        assert_eq!(dbm.lower_bound(10), 0.0);
527        assert_eq!(dbm.upper_bound(10), f64::INFINITY);
528    }
529
530    #[test]
531    fn dbm_equality_with_different_names() {
532        let a = Dbm::create(vec!["t1".into()], &[5.0], &[10.0]);
533        let b = Dbm::create(vec!["t2".into()], &[5.0], &[10.0]);
534        // Different names → not equal
535        assert_ne!(a, b);
536    }
537
538    #[test]
539    fn dbm_empty_equality() {
540        let a = Dbm::empty_dbm();
541        let b = Dbm::empty_dbm();
542        assert_eq!(a, b);
543    }
544
545    #[test]
546    fn dbm_let_time_pass_preserves_upper() {
547        let dbm = Dbm::create(vec!["t1".into(), "t2".into()], &[3.0, 5.0], &[10.0, 15.0]);
548        let passed = dbm.let_time_pass();
549        assert_eq!(passed.lower_bound(0), 0.0);
550        assert_eq!(passed.lower_bound(1), 0.0);
551        assert_eq!(passed.upper_bound(0), 10.0);
552        assert_eq!(passed.upper_bound(1), 15.0);
553    }
554}