Skip to main content

oxiphysics_core/
petri_nets.rs

1#![allow(clippy::needless_range_loop)]
2// Copyright 2026 COOLJAPAN OU (Team KitaSan)
3// SPDX-License-Identifier: Apache-2.0
4
5//! Petri net modeling for concurrent discrete event systems.
6//!
7//! This module provides classical Petri nets, stochastic Petri nets,
8//! colored Petri nets, and supporting analysis algorithms such as
9//! reachability BFS and coverability trees.
10
11#![allow(dead_code)]
12
13use std::collections::{HashMap, HashSet, VecDeque};
14
15// ---------------------------------------------------------------------------
16// Place
17// ---------------------------------------------------------------------------
18
19/// A place in a Petri net, holding a non-negative token count.
20#[derive(Debug, Clone)]
21pub struct Place {
22    /// Human-readable name for the place.
23    pub name: String,
24    /// Current number of tokens in this place.
25    pub tokens: usize,
26}
27
28impl Place {
29    /// Create a new place with zero tokens.
30    pub fn new(name: impl Into<String>) -> Self {
31        Self {
32            name: name.into(),
33            tokens: 0,
34        }
35    }
36
37    /// Create a new place with `tokens` initial tokens.
38    pub fn with_tokens(name: impl Into<String>, tokens: usize) -> Self {
39        Self {
40            name: name.into(),
41            tokens,
42        }
43    }
44}
45
46// ---------------------------------------------------------------------------
47// Transition
48// ---------------------------------------------------------------------------
49
50/// A transition in a Petri net, optionally guarded by a predicate over the
51/// token counts of the places it consumes from.
52#[derive(Clone)]
53pub struct Transition {
54    /// Human-readable name for the transition.
55    pub name: String,
56    /// Optional guard function: receives current token counts for every input
57    /// arc (in place-index order) and returns `true` iff the transition is
58    /// allowed to fire.
59    pub guard: Option<fn(&[usize]) -> bool>,
60}
61
62impl std::fmt::Debug for Transition {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        f.debug_struct("Transition")
65            .field("name", &self.name)
66            .field("guard", &self.guard.map(|_| "<fn>"))
67            .finish()
68    }
69}
70
71impl Transition {
72    /// Create a new unguarded transition.
73    pub fn new(name: impl Into<String>) -> Self {
74        Self {
75            name: name.into(),
76            guard: None,
77        }
78    }
79
80    /// Create a new guarded transition.
81    pub fn with_guard(name: impl Into<String>, guard: fn(&[usize]) -> bool) -> Self {
82        Self {
83            name: name.into(),
84            guard: Some(guard),
85        }
86    }
87}
88
89// ---------------------------------------------------------------------------
90// Arc weight type alias
91// ---------------------------------------------------------------------------
92
93/// Weight on an arc (number of tokens consumed / produced per firing).
94pub type Weight = usize;
95
96// ---------------------------------------------------------------------------
97// PetriNet
98// ---------------------------------------------------------------------------
99
100/// A classical (Place/Transition) Petri net.
101///
102/// Arcs are stored as `(transition_idx, place_idx, weight)` triples.
103#[derive(Debug, Clone)]
104pub struct PetriNet {
105    /// All places in this net.
106    pub places: Vec<Place>,
107    /// All transitions in this net.
108    pub transitions: Vec<Transition>,
109    /// Input arcs: `(transition, place, weight)` – tokens consumed on fire.
110    pub arcs_in: Vec<(usize, usize, Weight)>,
111    /// Output arcs: `(transition, place, weight)` – tokens produced on fire.
112    pub arcs_out: Vec<(usize, usize, Weight)>,
113}
114
115impl PetriNet {
116    /// Create an empty Petri net.
117    pub fn new() -> Self {
118        Self {
119            places: Vec::new(),
120            transitions: Vec::new(),
121            arcs_in: Vec::new(),
122            arcs_out: Vec::new(),
123        }
124    }
125
126    /// Add a place and return its index.
127    pub fn add_place(&mut self, place: Place) -> usize {
128        let idx = self.places.len();
129        self.places.push(place);
130        idx
131    }
132
133    /// Add a transition and return its index.
134    pub fn add_transition(&mut self, transition: Transition) -> usize {
135        let idx = self.transitions.len();
136        self.transitions.push(transition);
137        idx
138    }
139
140    /// Add an input arc: transition `t` consumes `weight` tokens from place `p`.
141    pub fn add_arc_in(&mut self, t: usize, p: usize, weight: Weight) {
142        self.arcs_in.push((t, p, weight));
143    }
144
145    /// Add an output arc: transition `t` produces `weight` tokens into place `p`.
146    pub fn add_arc_out(&mut self, t: usize, p: usize, weight: Weight) {
147        self.arcs_out.push((t, p, weight));
148    }
149
150    /// Return the current marking (token counts for every place).
151    pub fn marking(&self) -> Vec<usize> {
152        self.places.iter().map(|p| p.tokens).collect()
153    }
154
155    /// Apply a marking vector to the net (sets token counts).
156    pub fn set_marking(&mut self, marking: &[usize]) {
157        for (p, &tok) in self.places.iter_mut().zip(marking.iter()) {
158            p.tokens = tok;
159        }
160    }
161
162    /// Return indices of all currently enabled transitions.
163    ///
164    /// A transition is enabled when every input arc can be satisfied and the
165    /// guard (if any) returns `true`.
166    pub fn enabled_transitions(&self) -> Vec<usize> {
167        let mut enabled = Vec::new();
168        for (t_idx, transition) in self.transitions.iter().enumerate() {
169            if self.is_enabled(t_idx, transition) {
170                enabled.push(t_idx);
171            }
172        }
173        enabled
174    }
175
176    fn is_enabled(&self, t_idx: usize, transition: &Transition) -> bool {
177        // Collect input token counts for this transition (for guard evaluation).
178        let mut input_tokens: Vec<usize> = Vec::new();
179        for &(t, p, w) in &self.arcs_in {
180            if t == t_idx {
181                if self.places[p].tokens < w {
182                    return false;
183                }
184                input_tokens.push(self.places[p].tokens);
185            }
186        }
187        if let Some(guard) = transition.guard
188            && !guard(&input_tokens)
189        {
190            return false;
191        }
192        true
193    }
194
195    /// Fire transition `t_idx`, updating token counts.
196    ///
197    /// Returns `Ok(())` on success or `Err(String)` if the transition is not
198    /// enabled.
199    pub fn fire(&mut self, t_idx: usize) -> Result<(), String> {
200        if t_idx >= self.transitions.len() {
201            return Err(format!("transition index {} out of range", t_idx));
202        }
203        let transition = self.transitions[t_idx].clone();
204        if !self.is_enabled(t_idx, &transition) {
205            return Err(format!(
206                "transition {} is not enabled",
207                self.transitions[t_idx].name
208            ));
209        }
210        // Consume tokens.
211        for &(t, p, w) in &self.arcs_in {
212            if t == t_idx {
213                self.places[p].tokens -= w;
214            }
215        }
216        // Produce tokens.
217        for &(t, p, w) in &self.arcs_out {
218            if t == t_idx {
219                self.places[p].tokens += w;
220            }
221        }
222        Ok(())
223    }
224
225    /// Returns `true` if no transition is currently enabled (deadlock).
226    pub fn is_deadlocked(&self) -> bool {
227        self.enabled_transitions().is_empty()
228    }
229
230    /// Sum of all tokens in the net (useful for conservation checks).
231    pub fn marking_sum(&self) -> usize {
232        self.places.iter().map(|p| p.tokens).sum()
233    }
234
235    /// BFS over reachable markings from the current marking.
236    ///
237    /// Returns the set of all reachable markings (including the initial one).
238    pub fn reachability_bfs(&self) -> HashSet<Vec<usize>> {
239        let mut visited: HashSet<Vec<usize>> = HashSet::new();
240        let mut queue: VecDeque<Vec<usize>> = VecDeque::new();
241        let initial = self.marking();
242        queue.push_back(initial.clone());
243        visited.insert(initial);
244
245        // Clone the net for simulation.
246        let mut sim = self.clone();
247
248        while let Some(marking) = queue.pop_front() {
249            sim.set_marking(&marking);
250            for t_idx in sim.enabled_transitions() {
251                let mut next_sim = sim.clone();
252                let _ = next_sim.fire(t_idx);
253                let next_marking = next_sim.marking();
254                if !visited.contains(&next_marking) {
255                    visited.insert(next_marking.clone());
256                    queue.push_back(next_marking);
257                }
258            }
259        }
260        visited
261    }
262}
263
264impl Default for PetriNet {
265    fn default() -> Self {
266        Self::new()
267    }
268}
269
270// ---------------------------------------------------------------------------
271// CoverabilityTree
272// ---------------------------------------------------------------------------
273
274/// Omega sentinel value used in coverability analysis (represents unbounded
275/// token counts).
276pub const OMEGA: usize = usize::MAX;
277
278/// A node in the coverability tree.
279#[derive(Debug, Clone)]
280pub struct CoverabilityNode {
281    /// Marking at this node (some entries may be `OMEGA`).
282    pub marking: Vec<usize>,
283    /// Index of the parent node (None for root).
284    pub parent: Option<usize>,
285    /// Transition fired to reach this node.
286    pub fired_transition: Option<usize>,
287    /// Indices of child nodes.
288    pub children: Vec<usize>,
289}
290
291/// Coverability tree for a Petri net.
292///
293/// Can determine whether the net is bounded (no `OMEGA` entries exist).
294#[derive(Debug)]
295pub struct CoverabilityTree {
296    /// All nodes in the tree.
297    pub nodes: Vec<CoverabilityNode>,
298}
299
300impl CoverabilityTree {
301    /// Build the coverability tree for `net` starting from its current marking.
302    pub fn build(net: &PetriNet) -> Self {
303        let mut tree = CoverabilityTree { nodes: Vec::new() };
304        let root_marking = net.marking();
305        let root = CoverabilityNode {
306            marking: root_marking,
307            parent: None,
308            fired_transition: None,
309            children: Vec::new(),
310        };
311        tree.nodes.push(root);
312
313        // Iterative BFS construction.
314        let mut frontier: VecDeque<usize> = VecDeque::new();
315        frontier.push_back(0);
316
317        let mut sim = net.clone();
318
319        while let Some(node_idx) = frontier.pop_front() {
320            let current_marking = tree.nodes[node_idx].marking.clone();
321            sim.set_marking(&current_marking);
322
323            for t_idx in sim.enabled_transitions() {
324                let mut next_sim = sim.clone();
325                // Replace OMEGA entries with a large finite value for simulation.
326                let tmp_marking: Vec<usize> = current_marking
327                    .iter()
328                    .map(|&x| if x == OMEGA { 1_000_000 } else { x })
329                    .collect();
330                next_sim.set_marking(&tmp_marking);
331                if next_sim.fire(t_idx).is_err() {
332                    continue;
333                }
334                let mut new_marking = next_sim.marking();
335
336                // Walk ancestors to find coverability witnesses.
337                let mut anc_idx = Some(node_idx);
338                while let Some(ai) = anc_idx {
339                    let anc_marking = &tree.nodes[ai].marking;
340                    if anc_marking
341                        .iter()
342                        .zip(new_marking.iter())
343                        .all(|(&a, &n)| a == OMEGA || a <= n)
344                        && anc_marking
345                            .iter()
346                            .zip(new_marking.iter())
347                            .any(|(&a, &n)| a != OMEGA && a < n)
348                    {
349                        // Strictly covered – promote to OMEGA.
350                        for (m, a) in new_marking.iter_mut().zip(anc_marking.iter()) {
351                            if *a != OMEGA && *a < *m {
352                                *m = OMEGA;
353                            }
354                        }
355                    }
356                    anc_idx = tree.nodes[ai].parent;
357                }
358
359                // Check for duplicate markings (to terminate).
360                let already_visited = tree.nodes.iter().any(|n| n.marking == new_marking);
361                let child_idx = tree.nodes.len();
362                tree.nodes.push(CoverabilityNode {
363                    marking: new_marking,
364                    parent: Some(node_idx),
365                    fired_transition: Some(t_idx),
366                    children: Vec::new(),
367                });
368                tree.nodes[node_idx].children.push(child_idx);
369
370                if !already_visited {
371                    frontier.push_back(child_idx);
372                }
373            }
374        }
375
376        tree
377    }
378
379    /// Returns `true` if the net is bounded (no `OMEGA` tokens in the tree).
380    pub fn is_bounded(&self) -> bool {
381        self.nodes
382            .iter()
383            .all(|n| n.marking.iter().all(|&t| t != OMEGA))
384    }
385
386    /// Returns the total number of nodes in the tree.
387    pub fn node_count(&self) -> usize {
388        self.nodes.len()
389    }
390}
391
392// ---------------------------------------------------------------------------
393// StochasticPetriNet
394// ---------------------------------------------------------------------------
395
396/// A stochastic Petri net where each transition fires with a rate drawn from
397/// an exponential distribution.
398#[derive(Debug, Clone)]
399pub struct StochasticPetriNet {
400    /// Underlying classical Petri net.
401    pub net: PetriNet,
402    /// Firing rates for each transition (exponential distribution parameter λ).
403    pub rates: Vec<f64>,
404}
405
406impl StochasticPetriNet {
407    /// Create a stochastic Petri net from an existing `PetriNet` and a rate
408    /// vector (one rate per transition, in order).
409    pub fn new(net: PetriNet, rates: Vec<f64>) -> Self {
410        assert_eq!(
411            net.transitions.len(),
412            rates.len(),
413            "one rate per transition required"
414        );
415        Self { net, rates }
416    }
417
418    /// Simulate until simulated time reaches `t_end`.
419    ///
420    /// Returns a vector of `(time, fired_transition_idx)` events.
421    pub fn simulate_until(&mut self, t_end: f64) -> Vec<(f64, usize)> {
422        let mut events = Vec::new();
423        let mut t = 0.0_f64;
424        use rand::RngExt as _;
425        let mut rng = rand::rng();
426
427        loop {
428            let enabled = self.net.enabled_transitions();
429            if enabled.is_empty() {
430                break;
431            }
432            // Total rate of all enabled transitions.
433            let total_rate: f64 = enabled.iter().map(|&i| self.rates[i]).sum();
434            if total_rate == 0.0 {
435                break;
436            }
437            // Sample time to next event (exponential distribution).
438            let u: f64 = rng.random_range(0.0_f64..1.0_f64);
439            let dt = -u.ln() / total_rate;
440            t += dt;
441            if t > t_end {
442                break;
443            }
444            // Choose which transition fires (proportional to rates).
445            let selector: f64 = rng.random_range(0.0_f64..total_rate);
446            let mut cumulative = 0.0;
447            let mut chosen = enabled[0];
448            for &ti in &enabled {
449                cumulative += self.rates[ti];
450                if selector < cumulative {
451                    chosen = ti;
452                    break;
453                }
454            }
455            let _ = self.net.fire(chosen);
456            events.push((t, chosen));
457        }
458        events
459    }
460
461    /// Estimate steady-state token distribution by averaging over `n_samples`
462    /// independent simulations each run to `t_end`.
463    ///
464    /// Returns a vector of mean token counts for each place.
465    pub fn steady_state_estimate(&self, t_end: f64, n_samples: usize) -> Vec<f64> {
466        let n_places = self.net.places.len();
467        let mut totals = vec![0.0_f64; n_places];
468
469        for _ in 0..n_samples {
470            let mut sim = self.clone();
471            let events = sim.simulate_until(t_end);
472            let _ = events; // We care about the final marking.
473            let marking = sim.net.marking();
474            for (i, &tok) in marking.iter().enumerate() {
475                totals[i] += tok as f64;
476            }
477        }
478        totals.iter().map(|&s| s / n_samples as f64).collect()
479    }
480}
481
482// ---------------------------------------------------------------------------
483// ColoredPetriNet
484// ---------------------------------------------------------------------------
485
486/// A token in a colored Petri net, represented as an `f64` feature vector.
487#[derive(Debug, Clone, PartialEq)]
488pub struct ColoredToken {
489    /// Feature vector (the "color") of the token.
490    pub color: Vec<f64>,
491}
492
493impl ColoredToken {
494    /// Create a token with the given color vector.
495    pub fn new(color: Vec<f64>) -> Self {
496        Self { color }
497    }
498}
499
500/// A place in a colored Petri net, holding typed tokens.
501#[derive(Debug, Clone)]
502pub struct ColoredPlace {
503    /// Name of the place.
504    pub name: String,
505    /// Tokens currently in this place.
506    pub tokens: Vec<ColoredToken>,
507}
508
509impl ColoredPlace {
510    /// Create an empty colored place.
511    pub fn new(name: impl Into<String>) -> Self {
512        Self {
513            name: name.into(),
514            tokens: Vec::new(),
515        }
516    }
517
518    /// Add a token to this place.
519    pub fn add_token(&mut self, token: ColoredToken) {
520        self.tokens.push(token);
521    }
522
523    /// Remove and return the first token, if any.
524    pub fn take_token(&mut self) -> Option<ColoredToken> {
525        if self.tokens.is_empty() {
526            None
527        } else {
528            Some(self.tokens.remove(0))
529        }
530    }
531
532    /// Number of tokens in the place.
533    pub fn token_count(&self) -> usize {
534        self.tokens.len()
535    }
536}
537
538/// A transition in a colored Petri net.
539#[derive(Debug, Clone)]
540pub struct ColoredTransition {
541    /// Name of the transition.
542    pub name: String,
543    /// Optional color guard: returns `true` iff the token is accepted.
544    pub color_guard: Option<fn(&ColoredToken) -> bool>,
545}
546
547impl ColoredTransition {
548    /// Create a new unguarded colored transition.
549    pub fn new(name: impl Into<String>) -> Self {
550        Self {
551            name: name.into(),
552            color_guard: None,
553        }
554    }
555
556    /// Create a guarded colored transition.
557    pub fn with_guard(name: impl Into<String>, guard: fn(&ColoredToken) -> bool) -> Self {
558        Self {
559            name: name.into(),
560            color_guard: Some(guard),
561        }
562    }
563}
564
565/// A colored Petri net where tokens carry `f64` vectors as their type.
566#[derive(Debug, Clone)]
567pub struct ColoredPetriNet {
568    /// Places in this colored net.
569    pub places: Vec<ColoredPlace>,
570    /// Transitions in this colored net.
571    pub transitions: Vec<ColoredTransition>,
572    /// Input arcs: `(transition_idx, place_idx)`.
573    pub arcs_in: Vec<(usize, usize)>,
574    /// Output arcs: `(transition_idx, place_idx)`.
575    pub arcs_out: Vec<(usize, usize)>,
576}
577
578impl ColoredPetriNet {
579    /// Create an empty colored Petri net.
580    pub fn new() -> Self {
581        Self {
582            places: Vec::new(),
583            transitions: Vec::new(),
584            arcs_in: Vec::new(),
585            arcs_out: Vec::new(),
586        }
587    }
588
589    /// Add a colored place and return its index.
590    pub fn add_place(&mut self, place: ColoredPlace) -> usize {
591        let idx = self.places.len();
592        self.places.push(place);
593        idx
594    }
595
596    /// Add a colored transition and return its index.
597    pub fn add_transition(&mut self, transition: ColoredTransition) -> usize {
598        let idx = self.transitions.len();
599        self.transitions.push(transition);
600        idx
601    }
602
603    /// Add an input arc.
604    pub fn add_arc_in(&mut self, t: usize, p: usize) {
605        self.arcs_in.push((t, p));
606    }
607
608    /// Add an output arc.
609    pub fn add_arc_out(&mut self, t: usize, p: usize) {
610        self.arcs_out.push((t, p));
611    }
612
613    /// Return indices of enabled transitions.
614    pub fn enabled_transitions(&self) -> Vec<usize> {
615        let mut enabled = Vec::new();
616        'outer: for (t_idx, trans) in self.transitions.iter().enumerate() {
617            for &(t, p) in &self.arcs_in {
618                if t == t_idx {
619                    if self.places[p].tokens.is_empty() {
620                        continue 'outer;
621                    }
622                    if let Some(guard) = trans.color_guard
623                        && !guard(&self.places[p].tokens[0])
624                    {
625                        continue 'outer;
626                    }
627                }
628            }
629            enabled.push(t_idx);
630        }
631        enabled
632    }
633
634    /// Fire transition `t_idx`, consuming one token from each input place and
635    /// producing a copy into each output place.
636    pub fn fire(&mut self, t_idx: usize) -> Result<(), String> {
637        if t_idx >= self.transitions.len() {
638            return Err(format!("transition index {} out of range", t_idx));
639        }
640        // Collect tokens to move.
641        let mut consumed: Vec<(usize, ColoredToken)> = Vec::new();
642        for &(t, p) in &self.arcs_in {
643            if t == t_idx {
644                let tok = self.places[p]
645                    .take_token()
646                    .ok_or_else(|| format!("place {} has no tokens", p))?;
647                consumed.push((p, tok));
648            }
649        }
650        // Deposit tokens into output places.
651        let token_to_pass = consumed
652            .first()
653            .map(|(_, tok)| tok.clone())
654            .unwrap_or_else(|| ColoredToken::new(vec![0.0]));
655        for &(t, p) in &self.arcs_out {
656            if t == t_idx {
657                self.places[p].add_token(token_to_pass.clone());
658            }
659        }
660        Ok(())
661    }
662
663    /// Total token count across all places.
664    pub fn total_tokens(&self) -> usize {
665        self.places.iter().map(|p| p.token_count()).sum()
666    }
667}
668
669impl Default for ColoredPetriNet {
670    fn default() -> Self {
671        Self::new()
672    }
673}
674
675// ---------------------------------------------------------------------------
676// PNML stub
677// ---------------------------------------------------------------------------
678
679/// Stub for parsing Petri Net Markup Language (PNML) XML files.
680///
681/// Returns an empty `PetriNet` – full XML parsing is outside the scope of this
682/// crate (no external crate dependencies allowed).
683pub fn parse_pnml(_input: &str) -> PetriNet {
684    // In a full implementation this would parse the PNML XML format.
685    PetriNet::new()
686}
687
688// ---------------------------------------------------------------------------
689// Utility helpers
690// ---------------------------------------------------------------------------
691
692/// Build a simple producer-consumer Petri net for illustration.
693///
694/// Returns a net with two places ("buffer" and "consumer_ready") and two
695/// transitions ("produce" and "consume").
696pub fn producer_consumer_net() -> PetriNet {
697    let mut net = PetriNet::new();
698    let buffer = net.add_place(Place::with_tokens("buffer", 0));
699    let ready = net.add_place(Place::with_tokens("consumer_ready", 1));
700    let produce = net.add_transition(Transition::new("produce"));
701    let consume = net.add_transition(Transition::new("consume"));
702    net.add_arc_in(consume, buffer, 1);
703    net.add_arc_in(consume, ready, 1);
704    net.add_arc_out(produce, buffer, 1);
705    net.add_arc_out(consume, ready, 1);
706    net
707}
708
709/// Build a simple mutual-exclusion Petri net.
710///
711/// Two processes compete for a single resource token.
712pub fn mutex_net() -> PetriNet {
713    let mut net = PetriNet::new();
714    let resource = net.add_place(Place::with_tokens("resource", 1));
715    let p1_idle = net.add_place(Place::with_tokens("p1_idle", 1));
716    let p2_idle = net.add_place(Place::with_tokens("p2_idle", 1));
717    let p1_cs = net.add_place(Place::with_tokens("p1_cs", 0));
718    let p2_cs = net.add_place(Place::with_tokens("p2_cs", 0));
719    let enter1 = net.add_transition(Transition::new("enter1"));
720    let exit1 = net.add_transition(Transition::new("exit1"));
721    let enter2 = net.add_transition(Transition::new("enter2"));
722    let exit2 = net.add_transition(Transition::new("exit2"));
723    net.add_arc_in(enter1, resource, 1);
724    net.add_arc_in(enter1, p1_idle, 1);
725    net.add_arc_out(enter1, p1_cs, 1);
726    net.add_arc_in(exit1, p1_cs, 1);
727    net.add_arc_out(exit1, resource, 1);
728    net.add_arc_out(exit1, p1_idle, 1);
729    net.add_arc_in(enter2, resource, 1);
730    net.add_arc_in(enter2, p2_idle, 1);
731    net.add_arc_out(enter2, p2_cs, 1);
732    net.add_arc_in(exit2, p2_cs, 1);
733    net.add_arc_out(exit2, resource, 1);
734    net.add_arc_out(exit2, p2_idle, 1);
735    net
736}
737
738// ---------------------------------------------------------------------------
739// Incidence matrix helpers
740// ---------------------------------------------------------------------------
741
742/// Compute the incidence matrix of a Petri net.
743///
744/// Entry `[t][p]` = (weight produced by t in p) − (weight consumed by t from p).
745pub fn incidence_matrix(net: &PetriNet) -> Vec<Vec<i64>> {
746    let nt = net.transitions.len();
747    let np = net.places.len();
748    let mut matrix = vec![vec![0i64; np]; nt];
749    for &(t, p, w) in &net.arcs_out {
750        matrix[t][p] += w as i64;
751    }
752    for &(t, p, w) in &net.arcs_in {
753        matrix[t][p] -= w as i64;
754    }
755    matrix
756}
757
758/// Compute the state equation: `m' = m + C^T * sigma` where `sigma` is a
759/// firing count vector and `C` is the incidence matrix.
760pub fn state_equation(net: &PetriNet, sigma: &[i64]) -> Vec<i64> {
761    let matrix = incidence_matrix(net);
762    let marking: Vec<i64> = net.places.iter().map(|p| p.tokens as i64).collect();
763    let np = net.places.len();
764    let mut result = marking;
765    for (t_idx, &fires) in sigma.iter().enumerate() {
766        if t_idx < matrix.len() {
767            for p in 0..np {
768                result[p] += matrix[t_idx][p] * fires;
769            }
770        }
771    }
772    result
773}
774
775// ---------------------------------------------------------------------------
776// Trap and siphon detection
777// ---------------------------------------------------------------------------
778
779/// Check whether a subset of place indices forms a trap.
780///
781/// A trap is a set T of places such that every transition that removes a token
782/// from some place in T also puts a token into some place in T.
783pub fn is_trap(net: &PetriNet, place_set: &[usize]) -> bool {
784    let set: HashSet<usize> = place_set.iter().copied().collect();
785    for &(t, p_in, _) in &net.arcs_in {
786        if set.contains(&p_in) {
787            // This transition takes from the set; it must also put into the set.
788            let produces_into_set = net
789                .arcs_out
790                .iter()
791                .any(|&(tout, pout, _)| tout == t && set.contains(&pout));
792            if !produces_into_set {
793                return false;
794            }
795        }
796    }
797    true
798}
799
800/// Check whether a subset of place indices forms a siphon.
801///
802/// A siphon is a set S of places such that every transition that puts a token
803/// into some place in S also removes a token from some place in S.
804pub fn is_siphon(net: &PetriNet, place_set: &[usize]) -> bool {
805    let set: HashSet<usize> = place_set.iter().copied().collect();
806    for &(t, p_out, _) in &net.arcs_out {
807        if set.contains(&p_out) {
808            let takes_from_set = net
809                .arcs_in
810                .iter()
811                .any(|&(tin, pin, _)| tin == t && set.contains(&pin));
812            if !takes_from_set {
813                return false;
814            }
815        }
816    }
817    true
818}
819
820// ---------------------------------------------------------------------------
821// T-invariant computation (simple linear algebra stub)
822// ---------------------------------------------------------------------------
823
824/// A T-invariant (transition invariant) is a non-negative integer vector `x`
825/// such that `C * x = 0` where `C` is the incidence matrix.
826///
827/// This function uses a naive exhaustive search over small domains and returns
828/// the first non-trivial T-invariant found (if any).
829pub fn find_t_invariant(net: &PetriNet, max_count: usize) -> Option<Vec<i64>> {
830    let matrix = incidence_matrix(net);
831    let nt = net.transitions.len();
832    let np = net.places.len();
833    // Try all combinations up to max_count per transition.
834    fn rec(
835        matrix: &[Vec<i64>],
836        nt: usize,
837        np: usize,
838        max_count: usize,
839        idx: usize,
840        current: &mut Vec<i64>,
841    ) -> Option<Vec<i64>> {
842        if idx == nt {
843            // Check if C * current == 0.
844            let mut is_trivial = true;
845            for &c in current.iter() {
846                if c != 0 {
847                    is_trivial = false;
848                    break;
849                }
850            }
851            if is_trivial {
852                return None;
853            }
854            for p in 0..np {
855                let mut sum = 0i64;
856                for t in 0..nt {
857                    sum += matrix[t][p] * current[t];
858                }
859                if sum != 0 {
860                    return None;
861                }
862            }
863            return Some(current.clone());
864        }
865        for v in 0..=(max_count as i64) {
866            current[idx] = v;
867            if let Some(res) = rec(matrix, nt, np, max_count, idx + 1, current) {
868                return Some(res);
869            }
870        }
871        None
872    }
873    let mut current = vec![0i64; nt];
874    rec(&matrix, nt, np, max_count, 0, &mut current)
875}
876
877// ---------------------------------------------------------------------------
878// Firing sequence analysis
879// ---------------------------------------------------------------------------
880
881/// Attempt to replay a firing sequence from the current marking.
882///
883/// Returns `Ok(final_marking)` or `Err(step_index)` on failure.
884pub fn replay_sequence(net: &mut PetriNet, sequence: &[usize]) -> Result<Vec<usize>, usize> {
885    for (step, &t_idx) in sequence.iter().enumerate() {
886        if net.fire(t_idx).is_err() {
887            return Err(step);
888        }
889    }
890    Ok(net.marking())
891}
892
893// ---------------------------------------------------------------------------
894// Petri net metrics
895// ---------------------------------------------------------------------------
896
897/// Compute structural properties summary for a Petri net.
898#[derive(Debug, Clone)]
899pub struct PetriNetMetrics {
900    /// Number of places.
901    pub n_places: usize,
902    /// Number of transitions.
903    pub n_transitions: usize,
904    /// Number of arcs (in + out).
905    pub n_arcs: usize,
906    /// Average out-degree of transitions.
907    pub avg_out_degree: f64,
908    /// Whether any transition has no input arcs (source transition).
909    pub has_source_transition: bool,
910    /// Whether any transition has no output arcs (sink transition).
911    pub has_sink_transition: bool,
912}
913
914/// Compute structural metrics for the given Petri net.
915pub fn compute_metrics(net: &PetriNet) -> PetriNetMetrics {
916    let n_transitions = net.transitions.len();
917    let total_out: usize = net
918        .transitions
919        .iter()
920        .enumerate()
921        .map(|(t, _)| net.arcs_out.iter().filter(|&&(ti, _, _)| ti == t).count())
922        .sum();
923    let avg_out_degree = if n_transitions == 0 {
924        0.0
925    } else {
926        total_out as f64 / n_transitions as f64
927    };
928    let has_source_transition =
929        (0..n_transitions).any(|t| !net.arcs_in.iter().any(|&(ti, _, _)| ti == t));
930    let has_sink_transition =
931        (0..n_transitions).any(|t| !net.arcs_out.iter().any(|&(ti, _, _)| ti == t));
932    PetriNetMetrics {
933        n_places: net.places.len(),
934        n_transitions,
935        n_arcs: net.arcs_in.len() + net.arcs_out.len(),
936        avg_out_degree,
937        has_source_transition,
938        has_sink_transition,
939    }
940}
941
942// ---------------------------------------------------------------------------
943// Merging nets
944// ---------------------------------------------------------------------------
945
946/// Merge two Petri nets by disjoint union (all places and transitions are
947/// included; no arcs are shared).
948pub fn disjoint_union(a: &PetriNet, b: &PetriNet) -> PetriNet {
949    let mut result = a.clone();
950    let place_offset = a.places.len();
951    let trans_offset = a.transitions.len();
952    for p in &b.places {
953        result.places.push(p.clone());
954    }
955    for t in &b.transitions {
956        result.transitions.push(t.clone());
957    }
958    for &(t, p, w) in &b.arcs_in {
959        result.arcs_in.push((t + trans_offset, p + place_offset, w));
960    }
961    for &(t, p, w) in &b.arcs_out {
962        result
963            .arcs_out
964            .push((t + trans_offset, p + place_offset, w));
965    }
966    result
967}
968
969// ---------------------------------------------------------------------------
970// Liveness analysis
971// ---------------------------------------------------------------------------
972
973/// Check whether transition `t_idx` is live (can always eventually fire again
974/// from any reachable marking).
975///
976/// This is a lightweight approximation: returns `true` if `t_idx` appears in
977/// the enabled set of at least one reachable marking.
978pub fn is_live_approx(net: &PetriNet, t_idx: usize) -> bool {
979    let reachable = net.reachability_bfs();
980    let mut sim = net.clone();
981    for marking in &reachable {
982        sim.set_marking(marking);
983        if sim.enabled_transitions().contains(&t_idx) {
984            return true;
985        }
986    }
987    false
988}
989
990// ---------------------------------------------------------------------------
991// Shared-resource workflow example
992// ---------------------------------------------------------------------------
993
994/// Build a workflow Petri net with a shared resource pool of size `capacity`.
995pub fn workflow_with_resource(capacity: usize) -> PetriNet {
996    let mut net = PetriNet::new();
997    let start = net.add_place(Place::with_tokens("start", 1));
998    let working = net.add_place(Place::with_tokens("working", 0));
999    let done = net.add_place(Place::with_tokens("done", 0));
1000    let resource = net.add_place(Place::with_tokens("resource", capacity));
1001    let begin = net.add_transition(Transition::new("begin"));
1002    let finish = net.add_transition(Transition::new("finish"));
1003    net.add_arc_in(begin, start, 1);
1004    net.add_arc_in(begin, resource, 1);
1005    net.add_arc_out(begin, working, 1);
1006    net.add_arc_in(finish, working, 1);
1007    net.add_arc_out(finish, done, 1);
1008    net.add_arc_out(finish, resource, 1);
1009    net
1010}
1011
1012// ---------------------------------------------------------------------------
1013// HashMap-based marking for large nets
1014// ---------------------------------------------------------------------------
1015
1016/// Sparse marking: maps place names to token counts.
1017pub type SparseMarking = HashMap<String, usize>;
1018
1019/// Convert a dense marking vector (aligned with `net.places`) into a sparse map.
1020pub fn dense_to_sparse(net: &PetriNet, marking: &[usize]) -> SparseMarking {
1021    net.places
1022        .iter()
1023        .zip(marking.iter())
1024        .map(|(p, &tok)| (p.name.clone(), tok))
1025        .collect()
1026}
1027
1028/// Convert a sparse marking map back to a dense vector (missing places = 0).
1029pub fn sparse_to_dense(net: &PetriNet, sparse: &SparseMarking) -> Vec<usize> {
1030    net.places
1031        .iter()
1032        .map(|p| *sparse.get(&p.name).unwrap_or(&0))
1033        .collect()
1034}
1035
1036// ---------------------------------------------------------------------------
1037// Tests
1038// ---------------------------------------------------------------------------
1039
1040#[cfg(test)]
1041mod tests {
1042    use super::*;
1043
1044    fn simple_net() -> PetriNet {
1045        let mut net = PetriNet::new();
1046        let p0 = net.add_place(Place::with_tokens("p0", 1));
1047        let p1 = net.add_place(Place::with_tokens("p1", 0));
1048        let t0 = net.add_transition(Transition::new("t0"));
1049        net.add_arc_in(t0, p0, 1);
1050        net.add_arc_out(t0, p1, 1);
1051        net
1052    }
1053
1054    #[test]
1055    fn test_place_tokens() {
1056        let p = Place::with_tokens("p", 3);
1057        assert_eq!(p.tokens, 3);
1058    }
1059
1060    #[test]
1061    fn test_transition_name() {
1062        let t = Transition::new("t1");
1063        assert_eq!(t.name, "t1");
1064    }
1065
1066    #[test]
1067    fn test_add_place_transition() {
1068        let mut net = PetriNet::new();
1069        let pi = net.add_place(Place::new("p"));
1070        let ti = net.add_transition(Transition::new("t"));
1071        assert_eq!(pi, 0);
1072        assert_eq!(ti, 0);
1073    }
1074
1075    #[test]
1076    fn test_fire_basic() {
1077        let mut net = simple_net();
1078        assert_eq!(net.marking(), vec![1, 0]);
1079        net.fire(0).unwrap();
1080        assert_eq!(net.marking(), vec![0, 1]);
1081    }
1082
1083    #[test]
1084    fn test_fire_not_enabled() {
1085        let mut net = simple_net();
1086        net.fire(0).unwrap(); // consume the token
1087        assert!(net.fire(0).is_err());
1088    }
1089
1090    #[test]
1091    fn test_enabled_transitions_empty() {
1092        let mut net = simple_net();
1093        net.fire(0).unwrap();
1094        assert!(net.enabled_transitions().is_empty());
1095    }
1096
1097    #[test]
1098    fn test_is_deadlocked() {
1099        let mut net = simple_net();
1100        net.fire(0).unwrap();
1101        assert!(net.is_deadlocked());
1102    }
1103
1104    #[test]
1105    fn test_marking_sum() {
1106        let net = simple_net();
1107        assert_eq!(net.marking_sum(), 1);
1108    }
1109
1110    #[test]
1111    fn test_reachability_bfs() {
1112        let net = simple_net();
1113        let reachable = net.reachability_bfs();
1114        assert!(reachable.contains(&vec![1, 0]));
1115        assert!(reachable.contains(&vec![0, 1]));
1116        assert_eq!(reachable.len(), 2);
1117    }
1118
1119    #[test]
1120    fn test_mutex_net_reachability() {
1121        let net = mutex_net();
1122        let reachable = net.reachability_bfs();
1123        // Mutual exclusion: p1_cs and p2_cs should never both hold a token.
1124        for marking in &reachable {
1125            // places: resource=0, p1_idle=1, p2_idle=2, p1_cs=3, p2_cs=4
1126            assert!(
1127                !(marking[3] >= 1 && marking[4] >= 1),
1128                "mutual exclusion violated"
1129            );
1130        }
1131    }
1132
1133    #[test]
1134    fn test_producer_consumer_enabled() {
1135        let mut net = producer_consumer_net();
1136        // Initially buffer=0, consumer_ready=1; only produce is enabled.
1137        let enabled = net.enabled_transitions();
1138        assert_eq!(enabled.len(), 1); // only produce
1139        net.fire(enabled[0]).unwrap(); // produce a token
1140        assert_eq!(net.marking_sum(), 2); // buffer=1, consumer_ready=1
1141    }
1142
1143    #[test]
1144    fn test_coverability_tree_bounded() {
1145        let net = simple_net();
1146        let tree = CoverabilityTree::build(&net);
1147        assert!(tree.is_bounded());
1148    }
1149
1150    #[test]
1151    fn test_coverability_tree_unbounded() {
1152        // A net with a self-loop that infinitely produces tokens is unbounded.
1153        let mut net = PetriNet::new();
1154        let p0 = net.add_place(Place::with_tokens("p0", 1));
1155        let t0 = net.add_transition(Transition::new("t0"));
1156        // t0 consumes 1 from p0, produces 2.
1157        net.add_arc_in(t0, p0, 1);
1158        net.add_arc_out(t0, p0, 2);
1159        let tree = CoverabilityTree::build(&net);
1160        assert!(!tree.is_bounded());
1161    }
1162
1163    #[test]
1164    fn test_stochastic_simulate() {
1165        let mut net = PetriNet::new();
1166        let p0 = net.add_place(Place::with_tokens("p0", 3));
1167        let p1 = net.add_place(Place::with_tokens("p1", 0));
1168        let t0 = net.add_transition(Transition::new("t0"));
1169        net.add_arc_in(t0, p0, 1);
1170        net.add_arc_out(t0, p1, 1);
1171        let mut spn = StochasticPetriNet::new(net, vec![1.0]);
1172        let events = spn.simulate_until(10.0);
1173        // Should have fired exactly 3 times (all tokens consumed).
1174        assert_eq!(events.len(), 3);
1175    }
1176
1177    #[test]
1178    fn test_stochastic_steady_state() {
1179        let net = simple_net();
1180        let spn = StochasticPetriNet::new(net, vec![1.0]);
1181        let ss = spn.steady_state_estimate(100.0, 5);
1182        // Total tokens must be conserved in the mean.
1183        assert!((ss.iter().sum::<f64>() - 1.0).abs() < 1e-9);
1184    }
1185
1186    #[test]
1187    fn test_colored_place_tokens() {
1188        let mut p = ColoredPlace::new("p");
1189        p.add_token(ColoredToken::new(vec![1.0, 2.0]));
1190        assert_eq!(p.token_count(), 1);
1191        let tok = p.take_token().unwrap();
1192        assert_eq!(tok.color, vec![1.0, 2.0]);
1193        assert_eq!(p.token_count(), 0);
1194    }
1195
1196    #[test]
1197    fn test_colored_petri_net_fire() {
1198        let mut cpn = ColoredPetriNet::new();
1199        let p0 = cpn.add_place(ColoredPlace::new("p0"));
1200        let p1 = cpn.add_place(ColoredPlace::new("p1"));
1201        let t0 = cpn.add_transition(ColoredTransition::new("t0"));
1202        cpn.add_arc_in(t0, p0);
1203        cpn.add_arc_out(t0, p1);
1204        cpn.places[p0].add_token(ColoredToken::new(vec![1.0]));
1205        cpn.fire(t0).unwrap();
1206        assert_eq!(cpn.places[p0].token_count(), 0);
1207        assert_eq!(cpn.places[p1].token_count(), 1);
1208    }
1209
1210    #[test]
1211    fn test_parse_pnml_stub() {
1212        let net = parse_pnml("<pnml/>");
1213        assert!(net.places.is_empty());
1214    }
1215
1216    #[test]
1217    fn test_incidence_matrix() {
1218        let net = simple_net();
1219        let m = incidence_matrix(&net);
1220        // t0: -1 for p0, +1 for p1
1221        assert_eq!(m[0][0], -1);
1222        assert_eq!(m[0][1], 1);
1223    }
1224
1225    #[test]
1226    fn test_state_equation() {
1227        let net = simple_net();
1228        let result = state_equation(&net, &[1]);
1229        assert_eq!(result, vec![0, 1]);
1230    }
1231
1232    #[test]
1233    fn test_is_trap() {
1234        // For the simple net: {p1} is a trap (nothing removes from it).
1235        let simple = simple_net();
1236        assert!(is_trap(&simple, &[1]));
1237    }
1238
1239    #[test]
1240    fn test_is_siphon() {
1241        let simple = simple_net();
1242        // {p0} is a siphon: nothing produces into p0.
1243        assert!(is_siphon(&simple, &[0]));
1244    }
1245
1246    #[test]
1247    fn test_find_t_invariant() {
1248        // A loop net: t0: p0 → p1, t1: p1 → p0 should have invariant [1,1].
1249        let mut net = PetriNet::new();
1250        let p0 = net.add_place(Place::with_tokens("p0", 1));
1251        let p1 = net.add_place(Place::with_tokens("p1", 0));
1252        let t0 = net.add_transition(Transition::new("t0"));
1253        let t1 = net.add_transition(Transition::new("t1"));
1254        net.add_arc_in(t0, p0, 1);
1255        net.add_arc_out(t0, p1, 1);
1256        net.add_arc_in(t1, p1, 1);
1257        net.add_arc_out(t1, p0, 1);
1258        let inv = find_t_invariant(&net, 2);
1259        assert!(inv.is_some());
1260    }
1261
1262    #[test]
1263    fn test_replay_sequence_ok() {
1264        let mut net = simple_net();
1265        let result = replay_sequence(&mut net, &[0]);
1266        assert_eq!(result, Ok(vec![0, 1]));
1267    }
1268
1269    #[test]
1270    fn test_replay_sequence_fail() {
1271        let mut net = simple_net();
1272        let result = replay_sequence(&mut net, &[0, 0]);
1273        assert_eq!(result, Err(1));
1274    }
1275
1276    #[test]
1277    fn test_compute_metrics() {
1278        let net = simple_net();
1279        let m = compute_metrics(&net);
1280        assert_eq!(m.n_places, 2);
1281        assert_eq!(m.n_transitions, 1);
1282        assert_eq!(m.n_arcs, 2);
1283    }
1284
1285    #[test]
1286    fn test_disjoint_union() {
1287        let a = simple_net();
1288        let b = simple_net();
1289        let u = disjoint_union(&a, &b);
1290        assert_eq!(u.places.len(), 4);
1291        assert_eq!(u.transitions.len(), 2);
1292    }
1293
1294    #[test]
1295    fn test_is_live_approx() {
1296        let net = simple_net();
1297        assert!(is_live_approx(&net, 0));
1298    }
1299
1300    #[test]
1301    fn test_workflow_net() {
1302        let net = workflow_with_resource(2);
1303        assert_eq!(net.places.len(), 4);
1304        assert_eq!(net.transitions.len(), 2);
1305    }
1306
1307    #[test]
1308    fn test_dense_sparse_roundtrip() {
1309        let net = simple_net();
1310        let marking = net.marking();
1311        let sparse = dense_to_sparse(&net, &marking);
1312        let dense = sparse_to_dense(&net, &sparse);
1313        assert_eq!(marking, dense);
1314    }
1315
1316    #[test]
1317    fn test_guarded_transition() {
1318        let mut net = PetriNet::new();
1319        let p0 = net.add_place(Place::with_tokens("p0", 2));
1320        let p1 = net.add_place(Place::with_tokens("p1", 0));
1321        // Only fire if p0 has at least 2 tokens.
1322        let t0 = net.add_transition(Transition::with_guard("t0", |tokens| {
1323            tokens.iter().all(|&t| t >= 2)
1324        }));
1325        net.add_arc_in(t0, p0, 1);
1326        net.add_arc_out(t0, p1, 1);
1327        let enabled = net.enabled_transitions();
1328        assert_eq!(enabled.len(), 1);
1329        net.fire(0).unwrap();
1330        // Now p0 has 1 token, guard should fail.
1331        assert!(net.enabled_transitions().is_empty());
1332    }
1333
1334    #[test]
1335    fn test_coverability_tree_node_count() {
1336        let net = simple_net();
1337        let tree = CoverabilityTree::build(&net);
1338        assert!(tree.node_count() >= 2);
1339    }
1340
1341    #[test]
1342    fn test_colored_transition_guard() {
1343        let mut cpn = ColoredPetriNet::new();
1344        let p0 = cpn.add_place(ColoredPlace::new("p0"));
1345        let p1 = cpn.add_place(ColoredPlace::new("p1"));
1346        let t0 = cpn.add_transition(ColoredTransition::with_guard("t0", |tok| {
1347            tok.color.first().copied().unwrap_or(0.0) > 0.5
1348        }));
1349        cpn.add_arc_in(t0, p0);
1350        cpn.add_arc_out(t0, p1);
1351        // Token with low value: guard should reject.
1352        cpn.places[p0].add_token(ColoredToken::new(vec![0.1]));
1353        assert!(cpn.enabled_transitions().is_empty());
1354        // Token with high value: guard should accept.
1355        cpn.places[p0].tokens.clear();
1356        cpn.places[p0].add_token(ColoredToken::new(vec![0.9]));
1357        assert_eq!(cpn.enabled_transitions(), vec![0]);
1358    }
1359}