Skip to main content

libpetri_runtime/
compiled_net.rs

1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3
4use libpetri_core::input::{In, required_count};
5use libpetri_core::petri_net::PetriNet;
6use libpetri_core::place::PlaceRef;
7use libpetri_core::transition::Transition;
8
9use crate::bitmap;
10
11/// Cardinality check for transitions with non-One inputs.
12#[derive(Debug, Clone)]
13pub struct CardinalityCheck {
14    pub place_ids: Vec<usize>,
15    pub required_counts: Vec<usize>,
16}
17
18/// Integer-indexed, precomputed representation of a PetriNet for bitmap-based execution.
19///
20/// Uses u64 bitmasks (64 bits per word) for O(W) enablement checks
21/// where W = ceil(place_count / 64).
22#[derive(Debug, Clone)]
23pub struct CompiledNet {
24    net: PetriNet,
25    pub(crate) place_count: usize,
26    pub(crate) transition_count: usize,
27    pub(crate) word_count: usize,
28
29    // ID mappings
30    places_by_id: Vec<PlaceRef>,
31    transitions_by_id: Vec<usize>, // index into net.transitions()
32    place_index: HashMap<Arc<str>, usize>,
33    #[allow(dead_code)]
34    transition_index: HashMap<usize, usize>, // net transition index -> compiled id
35
36    // Precomputed masks per transition (flat arrays: [tid * word_count .. (tid+1) * word_count])
37    needs_masks: Vec<u64>,
38    inhibitor_masks: Vec<u64>,
39
40    // Reverse index: place_id -> affected transition IDs
41    place_to_transitions: Vec<Vec<usize>>,
42
43    // Consumption place IDs per transition (input + reset places)
44    consumption_place_ids: Vec<Vec<usize>>,
45
46    // Cardinality and guard flags
47    cardinality_checks: Vec<Option<CardinalityCheck>>,
48    has_guards: Vec<bool>,
49}
50
51impl CompiledNet {
52    /// Compiles a PetriNet into an optimized bitmap representation.
53    pub fn compile(net: &PetriNet) -> Self {
54        // Collect all places (sorted for stable ordering)
55        let mut place_names: Vec<Arc<str>> = net
56            .places()
57            .iter()
58            .map(|p| Arc::clone(p.name_arc()))
59            .collect();
60        place_names.sort();
61        place_names.dedup();
62
63        let place_count = place_names.len();
64        let word_count = bitmap::word_count(place_count);
65
66        // Assign place IDs
67        let mut place_index = HashMap::new();
68        let mut places_by_id = Vec::with_capacity(place_count);
69        for (i, name) in place_names.iter().enumerate() {
70            place_index.insert(Arc::clone(name), i);
71            places_by_id.push(PlaceRef::new(Arc::clone(name)));
72        }
73
74        let transition_count = net.transitions().len();
75        let mut transitions_by_id = Vec::with_capacity(transition_count);
76        let mut transition_index = HashMap::new();
77
78        let mut needs_masks = vec![0u64; transition_count * word_count];
79        let mut inhibitor_masks = vec![0u64; transition_count * word_count];
80        let mut consumption_place_ids = Vec::with_capacity(transition_count);
81        let mut cardinality_checks: Vec<Option<CardinalityCheck>> = vec![None; transition_count];
82        let mut has_guards = vec![false; transition_count];
83
84        let mut place_to_transitions_tmp: Vec<HashSet<usize>> = vec![HashSet::new(); place_count];
85
86        for (net_idx, t) in net.transitions().iter().enumerate() {
87            let tid = net_idx;
88            transitions_by_id.push(net_idx);
89            transition_index.insert(net_idx, tid);
90
91            let mask_base = tid * word_count;
92            let mut needs_cardinality = false;
93
94            // Input specs
95            for in_spec in t.input_specs() {
96                let pid = place_index[in_spec.place().name_arc()];
97                if word_count > 0 {
98                    bitmap::set_bit(&mut needs_masks[mask_base..mask_base + word_count], pid);
99                }
100                place_to_transitions_tmp[pid].insert(tid);
101
102                if !matches!(in_spec, In::One { .. }) {
103                    needs_cardinality = true;
104                }
105                if in_spec.has_guard() {
106                    has_guards[tid] = true;
107                }
108            }
109
110            // Build cardinality check if needed
111            if needs_cardinality {
112                let mut pids = Vec::new();
113                let mut reqs = Vec::new();
114                for in_spec in t.input_specs() {
115                    pids.push(place_index[in_spec.place().name_arc()]);
116                    reqs.push(required_count(in_spec));
117                }
118                cardinality_checks[tid] = Some(CardinalityCheck {
119                    place_ids: pids,
120                    required_counts: reqs,
121                });
122            }
123
124            // Read arcs
125            for r in t.reads() {
126                let pid = place_index[r.place.name_arc()];
127                if word_count > 0 {
128                    bitmap::set_bit(&mut needs_masks[mask_base..mask_base + word_count], pid);
129                }
130                place_to_transitions_tmp[pid].insert(tid);
131            }
132
133            // Inhibitor arcs
134            for inh in t.inhibitors() {
135                let pid = place_index[inh.place.name_arc()];
136                if word_count > 0 {
137                    bitmap::set_bit(&mut inhibitor_masks[mask_base..mask_base + word_count], pid);
138                }
139                place_to_transitions_tmp[pid].insert(tid);
140            }
141
142            // Reset arcs
143            for r in t.resets() {
144                let pid = place_index[r.place.name_arc()];
145                place_to_transitions_tmp[pid].insert(tid);
146            }
147
148            // Consumption place IDs (input + reset, deduplicated)
149            let mut consumption_set = HashSet::new();
150            for spec in t.input_specs() {
151                consumption_set.insert(place_index[spec.place().name_arc()]);
152            }
153            for r in t.resets() {
154                consumption_set.insert(place_index[r.place.name_arc()]);
155            }
156            consumption_place_ids.push(consumption_set.into_iter().collect());
157        }
158
159        let place_to_transitions: Vec<Vec<usize>> = place_to_transitions_tmp
160            .into_iter()
161            .map(|s| s.into_iter().collect())
162            .collect();
163
164        CompiledNet {
165            net: net.clone(),
166            place_count,
167            transition_count,
168            word_count,
169            places_by_id,
170            transitions_by_id,
171            place_index,
172            transition_index,
173            needs_masks,
174            inhibitor_masks,
175            place_to_transitions,
176            consumption_place_ids,
177            cardinality_checks,
178            has_guards,
179        }
180    }
181
182    // ==================== Accessors ====================
183
184    /// Returns the underlying PetriNet.
185    pub fn net(&self) -> &PetriNet {
186        &self.net
187    }
188
189    /// Returns the place at the given ID.
190    pub fn place(&self, pid: usize) -> &PlaceRef {
191        &self.places_by_id[pid]
192    }
193
194    /// Returns the transition at the given compiled ID.
195    pub fn transition(&self, tid: usize) -> &Transition {
196        &self.net.transitions()[self.transitions_by_id[tid]]
197    }
198
199    /// Returns the place ID for a given place name.
200    pub fn place_id(&self, name: &str) -> Option<usize> {
201        self.place_index.get(name).copied()
202    }
203
204    /// Returns the place ID, panicking if not found.
205    pub fn place_id_or_panic(&self, name: &str) -> usize {
206        self.place_index
207            .get(name)
208            .copied()
209            .unwrap_or_else(|| panic!("Unknown place: {name}"))
210    }
211
212    /// Returns affected transition IDs for a place.
213    pub fn affected_transitions(&self, pid: usize) -> &[usize] {
214        &self.place_to_transitions[pid]
215    }
216
217    /// Returns consumption place IDs for a transition.
218    pub fn consumption_place_ids(&self, tid: usize) -> &[usize] {
219        &self.consumption_place_ids[tid]
220    }
221
222    /// Returns the cardinality check for a transition, if any.
223    pub fn cardinality_check(&self, tid: usize) -> Option<&CardinalityCheck> {
224        self.cardinality_checks[tid].as_ref()
225    }
226
227    /// Returns whether a transition has guard predicates.
228    pub fn has_guards(&self, tid: usize) -> bool {
229        self.has_guards[tid]
230    }
231
232    /// Returns the needs mask slice for a transition.
233    pub fn needs_mask(&self, tid: usize) -> &[u64] {
234        let base = tid * self.word_count;
235        &self.needs_masks[base..base + self.word_count]
236    }
237
238    /// Returns the inhibitor mask slice for a transition.
239    pub fn inhibitor_mask(&self, tid: usize) -> &[u64] {
240        let base = tid * self.word_count;
241        &self.inhibitor_masks[base..base + self.word_count]
242    }
243
244    // ==================== Enablement Check ====================
245
246    /// Two-phase bitmap enablement check for a transition:
247    /// 1. Presence check: verifies all required places have tokens
248    /// 2. Inhibitor check: verifies no inhibitor places have tokens
249    ///
250    /// This is a necessary but not sufficient condition — cardinality and guard checks
251    /// are performed separately by the executor for transitions that pass this fast path.
252    pub fn can_enable_bitmap(&self, tid: usize, marking_snapshot: &[u64]) -> bool {
253        let needs = self.needs_mask(tid);
254        let inhibitors = self.inhibitor_mask(tid);
255
256        bitmap::contains_all(marking_snapshot, needs)
257            && !bitmap::intersects(marking_snapshot, inhibitors)
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use libpetri_core::input::one;
265    use libpetri_core::output::out_place;
266    use libpetri_core::place::Place;
267
268    fn simple_chain_net() -> PetriNet {
269        let p1 = Place::<i32>::new("p1");
270        let p2 = Place::<i32>::new("p2");
271        let p3 = Place::<i32>::new("p3");
272
273        let t1 = Transition::builder("t1")
274            .input(one(&p1))
275            .output(out_place(&p2))
276            .build();
277        let t2 = Transition::builder("t2")
278            .input(one(&p2))
279            .output(out_place(&p3))
280            .build();
281
282        PetriNet::builder("chain").transitions([t1, t2]).build()
283    }
284
285    #[test]
286    fn compile_basic() {
287        let net = simple_chain_net();
288        let compiled = CompiledNet::compile(&net);
289
290        assert_eq!(compiled.place_count, 3);
291        assert_eq!(compiled.transition_count, 2);
292        assert!(compiled.word_count >= 1);
293    }
294
295    #[test]
296    fn place_id_lookup() {
297        let net = simple_chain_net();
298        let compiled = CompiledNet::compile(&net);
299
300        assert!(compiled.place_id("p1").is_some());
301        assert!(compiled.place_id("p2").is_some());
302        assert!(compiled.place_id("p3").is_some());
303        assert!(compiled.place_id("nonexistent").is_none());
304    }
305
306    #[test]
307    fn bitmap_enablement() {
308        let net = simple_chain_net();
309        let compiled = CompiledNet::compile(&net);
310
311        let mut snapshot = vec![0u64; compiled.word_count];
312
313        // Nothing marked — neither transition enabled
314        assert!(!compiled.can_enable_bitmap(0, &snapshot));
315        assert!(!compiled.can_enable_bitmap(1, &snapshot));
316
317        // Mark p1 — t1 should be enabled
318        let p1_id = compiled.place_id("p1").unwrap();
319        bitmap::set_bit(&mut snapshot, p1_id);
320        assert!(compiled.can_enable_bitmap(0, &snapshot));
321        assert!(!compiled.can_enable_bitmap(1, &snapshot));
322
323        // Mark p2 — t2 should also be enabled
324        let p2_id = compiled.place_id("p2").unwrap();
325        bitmap::set_bit(&mut snapshot, p2_id);
326        assert!(compiled.can_enable_bitmap(0, &snapshot));
327        assert!(compiled.can_enable_bitmap(1, &snapshot));
328    }
329
330    #[test]
331    fn inhibitor_blocks_transition() {
332        let p1 = Place::<i32>::new("p1");
333        let p2 = Place::<i32>::new("p2");
334        let p_inh = Place::<i32>::new("inh");
335
336        let t = Transition::builder("t1")
337            .input(one(&p1))
338            .output(out_place(&p2))
339            .inhibitor(libpetri_core::arc::inhibitor(&p_inh))
340            .build();
341
342        let net = PetriNet::builder("test").transition(t).build();
343        let compiled = CompiledNet::compile(&net);
344
345        let mut snapshot = vec![0u64; compiled.word_count];
346        let p1_id = compiled.place_id("p1").unwrap();
347        let inh_id = compiled.place_id("inh").unwrap();
348
349        bitmap::set_bit(&mut snapshot, p1_id);
350        assert!(compiled.can_enable_bitmap(0, &snapshot));
351
352        // Adding inhibitor token disables
353        bitmap::set_bit(&mut snapshot, inh_id);
354        assert!(!compiled.can_enable_bitmap(0, &snapshot));
355    }
356
357    #[test]
358    fn reverse_index() {
359        let net = simple_chain_net();
360        let compiled = CompiledNet::compile(&net);
361
362        // p2 is output of t1 and input of t2, so both should be affected
363        let p2_id = compiled.place_id("p2").unwrap();
364        let affected = compiled.affected_transitions(p2_id);
365        // t2 reads from p2
366        assert!(affected.contains(&1) || affected.contains(&0));
367    }
368}