Skip to main content

libpetri_verification/
structural_check.rs

1use crate::net_flattener::FlatNet;
2
3/// Result of structural deadlock pre-check.
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum StructuralCheckResult {
6    /// No potential deadlock based on siphon/trap analysis.
7    NoPotentialDeadlock,
8    /// Potential deadlock detected.
9    PotentialDeadlock,
10    /// Structural analysis is inconclusive.
11    Inconclusive,
12}
13
14/// Structural deadlock pre-check using Commoner's theorem (siphon/trap analysis).
15///
16/// For nets with <= 50 places, finds minimal siphons and checks if each contains a trap.
17pub fn structural_check(flat: &FlatNet) -> StructuralCheckResult {
18    if flat.place_count > 50 {
19        return StructuralCheckResult::Inconclusive;
20    }
21
22    let siphons = find_minimal_siphons(flat);
23    for siphon in &siphons {
24        let trap = find_maximal_trap_in(flat, siphon);
25        if trap.is_empty() {
26            return StructuralCheckResult::PotentialDeadlock;
27        }
28    }
29
30    StructuralCheckResult::NoPotentialDeadlock
31}
32
33/// Finds minimal siphons using fixed-point expansion.
34fn find_minimal_siphons(flat: &FlatNet) -> Vec<Vec<usize>> {
35    let mut siphons = Vec::new();
36
37    // For each place, try to build a minimal siphon containing it
38    for start_pid in 0..flat.place_count {
39        let mut siphon = vec![false; flat.place_count];
40        siphon[start_pid] = true;
41
42        // Fixed-point expansion: add all places needed to close the siphon
43        let mut changed = true;
44        while changed {
45            changed = false;
46            for ft in &flat.transitions {
47                // Check if this transition outputs to any siphon place
48                let outputs_to_siphon = ft
49                    .post
50                    .iter()
51                    .enumerate()
52                    .any(|(pid, &count)| count > 0 && siphon[pid]);
53
54                if outputs_to_siphon {
55                    // All input places must be in the siphon
56                    for (pid, &count) in ft.pre.iter().enumerate() {
57                        if count > 0 && !siphon[pid] {
58                            siphon[pid] = true;
59                            changed = true;
60                        }
61                    }
62                }
63            }
64        }
65
66        let siphon_places: Vec<usize> = siphon
67            .iter()
68            .enumerate()
69            .filter(|(_, in_s)| **in_s)
70            .map(|(i, _)| i)
71            .collect();
72
73        // Check if it's truly minimal (not a superset of existing siphon)
74        let is_superset = siphons
75            .iter()
76            .any(|existing: &Vec<usize>| existing.iter().all(|p| siphon_places.contains(p)));
77
78        if !is_superset {
79            siphons.push(siphon_places);
80        }
81    }
82
83    siphons
84}
85
86/// Finds the maximal trap contained within a siphon using fixed-point contraction.
87fn find_maximal_trap_in(flat: &FlatNet, siphon: &[usize]) -> Vec<usize> {
88    let mut trap = vec![false; flat.place_count];
89    for &pid in siphon {
90        trap[pid] = true;
91    }
92
93    // Fixed-point contraction: remove places that don't satisfy trap property
94    let mut changed = true;
95    while changed {
96        changed = false;
97        for &pid in siphon {
98            if !trap[pid] {
99                continue;
100            }
101
102            // For a trap: every transition that consumes from pid must also produce into the trap
103            let mut satisfies = true;
104            for ft in &flat.transitions {
105                if ft.pre[pid] > 0 {
106                    // This transition consumes from pid
107                    let produces_to_trap = ft
108                        .post
109                        .iter()
110                        .enumerate()
111                        .any(|(p, &count)| count > 0 && trap[p]);
112
113                    if !produces_to_trap {
114                        satisfies = false;
115                        break;
116                    }
117                }
118            }
119
120            if !satisfies {
121                trap[pid] = false;
122                changed = true;
123            }
124        }
125    }
126
127    trap.iter()
128        .enumerate()
129        .filter(|(_, in_t)| **in_t)
130        .map(|(i, _)| i)
131        .collect()
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::net_flattener::flatten;
138    use libpetri_core::input::one;
139    use libpetri_core::output::out_place;
140    use libpetri_core::petri_net::PetriNet;
141    use libpetri_core::place::Place;
142    use libpetri_core::transition::Transition;
143
144    #[test]
145    fn simple_chain_potential_deadlock() {
146        // A simple chain p1->t1->p2 IS a potential deadlock
147        // because p2 is a terminal siphon with no trap
148        let p1 = Place::<i32>::new("p1");
149        let p2 = Place::<i32>::new("p2");
150        let t = Transition::builder("t1")
151            .input(one(&p1))
152            .output(out_place(&p2))
153            .build();
154        let net = PetriNet::builder("test").transition(t).build();
155
156        let flat = flatten(&net);
157        let result = structural_check(&flat);
158        assert_eq!(result, StructuralCheckResult::PotentialDeadlock);
159    }
160
161    #[test]
162    fn cycle_no_deadlock() {
163        let p1 = Place::<i32>::new("p1");
164        let p2 = Place::<i32>::new("p2");
165        let t1 = Transition::builder("t1")
166            .input(one(&p1))
167            .output(out_place(&p2))
168            .build();
169        let t2 = Transition::builder("t2")
170            .input(one(&p2))
171            .output(out_place(&p1))
172            .build();
173        let net = PetriNet::builder("cycle").transitions([t1, t2]).build();
174
175        let flat = flatten(&net);
176        let result = structural_check(&flat);
177        // Cycle forms a siphon that contains its own trap
178        assert_eq!(result, StructuralCheckResult::NoPotentialDeadlock);
179    }
180
181    #[test]
182    fn potential_deadlock_detected() {
183        // Two transitions competing for shared resource without cycle
184        let p1 = Place::<i32>::new("p1");
185        let p2 = Place::<i32>::new("p2");
186        let shared = Place::<i32>::new("shared");
187        let out1 = Place::<i32>::new("out1");
188        let out2 = Place::<i32>::new("out2");
189
190        let t1 = Transition::builder("t1")
191            .input(one(&p1))
192            .input(one(&shared))
193            .output(out_place(&out1))
194            .build();
195        let t2 = Transition::builder("t2")
196            .input(one(&p2))
197            .input(one(&shared))
198            .output(out_place(&out2))
199            .build();
200
201        let net = PetriNet::builder("deadlock").transitions([t1, t2]).build();
202
203        let flat = flatten(&net);
204        let result = structural_check(&flat);
205        // The shared place is a siphon without a trap (no transition produces back into it)
206        assert_eq!(result, StructuralCheckResult::PotentialDeadlock);
207    }
208}