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 parser (hand-rolled, no external XML crate dependencies)
677// ---------------------------------------------------------------------------
678
679/// Extract the value of a named attribute from an XML tag string.
680///
681/// Finds `attr="..."` patterns within a raw tag slice and returns the value.
682fn xml_attr_value<'a>(tag: &'a str, attr: &str) -> Option<&'a str> {
683    let needle = format!("{}=\"", attr);
684    tag.find(needle.as_str()).and_then(|start| {
685        let value_start = start + needle.len();
686        tag[value_start..]
687            .find('"')
688            .map(|end| &tag[value_start..value_start + end])
689    })
690}
691
692/// Extract text content from a `<text>N</text>` block appearing anywhere in `src`.
693fn xml_text_content(src: &str) -> Option<&str> {
694    let open = src.find("<text>")?;
695    let content_start = open + "<text>".len();
696    let close = src[content_start..].find("</text>")?;
697    Some(src[content_start..content_start + close].trim())
698}
699
700/// Parse Petri Net Markup Language (PNML) XML and return a `PetriNet`.
701///
702/// Implements a forward tag scanner with a small context stack.  It handles
703/// `<place>`, `<transition>`, and `<arc>` elements, extracting `id`, `name`,
704/// `source`, `target` attributes.  Initial markings come from
705/// `<initialMarking><text>N</text></initialMarking>` nested inside `<place>`.
706/// Arc weights come from `<inscription><text>W</text></inscription>` nested
707/// inside `<arc>`.  Unknown wrapper elements (e.g. `<pnml>`, `<net>`,
708/// `<page>`) are traversed transparently.
709///
710/// On malformed or unrecognised input the parser returns whatever portion of
711/// the net has been constructed so far (best-effort).
712pub fn parse_pnml(input: &str) -> PetriNet {
713    let mut net = PetriNet::new();
714
715    // Maps PNML id strings → internal place/transition indices.
716    let mut place_id_map: HashMap<String, usize> = HashMap::new();
717    let mut trans_id_map: HashMap<String, usize> = HashMap::new();
718
719    // A pending element accumulates attributes until its closing tag is seen.
720    #[derive(Clone)]
721    enum Pending {
722        Place {
723            id: String,
724            name: String,
725            tokens: usize,
726        },
727        Trans {
728            id: String,
729            name: String,
730        },
731        Arc {
732            source: String,
733            target: String,
734            weight: usize,
735        },
736    }
737
738    // Element kind we are currently nested inside (for <text> attribution).
739    #[derive(Clone, Copy, PartialEq)]
740    enum Context {
741        None,
742        InitMarking,
743        Inscription,
744    }
745
746    let mut stack: Vec<Pending> = Vec::new();
747    let mut context = Context::None;
748
749    let mut pos = 0;
750    while pos < input.len() {
751        // Find next '<'.
752        let Some(rel_open) = input[pos..].find('<') else {
753            break;
754        };
755        let tag_open = pos + rel_open;
756
757        // Skip XML comments.
758        if input[tag_open..].starts_with("<!--") {
759            pos = input[tag_open..]
760                .find("-->")
761                .map(|e| tag_open + e + 3)
762                .unwrap_or(input.len());
763            continue;
764        }
765        // Skip processing instructions.
766        if input[tag_open..].starts_with("<?") {
767            pos = input[tag_open..]
768                .find("?>")
769                .map(|e| tag_open + e + 2)
770                .unwrap_or(input.len());
771            continue;
772        }
773
774        // Find the '>' that closes this tag.
775        let Some(rel_close) = input[tag_open..].find('>') else {
776            break;
777        };
778        let tag_close = tag_open + rel_close + 1;
779        let raw_tag = &input[tag_open..tag_close];
780        pos = tag_close;
781
782        let is_closing = raw_tag.starts_with("</");
783        let is_self_closing = !is_closing
784            && (raw_tag.ends_with("/>") || raw_tag.trim_end_matches('>').trim().ends_with('/'));
785
786        if is_closing {
787            // Extract closing element name.
788            let rest = &raw_tag[2..];
789            let name_len = rest
790                .find(|c: char| c == '>' || c.is_whitespace())
791                .unwrap_or(rest.len());
792            let closing = rest[..name_len].trim_end_matches('>');
793
794            match closing {
795                "initialMarking" => {
796                    context = Context::None;
797                }
798                "inscription" => {
799                    context = Context::None;
800                }
801                "place" => {
802                    if let Some(pos_idx) = stack
803                        .iter()
804                        .rposition(|p| matches!(p, Pending::Place { .. }))
805                    {
806                        let pending = stack.remove(pos_idx);
807                        if let Pending::Place { id, name, tokens } = pending {
808                            let display = if name.is_empty() { &id } else { &name };
809                            let idx = net.add_place(Place::with_tokens(display, tokens));
810                            place_id_map.insert(id, idx);
811                        }
812                    }
813                }
814                "transition" => {
815                    if let Some(pos_idx) = stack
816                        .iter()
817                        .rposition(|p| matches!(p, Pending::Trans { .. }))
818                    {
819                        let pending = stack.remove(pos_idx);
820                        if let Pending::Trans { id, name } = pending {
821                            let display = if name.is_empty() { &id } else { &name };
822                            let idx = net.add_transition(Transition::new(display));
823                            trans_id_map.insert(id, idx);
824                        }
825                    }
826                }
827                "arc" => {
828                    if let Some(pos_idx) =
829                        stack.iter().rposition(|p| matches!(p, Pending::Arc { .. }))
830                    {
831                        let pending = stack.remove(pos_idx);
832                        if let Pending::Arc {
833                            source,
834                            target,
835                            weight,
836                        } = pending
837                        {
838                            add_pnml_arc(
839                                &mut net,
840                                &source,
841                                &target,
842                                weight,
843                                &place_id_map,
844                                &trans_id_map,
845                            );
846                        }
847                    }
848                }
849                _ => {}
850            }
851            continue;
852        }
853
854        // Open or self-closing tag — extract element name.
855        let interior = raw_tag
856            .trim_start_matches('<')
857            .trim_end_matches('>')
858            .trim_end_matches('/')
859            .trim();
860        let name_len = interior
861            .find(|c: char| c.is_whitespace())
862            .unwrap_or(interior.len());
863        let elem_name = &interior[..name_len];
864
865        match elem_name {
866            "place" => {
867                let id = xml_attr_value(raw_tag, "id").unwrap_or("").to_owned();
868                let name = xml_attr_value(raw_tag, "name").unwrap_or("").to_owned();
869                if is_self_closing {
870                    let display = if name.is_empty() { &id } else { &name };
871                    let idx = net.add_place(Place::new(display));
872                    place_id_map.insert(id, idx);
873                } else {
874                    stack.push(Pending::Place {
875                        id,
876                        name,
877                        tokens: 0,
878                    });
879                }
880            }
881            "transition" => {
882                let id = xml_attr_value(raw_tag, "id").unwrap_or("").to_owned();
883                let name = xml_attr_value(raw_tag, "name").unwrap_or("").to_owned();
884                if is_self_closing {
885                    let display = if name.is_empty() { &id } else { &name };
886                    let idx = net.add_transition(Transition::new(display));
887                    trans_id_map.insert(id, idx);
888                } else {
889                    stack.push(Pending::Trans { id, name });
890                }
891            }
892            "arc" => {
893                let source = xml_attr_value(raw_tag, "source").unwrap_or("").to_owned();
894                let target = xml_attr_value(raw_tag, "target").unwrap_or("").to_owned();
895                let weight: usize = xml_attr_value(raw_tag, "weight")
896                    .and_then(|w| w.parse().ok())
897                    .unwrap_or(1);
898                if is_self_closing {
899                    add_pnml_arc(
900                        &mut net,
901                        &source,
902                        &target,
903                        weight,
904                        &place_id_map,
905                        &trans_id_map,
906                    );
907                } else {
908                    stack.push(Pending::Arc {
909                        source,
910                        target,
911                        weight,
912                    });
913                }
914            }
915            "initialMarking" if !is_self_closing => {
916                context = Context::InitMarking;
917            }
918            "inscription" if !is_self_closing => {
919                context = Context::Inscription;
920            }
921            "text" if !is_self_closing => {
922                // Consume text until </text>.
923                if let Some(end_rel) = input[pos..].find("</text>") {
924                    let text_val = input[pos..pos + end_rel].trim();
925                    pos += end_rel + "</text>".len();
926
927                    match context {
928                        Context::InitMarking => {
929                            if let (Ok(tok), Some(Pending::Place { tokens, .. })) = (
930                                text_val.parse::<usize>(),
931                                stack
932                                    .iter_mut()
933                                    .rfind(|p| matches!(p, Pending::Place { .. })),
934                            ) {
935                                *tokens = tok;
936                            }
937                        }
938                        Context::Inscription => {
939                            if let (Ok(w), Some(Pending::Arc { weight, .. })) = (
940                                text_val.parse::<usize>(),
941                                stack.iter_mut().rfind(|p| matches!(p, Pending::Arc { .. })),
942                            ) {
943                                *weight = w;
944                            }
945                        }
946                        Context::None => {}
947                    }
948                }
949            }
950            _ => {} // wrapper elements — traverse transparently
951        }
952    }
953
954    net
955}
956
957/// Helper: add an arc to the net given source and target PNML ids.
958///
959/// An arc from a place to a transition becomes an input arc (consumed tokens).
960/// An arc from a transition to a place becomes an output arc (produced tokens).
961fn add_pnml_arc(
962    net: &mut PetriNet,
963    src_id: &str,
964    tgt_id: &str,
965    weight: Weight,
966    place_id_map: &HashMap<String, usize>,
967    trans_id_map: &HashMap<String, usize>,
968) {
969    match (place_id_map.get(src_id), trans_id_map.get(tgt_id)) {
970        // Place → Transition: input arc (tokens consumed on fire).
971        (Some(&p_idx), Some(&t_idx)) => net.add_arc_in(t_idx, p_idx, weight),
972        _ => {
973            // Try Transition → Place: output arc (tokens produced on fire).
974            if let (Some(&t_idx), Some(&p_idx)) =
975                (trans_id_map.get(src_id), place_id_map.get(tgt_id))
976            {
977                net.add_arc_out(t_idx, p_idx, weight);
978            }
979        }
980    }
981}
982
983// ---------------------------------------------------------------------------
984// Utility helpers
985// ---------------------------------------------------------------------------
986
987/// Build a simple producer-consumer Petri net for illustration.
988///
989/// Returns a net with two places ("buffer" and "consumer_ready") and two
990/// transitions ("produce" and "consume").
991pub fn producer_consumer_net() -> PetriNet {
992    let mut net = PetriNet::new();
993    let buffer = net.add_place(Place::with_tokens("buffer", 0));
994    let ready = net.add_place(Place::with_tokens("consumer_ready", 1));
995    let produce = net.add_transition(Transition::new("produce"));
996    let consume = net.add_transition(Transition::new("consume"));
997    net.add_arc_in(consume, buffer, 1);
998    net.add_arc_in(consume, ready, 1);
999    net.add_arc_out(produce, buffer, 1);
1000    net.add_arc_out(consume, ready, 1);
1001    net
1002}
1003
1004/// Build a simple mutual-exclusion Petri net.
1005///
1006/// Two processes compete for a single resource token.
1007pub fn mutex_net() -> PetriNet {
1008    let mut net = PetriNet::new();
1009    let resource = net.add_place(Place::with_tokens("resource", 1));
1010    let p1_idle = net.add_place(Place::with_tokens("p1_idle", 1));
1011    let p2_idle = net.add_place(Place::with_tokens("p2_idle", 1));
1012    let p1_cs = net.add_place(Place::with_tokens("p1_cs", 0));
1013    let p2_cs = net.add_place(Place::with_tokens("p2_cs", 0));
1014    let enter1 = net.add_transition(Transition::new("enter1"));
1015    let exit1 = net.add_transition(Transition::new("exit1"));
1016    let enter2 = net.add_transition(Transition::new("enter2"));
1017    let exit2 = net.add_transition(Transition::new("exit2"));
1018    net.add_arc_in(enter1, resource, 1);
1019    net.add_arc_in(enter1, p1_idle, 1);
1020    net.add_arc_out(enter1, p1_cs, 1);
1021    net.add_arc_in(exit1, p1_cs, 1);
1022    net.add_arc_out(exit1, resource, 1);
1023    net.add_arc_out(exit1, p1_idle, 1);
1024    net.add_arc_in(enter2, resource, 1);
1025    net.add_arc_in(enter2, p2_idle, 1);
1026    net.add_arc_out(enter2, p2_cs, 1);
1027    net.add_arc_in(exit2, p2_cs, 1);
1028    net.add_arc_out(exit2, resource, 1);
1029    net.add_arc_out(exit2, p2_idle, 1);
1030    net
1031}
1032
1033// ---------------------------------------------------------------------------
1034// Incidence matrix helpers
1035// ---------------------------------------------------------------------------
1036
1037/// Compute the incidence matrix of a Petri net.
1038///
1039/// Entry `[t][p]` = (weight produced by t in p) − (weight consumed by t from p).
1040pub fn incidence_matrix(net: &PetriNet) -> Vec<Vec<i64>> {
1041    let nt = net.transitions.len();
1042    let np = net.places.len();
1043    let mut matrix = vec![vec![0i64; np]; nt];
1044    for &(t, p, w) in &net.arcs_out {
1045        matrix[t][p] += w as i64;
1046    }
1047    for &(t, p, w) in &net.arcs_in {
1048        matrix[t][p] -= w as i64;
1049    }
1050    matrix
1051}
1052
1053/// Compute the state equation: `m' = m + C^T * sigma` where `sigma` is a
1054/// firing count vector and `C` is the incidence matrix.
1055pub fn state_equation(net: &PetriNet, sigma: &[i64]) -> Vec<i64> {
1056    let matrix = incidence_matrix(net);
1057    let marking: Vec<i64> = net.places.iter().map(|p| p.tokens as i64).collect();
1058    let np = net.places.len();
1059    let mut result = marking;
1060    for (t_idx, &fires) in sigma.iter().enumerate() {
1061        if t_idx < matrix.len() {
1062            for p in 0..np {
1063                result[p] += matrix[t_idx][p] * fires;
1064            }
1065        }
1066    }
1067    result
1068}
1069
1070// ---------------------------------------------------------------------------
1071// Trap and siphon detection
1072// ---------------------------------------------------------------------------
1073
1074/// Check whether a subset of place indices forms a trap.
1075///
1076/// A trap is a set T of places such that every transition that removes a token
1077/// from some place in T also puts a token into some place in T.
1078pub fn is_trap(net: &PetriNet, place_set: &[usize]) -> bool {
1079    let set: HashSet<usize> = place_set.iter().copied().collect();
1080    for &(t, p_in, _) in &net.arcs_in {
1081        if set.contains(&p_in) {
1082            // This transition takes from the set; it must also put into the set.
1083            let produces_into_set = net
1084                .arcs_out
1085                .iter()
1086                .any(|&(tout, pout, _)| tout == t && set.contains(&pout));
1087            if !produces_into_set {
1088                return false;
1089            }
1090        }
1091    }
1092    true
1093}
1094
1095/// Check whether a subset of place indices forms a siphon.
1096///
1097/// A siphon is a set S of places such that every transition that puts a token
1098/// into some place in S also removes a token from some place in S.
1099pub fn is_siphon(net: &PetriNet, place_set: &[usize]) -> bool {
1100    let set: HashSet<usize> = place_set.iter().copied().collect();
1101    for &(t, p_out, _) in &net.arcs_out {
1102        if set.contains(&p_out) {
1103            let takes_from_set = net
1104                .arcs_in
1105                .iter()
1106                .any(|&(tin, pin, _)| tin == t && set.contains(&pin));
1107            if !takes_from_set {
1108                return false;
1109            }
1110        }
1111    }
1112    true
1113}
1114
1115// ---------------------------------------------------------------------------
1116// T-invariant computation (simple linear algebra stub)
1117// ---------------------------------------------------------------------------
1118
1119/// A T-invariant (transition invariant) is a non-negative integer vector `x`
1120/// such that `C * x = 0` where `C` is the incidence matrix.
1121///
1122/// This function uses a naive exhaustive search over small domains and returns
1123/// the first non-trivial T-invariant found (if any).
1124pub fn find_t_invariant(net: &PetriNet, max_count: usize) -> Option<Vec<i64>> {
1125    let matrix = incidence_matrix(net);
1126    let nt = net.transitions.len();
1127    let np = net.places.len();
1128    // Try all combinations up to max_count per transition.
1129    fn rec(
1130        matrix: &[Vec<i64>],
1131        nt: usize,
1132        np: usize,
1133        max_count: usize,
1134        idx: usize,
1135        current: &mut Vec<i64>,
1136    ) -> Option<Vec<i64>> {
1137        if idx == nt {
1138            // Check if C * current == 0.
1139            let mut is_trivial = true;
1140            for &c in current.iter() {
1141                if c != 0 {
1142                    is_trivial = false;
1143                    break;
1144                }
1145            }
1146            if is_trivial {
1147                return None;
1148            }
1149            for p in 0..np {
1150                let mut sum = 0i64;
1151                for t in 0..nt {
1152                    sum += matrix[t][p] * current[t];
1153                }
1154                if sum != 0 {
1155                    return None;
1156                }
1157            }
1158            return Some(current.clone());
1159        }
1160        for v in 0..=(max_count as i64) {
1161            current[idx] = v;
1162            if let Some(res) = rec(matrix, nt, np, max_count, idx + 1, current) {
1163                return Some(res);
1164            }
1165        }
1166        None
1167    }
1168    let mut current = vec![0i64; nt];
1169    rec(&matrix, nt, np, max_count, 0, &mut current)
1170}
1171
1172// ---------------------------------------------------------------------------
1173// T-invariant computation via integer Gauss-Jordan null-space
1174// ---------------------------------------------------------------------------
1175
1176/// Compute the GCD of two non-negative integers.
1177fn gcd(mut a: i64, mut b: i64) -> i64 {
1178    a = a.abs();
1179    b = b.abs();
1180    while b != 0 {
1181        let r = a % b;
1182        a = b;
1183        b = r;
1184    }
1185    a
1186}
1187
1188/// Compute the LCM of two non-negative integers, returning `None` on overflow.
1189fn lcm_checked(a: i64, b: i64) -> Option<i64> {
1190    if a == 0 || b == 0 {
1191        return Some(0);
1192    }
1193    let g = gcd(a, b);
1194    // Use checked arithmetic to avoid overflow on pathological inputs.
1195    (a / g).checked_mul(b)
1196}
1197
1198/// Perform integer row-reduction (Hermite Normal Form style) on the augmented
1199/// matrix `mat` whose first `n_c` columns form the coefficient part (C^T).
1200///
1201/// The remaining columns carry the accumulated row operations that map the
1202/// original identity to the transformation; null-space vectors are extracted
1203/// from those columns once the coefficient block is zeroed.
1204///
1205/// The elimination step for zeroing `mat[row][col]` given pivot at `mat[pivot_row][col]`:
1206/// ```text
1207/// g = gcd(|pivot_val|, |target|)
1208/// new_row = row * (pivot_val/g) - pivot_row * (target/g)
1209/// ```
1210/// This guarantees `new_row[col] = target*(pivot_val/g) - pivot_val*(target/g) = 0`.
1211fn integer_row_reduce(mat: &mut [Vec<i64>], n_c: usize) {
1212    let n_rows = mat.len();
1213    if n_rows == 0 || n_c == 0 {
1214        return;
1215    }
1216    let n_cols = mat[0].len();
1217    let mut pivot_row = 0usize;
1218
1219    for col in 0..n_c {
1220        // Find a non-zero entry in this column at or below `pivot_row`.
1221        let Some(found) = (pivot_row..n_rows).find(|&r| mat[r][col] != 0) else {
1222            continue;
1223        };
1224        mat.swap(pivot_row, found);
1225
1226        // Eliminate all other rows using the pivot.
1227        let pivot_val = mat[pivot_row][col];
1228        for row in 0..n_rows {
1229            if row == pivot_row {
1230                continue;
1231            }
1232            let target = mat[row][col];
1233            if target == 0 {
1234                continue;
1235            }
1236            // new_row = row * (pivot_val/g) - pivot_row * (target/g)
1237            // This yields new_row[col] = target*(pivot_val/g) - pivot_val*(target/g) = 0.
1238            let g = gcd(pivot_val.abs(), target.abs());
1239            let scale_row = pivot_val / g; // = pivot_val / gcd (signed)
1240            let scale_pivot = target / g; // = target    / gcd (signed)
1241
1242            // Compute the updated row in-place.
1243            // Use saturating arithmetic to guard against overflow on degenerate nets.
1244            let updated: Vec<i64> = (0..n_cols)
1245                .map(|c| {
1246                    mat[row][c]
1247                        .saturating_mul(scale_row)
1248                        .saturating_sub(mat[pivot_row][c].saturating_mul(scale_pivot))
1249                })
1250                .collect();
1251            mat[row] = updated;
1252
1253            // Reduce the row by its GCD to keep coefficients small.
1254            let row_gcd = mat[row].iter().fold(0i64, |acc, &v| gcd(acc, v.abs()));
1255            if row_gcd > 1 {
1256                for c in 0..n_cols {
1257                    mat[row][c] /= row_gcd;
1258                }
1259            }
1260        }
1261
1262        pivot_row += 1;
1263    }
1264}
1265
1266/// Compute all minimal non-negative T-invariants of the Petri net using
1267/// integer Gauss-Jordan null-space decomposition of the incidence matrix.
1268///
1269/// A T-invariant is a non-negative integer vector `x` with `C * x = 0`
1270/// (the net returns to its original marking after firing each transition
1271/// `x[t]` times).
1272///
1273/// The algorithm works by:
1274/// 1. Building the augmented matrix `[C^T | I]` where `C^T` has one row per
1275///    transition and one column per place.
1276/// 2. Performing integer row reduction on the `C^T` block.
1277/// 3. Extracting the identity-part rows where the `C^T` block is entirely zero;
1278///    these rows are the null-space basis vectors.
1279/// 4. Filtering to keep only non-trivial non-negative vectors.
1280pub fn t_invariants(net: &PetriNet) -> Vec<Vec<i64>> {
1281    let n_t = net.transitions.len();
1282    let n_p = net.places.len();
1283
1284    if n_t == 0 || n_p == 0 {
1285        return Vec::new();
1286    }
1287
1288    // Build augmented matrix [C^T | I_{n_t}].
1289    // Row t: C^T[t][p] = post(t,p) - pre(t,p), followed by identity columns.
1290    let mut mat: Vec<Vec<i64>> = (0..n_t)
1291        .map(|t| {
1292            let mut row: Vec<i64> = (0..n_p)
1293                .map(|p| {
1294                    let post: i64 = net
1295                        .arcs_out
1296                        .iter()
1297                        .filter(|&&(ti, pi, _)| ti == t && pi == p)
1298                        .map(|&(_, _, w)| w as i64)
1299                        .sum();
1300                    let pre: i64 = net
1301                        .arcs_in
1302                        .iter()
1303                        .filter(|&&(ti, pi, _)| ti == t && pi == p)
1304                        .map(|&(_, _, w)| w as i64)
1305                        .sum();
1306                    post - pre
1307                })
1308                .collect();
1309            // Append identity block.
1310            for j in 0..n_t {
1311                row.push(if j == t { 1 } else { 0 });
1312            }
1313            row
1314        })
1315        .collect();
1316
1317    // Row-reduce the C^T part (first n_p columns).
1318    integer_row_reduce(&mut mat, n_p);
1319
1320    // Extract null-space vectors: rows where the C^T block is all-zero.
1321    let mut invariants: Vec<Vec<i64>> = Vec::new();
1322    for row in &mat {
1323        let ct_all_zero = row[..n_p].iter().all(|&x| x == 0);
1324        if !ct_all_zero {
1325            continue;
1326        }
1327        let v: Vec<i64> = row[n_p..].to_vec();
1328
1329        // T-invariants require non-negative entries.  Due to the sign of the
1330        // pivot chosen during row reduction the vector may come out all-negative;
1331        // negate it to obtain the canonical non-negative representative.
1332        let has_positive = v.iter().any(|&x| x > 0);
1333        let has_negative = v.iter().any(|&x| x < 0);
1334
1335        let candidate: Vec<i64> = if has_positive && !has_negative {
1336            // Already non-negative — use as-is.
1337            v
1338        } else if has_negative && !has_positive {
1339            // All-negative — negate to get non-negative.
1340            v.iter().map(|&x| -x).collect()
1341        } else {
1342            // Mixed signs — not a non-negative T-invariant.
1343            continue;
1344        };
1345
1346        // Normalise by GCD to obtain a minimal representative.
1347        let g = candidate.iter().fold(0i64, |acc, &x| gcd(acc, x.abs()));
1348        let normalised: Vec<i64> = if g > 1 {
1349            candidate.iter().map(|&x| x / g).collect()
1350        } else {
1351            candidate
1352        };
1353        invariants.push(normalised);
1354    }
1355    invariants
1356}
1357
1358// ---------------------------------------------------------------------------
1359// Firing sequence analysis
1360// ---------------------------------------------------------------------------
1361
1362/// Attempt to replay a firing sequence from the current marking.
1363///
1364/// Returns `Ok(final_marking)` or `Err(step_index)` on failure.
1365pub fn replay_sequence(net: &mut PetriNet, sequence: &[usize]) -> Result<Vec<usize>, usize> {
1366    for (step, &t_idx) in sequence.iter().enumerate() {
1367        if net.fire(t_idx).is_err() {
1368            return Err(step);
1369        }
1370    }
1371    Ok(net.marking())
1372}
1373
1374// ---------------------------------------------------------------------------
1375// Petri net metrics
1376// ---------------------------------------------------------------------------
1377
1378/// Compute structural properties summary for a Petri net.
1379#[derive(Debug, Clone)]
1380pub struct PetriNetMetrics {
1381    /// Number of places.
1382    pub n_places: usize,
1383    /// Number of transitions.
1384    pub n_transitions: usize,
1385    /// Number of arcs (in + out).
1386    pub n_arcs: usize,
1387    /// Average out-degree of transitions.
1388    pub avg_out_degree: f64,
1389    /// Whether any transition has no input arcs (source transition).
1390    pub has_source_transition: bool,
1391    /// Whether any transition has no output arcs (sink transition).
1392    pub has_sink_transition: bool,
1393}
1394
1395/// Compute structural metrics for the given Petri net.
1396pub fn compute_metrics(net: &PetriNet) -> PetriNetMetrics {
1397    let n_transitions = net.transitions.len();
1398    let total_out: usize = net
1399        .transitions
1400        .iter()
1401        .enumerate()
1402        .map(|(t, _)| net.arcs_out.iter().filter(|&&(ti, _, _)| ti == t).count())
1403        .sum();
1404    let avg_out_degree = if n_transitions == 0 {
1405        0.0
1406    } else {
1407        total_out as f64 / n_transitions as f64
1408    };
1409    let has_source_transition =
1410        (0..n_transitions).any(|t| !net.arcs_in.iter().any(|&(ti, _, _)| ti == t));
1411    let has_sink_transition =
1412        (0..n_transitions).any(|t| !net.arcs_out.iter().any(|&(ti, _, _)| ti == t));
1413    PetriNetMetrics {
1414        n_places: net.places.len(),
1415        n_transitions,
1416        n_arcs: net.arcs_in.len() + net.arcs_out.len(),
1417        avg_out_degree,
1418        has_source_transition,
1419        has_sink_transition,
1420    }
1421}
1422
1423// ---------------------------------------------------------------------------
1424// Merging nets
1425// ---------------------------------------------------------------------------
1426
1427/// Merge two Petri nets by disjoint union (all places and transitions are
1428/// included; no arcs are shared).
1429pub fn disjoint_union(a: &PetriNet, b: &PetriNet) -> PetriNet {
1430    let mut result = a.clone();
1431    let place_offset = a.places.len();
1432    let trans_offset = a.transitions.len();
1433    for p in &b.places {
1434        result.places.push(p.clone());
1435    }
1436    for t in &b.transitions {
1437        result.transitions.push(t.clone());
1438    }
1439    for &(t, p, w) in &b.arcs_in {
1440        result.arcs_in.push((t + trans_offset, p + place_offset, w));
1441    }
1442    for &(t, p, w) in &b.arcs_out {
1443        result
1444            .arcs_out
1445            .push((t + trans_offset, p + place_offset, w));
1446    }
1447    result
1448}
1449
1450// ---------------------------------------------------------------------------
1451// Liveness analysis
1452// ---------------------------------------------------------------------------
1453
1454/// Check whether transition `t_idx` is live (can always eventually fire again
1455/// from any reachable marking).
1456///
1457/// This is a lightweight approximation: returns `true` if `t_idx` appears in
1458/// the enabled set of at least one reachable marking.
1459pub fn is_live_approx(net: &PetriNet, t_idx: usize) -> bool {
1460    let reachable = net.reachability_bfs();
1461    let mut sim = net.clone();
1462    for marking in &reachable {
1463        sim.set_marking(marking);
1464        if sim.enabled_transitions().contains(&t_idx) {
1465            return true;
1466        }
1467    }
1468    false
1469}
1470
1471// ---------------------------------------------------------------------------
1472// Shared-resource workflow example
1473// ---------------------------------------------------------------------------
1474
1475/// Build a workflow Petri net with a shared resource pool of size `capacity`.
1476pub fn workflow_with_resource(capacity: usize) -> PetriNet {
1477    let mut net = PetriNet::new();
1478    let start = net.add_place(Place::with_tokens("start", 1));
1479    let working = net.add_place(Place::with_tokens("working", 0));
1480    let done = net.add_place(Place::with_tokens("done", 0));
1481    let resource = net.add_place(Place::with_tokens("resource", capacity));
1482    let begin = net.add_transition(Transition::new("begin"));
1483    let finish = net.add_transition(Transition::new("finish"));
1484    net.add_arc_in(begin, start, 1);
1485    net.add_arc_in(begin, resource, 1);
1486    net.add_arc_out(begin, working, 1);
1487    net.add_arc_in(finish, working, 1);
1488    net.add_arc_out(finish, done, 1);
1489    net.add_arc_out(finish, resource, 1);
1490    net
1491}
1492
1493// ---------------------------------------------------------------------------
1494// HashMap-based marking for large nets
1495// ---------------------------------------------------------------------------
1496
1497/// Sparse marking: maps place names to token counts.
1498pub type SparseMarking = HashMap<String, usize>;
1499
1500/// Convert a dense marking vector (aligned with `net.places`) into a sparse map.
1501pub fn dense_to_sparse(net: &PetriNet, marking: &[usize]) -> SparseMarking {
1502    net.places
1503        .iter()
1504        .zip(marking.iter())
1505        .map(|(p, &tok)| (p.name.clone(), tok))
1506        .collect()
1507}
1508
1509/// Convert a sparse marking map back to a dense vector (missing places = 0).
1510pub fn sparse_to_dense(net: &PetriNet, sparse: &SparseMarking) -> Vec<usize> {
1511    net.places
1512        .iter()
1513        .map(|p| *sparse.get(&p.name).unwrap_or(&0))
1514        .collect()
1515}
1516
1517// ---------------------------------------------------------------------------
1518// Tests
1519// ---------------------------------------------------------------------------
1520
1521#[cfg(test)]
1522mod tests {
1523    use super::*;
1524
1525    fn simple_net() -> PetriNet {
1526        let mut net = PetriNet::new();
1527        let p0 = net.add_place(Place::with_tokens("p0", 1));
1528        let p1 = net.add_place(Place::with_tokens("p1", 0));
1529        let t0 = net.add_transition(Transition::new("t0"));
1530        net.add_arc_in(t0, p0, 1);
1531        net.add_arc_out(t0, p1, 1);
1532        net
1533    }
1534
1535    #[test]
1536    fn test_place_tokens() {
1537        let p = Place::with_tokens("p", 3);
1538        assert_eq!(p.tokens, 3);
1539    }
1540
1541    #[test]
1542    fn test_transition_name() {
1543        let t = Transition::new("t1");
1544        assert_eq!(t.name, "t1");
1545    }
1546
1547    #[test]
1548    fn test_add_place_transition() {
1549        let mut net = PetriNet::new();
1550        let pi = net.add_place(Place::new("p"));
1551        let ti = net.add_transition(Transition::new("t"));
1552        assert_eq!(pi, 0);
1553        assert_eq!(ti, 0);
1554    }
1555
1556    #[test]
1557    fn test_fire_basic() {
1558        let mut net = simple_net();
1559        assert_eq!(net.marking(), vec![1, 0]);
1560        net.fire(0).unwrap();
1561        assert_eq!(net.marking(), vec![0, 1]);
1562    }
1563
1564    #[test]
1565    fn test_fire_not_enabled() {
1566        let mut net = simple_net();
1567        net.fire(0).unwrap(); // consume the token
1568        assert!(net.fire(0).is_err());
1569    }
1570
1571    #[test]
1572    fn test_enabled_transitions_empty() {
1573        let mut net = simple_net();
1574        net.fire(0).unwrap();
1575        assert!(net.enabled_transitions().is_empty());
1576    }
1577
1578    #[test]
1579    fn test_is_deadlocked() {
1580        let mut net = simple_net();
1581        net.fire(0).unwrap();
1582        assert!(net.is_deadlocked());
1583    }
1584
1585    #[test]
1586    fn test_marking_sum() {
1587        let net = simple_net();
1588        assert_eq!(net.marking_sum(), 1);
1589    }
1590
1591    #[test]
1592    fn test_reachability_bfs() {
1593        let net = simple_net();
1594        let reachable = net.reachability_bfs();
1595        assert!(reachable.contains(&vec![1, 0]));
1596        assert!(reachable.contains(&vec![0, 1]));
1597        assert_eq!(reachable.len(), 2);
1598    }
1599
1600    #[test]
1601    fn test_mutex_net_reachability() {
1602        let net = mutex_net();
1603        let reachable = net.reachability_bfs();
1604        // Mutual exclusion: p1_cs and p2_cs should never both hold a token.
1605        for marking in &reachable {
1606            // places: resource=0, p1_idle=1, p2_idle=2, p1_cs=3, p2_cs=4
1607            assert!(
1608                !(marking[3] >= 1 && marking[4] >= 1),
1609                "mutual exclusion violated"
1610            );
1611        }
1612    }
1613
1614    #[test]
1615    fn test_producer_consumer_enabled() {
1616        let mut net = producer_consumer_net();
1617        // Initially buffer=0, consumer_ready=1; only produce is enabled.
1618        let enabled = net.enabled_transitions();
1619        assert_eq!(enabled.len(), 1); // only produce
1620        net.fire(enabled[0]).unwrap(); // produce a token
1621        assert_eq!(net.marking_sum(), 2); // buffer=1, consumer_ready=1
1622    }
1623
1624    #[test]
1625    fn test_coverability_tree_bounded() {
1626        let net = simple_net();
1627        let tree = CoverabilityTree::build(&net);
1628        assert!(tree.is_bounded());
1629    }
1630
1631    #[test]
1632    fn test_coverability_tree_unbounded() {
1633        // A net with a self-loop that infinitely produces tokens is unbounded.
1634        let mut net = PetriNet::new();
1635        let p0 = net.add_place(Place::with_tokens("p0", 1));
1636        let t0 = net.add_transition(Transition::new("t0"));
1637        // t0 consumes 1 from p0, produces 2.
1638        net.add_arc_in(t0, p0, 1);
1639        net.add_arc_out(t0, p0, 2);
1640        let tree = CoverabilityTree::build(&net);
1641        assert!(!tree.is_bounded());
1642    }
1643
1644    #[test]
1645    fn test_stochastic_simulate() {
1646        let mut net = PetriNet::new();
1647        let p0 = net.add_place(Place::with_tokens("p0", 3));
1648        let p1 = net.add_place(Place::with_tokens("p1", 0));
1649        let t0 = net.add_transition(Transition::new("t0"));
1650        net.add_arc_in(t0, p0, 1);
1651        net.add_arc_out(t0, p1, 1);
1652        let mut spn = StochasticPetriNet::new(net, vec![1.0]);
1653        let events = spn.simulate_until(10.0);
1654        // Should have fired exactly 3 times (all tokens consumed).
1655        assert_eq!(events.len(), 3);
1656    }
1657
1658    #[test]
1659    fn test_stochastic_steady_state() {
1660        let net = simple_net();
1661        let spn = StochasticPetriNet::new(net, vec![1.0]);
1662        let ss = spn.steady_state_estimate(100.0, 5);
1663        // Total tokens must be conserved in the mean.
1664        assert!((ss.iter().sum::<f64>() - 1.0).abs() < 1e-9);
1665    }
1666
1667    #[test]
1668    fn test_colored_place_tokens() {
1669        let mut p = ColoredPlace::new("p");
1670        p.add_token(ColoredToken::new(vec![1.0, 2.0]));
1671        assert_eq!(p.token_count(), 1);
1672        let tok = p.take_token().unwrap();
1673        assert_eq!(tok.color, vec![1.0, 2.0]);
1674        assert_eq!(p.token_count(), 0);
1675    }
1676
1677    #[test]
1678    fn test_colored_petri_net_fire() {
1679        let mut cpn = ColoredPetriNet::new();
1680        let p0 = cpn.add_place(ColoredPlace::new("p0"));
1681        let p1 = cpn.add_place(ColoredPlace::new("p1"));
1682        let t0 = cpn.add_transition(ColoredTransition::new("t0"));
1683        cpn.add_arc_in(t0, p0);
1684        cpn.add_arc_out(t0, p1);
1685        cpn.places[p0].add_token(ColoredToken::new(vec![1.0]));
1686        cpn.fire(t0).unwrap();
1687        assert_eq!(cpn.places[p0].token_count(), 0);
1688        assert_eq!(cpn.places[p1].token_count(), 1);
1689    }
1690
1691    #[test]
1692    fn test_parse_pnml_empty() {
1693        let net = parse_pnml("<pnml/>");
1694        assert!(net.places.is_empty());
1695    }
1696
1697    #[test]
1698    fn test_parse_pnml_places_and_transitions() {
1699        let xml = r#"<pnml>
1700  <net>
1701    <place id="p1" name="Place1"><initialMarking><text>2</text></initialMarking></place>
1702    <place id="p2" name="Place2"><initialMarking><text>0</text></initialMarking></place>
1703    <transition id="t1" name="Trans1"/>
1704    <arc id="a1" source="p1" target="t1"/>
1705    <arc id="a2" source="t1" target="p2"/>
1706  </net>
1707</pnml>"#;
1708        let net = parse_pnml(xml);
1709        assert_eq!(net.places.len(), 2, "should have 2 places");
1710        assert_eq!(net.transitions.len(), 1, "should have 1 transition");
1711        assert_eq!(net.arcs_in.len(), 1, "should have 1 input arc");
1712        assert_eq!(net.arcs_out.len(), 1, "should have 1 output arc");
1713        // Check initial marking: Place1 has 2 tokens.
1714        assert_eq!(net.places[0].tokens, 2, "Place1 should have 2 tokens");
1715        assert_eq!(net.places[1].tokens, 0, "Place2 should have 0 tokens");
1716    }
1717
1718    #[test]
1719    fn test_parse_pnml_fire_sequence() {
1720        // Build a net via PNML and verify it fires correctly.
1721        let xml = r#"<pnml>
1722  <net>
1723    <place id="p0" name="p0"><initialMarking><text>1</text></initialMarking></place>
1724    <place id="p1" name="p1"><initialMarking><text>0</text></initialMarking></place>
1725    <transition id="t0" name="t0"/>
1726    <arc id="a0" source="p0" target="t0"/>
1727    <arc id="a1" source="t0" target="p1"/>
1728  </net>
1729</pnml>"#;
1730        let mut net = parse_pnml(xml);
1731        assert_eq!(net.marking(), vec![1, 0]);
1732        net.fire(0).expect("t0 should be enabled");
1733        assert_eq!(net.marking(), vec![0, 1]);
1734    }
1735
1736    #[test]
1737    fn test_parse_pnml_arc_weight() {
1738        // Arc with explicit inscription weight.
1739        let xml = r#"<pnml>
1740  <net>
1741    <place id="p0" name="p0"><initialMarking><text>3</text></initialMarking></place>
1742    <place id="p1" name="p1"><initialMarking><text>0</text></initialMarking></place>
1743    <transition id="t0" name="t0"/>
1744    <arc id="a0" source="p0" target="t0"><inscription><text>2</text></inscription></arc>
1745    <arc id="a1" source="t0" target="p1"><inscription><text>1</text></inscription></arc>
1746  </net>
1747</pnml>"#;
1748        let mut net = parse_pnml(xml);
1749        // arc weight 2: need at least 2 tokens in p0 to fire.
1750        assert_eq!(net.marking(), vec![3, 0]);
1751        net.fire(0).expect("t0 should be enabled (3 >= 2)");
1752        assert_eq!(net.marking(), vec![1, 1]);
1753    }
1754
1755    #[test]
1756    fn test_incidence_matrix() {
1757        let net = simple_net();
1758        let m = incidence_matrix(&net);
1759        // t0: -1 for p0, +1 for p1
1760        assert_eq!(m[0][0], -1);
1761        assert_eq!(m[0][1], 1);
1762    }
1763
1764    #[test]
1765    fn test_state_equation() {
1766        let net = simple_net();
1767        let result = state_equation(&net, &[1]);
1768        assert_eq!(result, vec![0, 1]);
1769    }
1770
1771    #[test]
1772    fn test_is_trap() {
1773        // For the simple net: {p1} is a trap (nothing removes from it).
1774        let simple = simple_net();
1775        assert!(is_trap(&simple, &[1]));
1776    }
1777
1778    #[test]
1779    fn test_is_siphon() {
1780        let simple = simple_net();
1781        // {p0} is a siphon: nothing produces into p0.
1782        assert!(is_siphon(&simple, &[0]));
1783    }
1784
1785    #[test]
1786    fn test_find_t_invariant() {
1787        // A loop net: t0: p0 → p1, t1: p1 → p0 should have invariant [1,1].
1788        let mut net = PetriNet::new();
1789        let p0 = net.add_place(Place::with_tokens("p0", 1));
1790        let p1 = net.add_place(Place::with_tokens("p1", 0));
1791        let t0 = net.add_transition(Transition::new("t0"));
1792        let t1 = net.add_transition(Transition::new("t1"));
1793        net.add_arc_in(t0, p0, 1);
1794        net.add_arc_out(t0, p1, 1);
1795        net.add_arc_in(t1, p1, 1);
1796        net.add_arc_out(t1, p0, 1);
1797        let inv = find_t_invariant(&net, 2);
1798        assert!(inv.is_some());
1799    }
1800
1801    #[test]
1802    fn test_t_invariants_loop_net() {
1803        // Loop net t0: p0→p1, t1: p1→p0.  Null-space of C is spanned by [1,1].
1804        let mut net = PetriNet::new();
1805        let p0 = net.add_place(Place::with_tokens("p0", 1));
1806        let p1 = net.add_place(Place::with_tokens("p1", 0));
1807        let t0 = net.add_transition(Transition::new("t0"));
1808        let t1 = net.add_transition(Transition::new("t1"));
1809        net.add_arc_in(t0, p0, 1);
1810        net.add_arc_out(t0, p1, 1);
1811        net.add_arc_in(t1, p1, 1);
1812        net.add_arc_out(t1, p0, 1);
1813        let invs = t_invariants(&net);
1814        assert!(
1815            !invs.is_empty(),
1816            "loop net must have at least one T-invariant"
1817        );
1818        // The canonical minimal invariant is [1,1].
1819        assert!(
1820            invs.iter().any(|v| v == &[1i64, 1]),
1821            "expected [1,1] in invariants, got {:?}",
1822            invs
1823        );
1824    }
1825
1826    #[test]
1827    fn test_t_invariants_producer_consumer() {
1828        // producer_consumer_net has two transitions; a valid T-invariant
1829        // fires both equally so marking is restored.
1830        let net = producer_consumer_net();
1831        let invs = t_invariants(&net);
1832        // There should be at least one non-trivial invariant.
1833        assert!(
1834            !invs.is_empty(),
1835            "producer-consumer net should have T-invariants"
1836        );
1837        // Every invariant must satisfy C * x = 0.
1838        let mat = incidence_matrix(&net);
1839        for inv in &invs {
1840            let n_p = net.places.len();
1841            for p in 0..n_p {
1842                let sum: i64 = mat.iter().zip(inv.iter()).map(|(row, &x)| row[p] * x).sum();
1843                assert_eq!(
1844                    sum, 0,
1845                    "T-invariant {:?} must satisfy C*x=0, failed at place {}",
1846                    inv, p
1847                );
1848            }
1849        }
1850    }
1851
1852    #[test]
1853    fn test_t_invariants_empty_net() {
1854        let net = PetriNet::new();
1855        let invs = t_invariants(&net);
1856        assert!(invs.is_empty());
1857    }
1858
1859    #[test]
1860    fn test_t_invariants_no_cycle() {
1861        // Simple net with a single transition and no cycles: C has no null-space
1862        // vector with all non-negative entries (other than the zero vector).
1863        let net = simple_net();
1864        let invs = t_invariants(&net);
1865        // A linear chain has no non-trivial non-negative T-invariants.
1866        for inv in &invs {
1867            let mat = incidence_matrix(&net);
1868            let n_p = net.places.len();
1869            for p in 0..n_p {
1870                let sum: i64 = mat.iter().zip(inv.iter()).map(|(row, &x)| row[p] * x).sum();
1871                assert_eq!(sum, 0, "Invariant {:?} must satisfy C*x=0", inv);
1872            }
1873        }
1874    }
1875
1876    #[test]
1877    fn test_replay_sequence_ok() {
1878        let mut net = simple_net();
1879        let result = replay_sequence(&mut net, &[0]);
1880        assert_eq!(result, Ok(vec![0, 1]));
1881    }
1882
1883    #[test]
1884    fn test_replay_sequence_fail() {
1885        let mut net = simple_net();
1886        let result = replay_sequence(&mut net, &[0, 0]);
1887        assert_eq!(result, Err(1));
1888    }
1889
1890    #[test]
1891    fn test_compute_metrics() {
1892        let net = simple_net();
1893        let m = compute_metrics(&net);
1894        assert_eq!(m.n_places, 2);
1895        assert_eq!(m.n_transitions, 1);
1896        assert_eq!(m.n_arcs, 2);
1897    }
1898
1899    #[test]
1900    fn test_disjoint_union() {
1901        let a = simple_net();
1902        let b = simple_net();
1903        let u = disjoint_union(&a, &b);
1904        assert_eq!(u.places.len(), 4);
1905        assert_eq!(u.transitions.len(), 2);
1906    }
1907
1908    #[test]
1909    fn test_is_live_approx() {
1910        let net = simple_net();
1911        assert!(is_live_approx(&net, 0));
1912    }
1913
1914    #[test]
1915    fn test_workflow_net() {
1916        let net = workflow_with_resource(2);
1917        assert_eq!(net.places.len(), 4);
1918        assert_eq!(net.transitions.len(), 2);
1919    }
1920
1921    #[test]
1922    fn test_dense_sparse_roundtrip() {
1923        let net = simple_net();
1924        let marking = net.marking();
1925        let sparse = dense_to_sparse(&net, &marking);
1926        let dense = sparse_to_dense(&net, &sparse);
1927        assert_eq!(marking, dense);
1928    }
1929
1930    #[test]
1931    fn test_guarded_transition() {
1932        let mut net = PetriNet::new();
1933        let p0 = net.add_place(Place::with_tokens("p0", 2));
1934        let p1 = net.add_place(Place::with_tokens("p1", 0));
1935        // Only fire if p0 has at least 2 tokens.
1936        let t0 = net.add_transition(Transition::with_guard("t0", |tokens| {
1937            tokens.iter().all(|&t| t >= 2)
1938        }));
1939        net.add_arc_in(t0, p0, 1);
1940        net.add_arc_out(t0, p1, 1);
1941        let enabled = net.enabled_transitions();
1942        assert_eq!(enabled.len(), 1);
1943        net.fire(0).unwrap();
1944        // Now p0 has 1 token, guard should fail.
1945        assert!(net.enabled_transitions().is_empty());
1946    }
1947
1948    #[test]
1949    fn test_coverability_tree_node_count() {
1950        let net = simple_net();
1951        let tree = CoverabilityTree::build(&net);
1952        assert!(tree.node_count() >= 2);
1953    }
1954
1955    #[test]
1956    fn test_colored_transition_guard() {
1957        let mut cpn = ColoredPetriNet::new();
1958        let p0 = cpn.add_place(ColoredPlace::new("p0"));
1959        let p1 = cpn.add_place(ColoredPlace::new("p1"));
1960        let t0 = cpn.add_transition(ColoredTransition::with_guard("t0", |tok| {
1961            tok.color.first().copied().unwrap_or(0.0) > 0.5
1962        }));
1963        cpn.add_arc_in(t0, p0);
1964        cpn.add_arc_out(t0, p1);
1965        // Token with low value: guard should reject.
1966        cpn.places[p0].add_token(ColoredToken::new(vec![0.1]));
1967        assert!(cpn.enabled_transitions().is_empty());
1968        // Token with high value: guard should accept.
1969        cpn.places[p0].tokens.clear();
1970        cpn.places[p0].add_token(ColoredToken::new(vec![0.9]));
1971        assert_eq!(cpn.enabled_transitions(), vec![0]);
1972    }
1973}