1#![allow(clippy::needless_range_loop)]
2#![allow(dead_code)]
12
13use std::collections::{HashMap, HashSet, VecDeque};
14
15#[derive(Debug, Clone)]
21pub struct Place {
22 pub name: String,
24 pub tokens: usize,
26}
27
28impl Place {
29 pub fn new(name: impl Into<String>) -> Self {
31 Self {
32 name: name.into(),
33 tokens: 0,
34 }
35 }
36
37 pub fn with_tokens(name: impl Into<String>, tokens: usize) -> Self {
39 Self {
40 name: name.into(),
41 tokens,
42 }
43 }
44}
45
46#[derive(Clone)]
53pub struct Transition {
54 pub name: String,
56 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 pub fn new(name: impl Into<String>) -> Self {
74 Self {
75 name: name.into(),
76 guard: None,
77 }
78 }
79
80 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
89pub type Weight = usize;
95
96#[derive(Debug, Clone)]
104pub struct PetriNet {
105 pub places: Vec<Place>,
107 pub transitions: Vec<Transition>,
109 pub arcs_in: Vec<(usize, usize, Weight)>,
111 pub arcs_out: Vec<(usize, usize, Weight)>,
113}
114
115impl PetriNet {
116 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 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 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 pub fn add_arc_in(&mut self, t: usize, p: usize, weight: Weight) {
142 self.arcs_in.push((t, p, weight));
143 }
144
145 pub fn add_arc_out(&mut self, t: usize, p: usize, weight: Weight) {
147 self.arcs_out.push((t, p, weight));
148 }
149
150 pub fn marking(&self) -> Vec<usize> {
152 self.places.iter().map(|p| p.tokens).collect()
153 }
154
155 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 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 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 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 for &(t, p, w) in &self.arcs_in {
212 if t == t_idx {
213 self.places[p].tokens -= w;
214 }
215 }
216 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 pub fn is_deadlocked(&self) -> bool {
227 self.enabled_transitions().is_empty()
228 }
229
230 pub fn marking_sum(&self) -> usize {
232 self.places.iter().map(|p| p.tokens).sum()
233 }
234
235 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 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
270pub const OMEGA: usize = usize::MAX;
277
278#[derive(Debug, Clone)]
280pub struct CoverabilityNode {
281 pub marking: Vec<usize>,
283 pub parent: Option<usize>,
285 pub fired_transition: Option<usize>,
287 pub children: Vec<usize>,
289}
290
291#[derive(Debug)]
295pub struct CoverabilityTree {
296 pub nodes: Vec<CoverabilityNode>,
298}
299
300impl CoverabilityTree {
301 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 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(¤t_marking);
322
323 for t_idx in sim.enabled_transitions() {
324 let mut next_sim = sim.clone();
325 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 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 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 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 pub fn is_bounded(&self) -> bool {
381 self.nodes
382 .iter()
383 .all(|n| n.marking.iter().all(|&t| t != OMEGA))
384 }
385
386 pub fn node_count(&self) -> usize {
388 self.nodes.len()
389 }
390}
391
392#[derive(Debug, Clone)]
399pub struct StochasticPetriNet {
400 pub net: PetriNet,
402 pub rates: Vec<f64>,
404}
405
406impl StochasticPetriNet {
407 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 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 let total_rate: f64 = enabled.iter().map(|&i| self.rates[i]).sum();
434 if total_rate == 0.0 {
435 break;
436 }
437 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 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 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; 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#[derive(Debug, Clone, PartialEq)]
488pub struct ColoredToken {
489 pub color: Vec<f64>,
491}
492
493impl ColoredToken {
494 pub fn new(color: Vec<f64>) -> Self {
496 Self { color }
497 }
498}
499
500#[derive(Debug, Clone)]
502pub struct ColoredPlace {
503 pub name: String,
505 pub tokens: Vec<ColoredToken>,
507}
508
509impl ColoredPlace {
510 pub fn new(name: impl Into<String>) -> Self {
512 Self {
513 name: name.into(),
514 tokens: Vec::new(),
515 }
516 }
517
518 pub fn add_token(&mut self, token: ColoredToken) {
520 self.tokens.push(token);
521 }
522
523 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 pub fn token_count(&self) -> usize {
534 self.tokens.len()
535 }
536}
537
538#[derive(Debug, Clone)]
540pub struct ColoredTransition {
541 pub name: String,
543 pub color_guard: Option<fn(&ColoredToken) -> bool>,
545}
546
547impl ColoredTransition {
548 pub fn new(name: impl Into<String>) -> Self {
550 Self {
551 name: name.into(),
552 color_guard: None,
553 }
554 }
555
556 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#[derive(Debug, Clone)]
567pub struct ColoredPetriNet {
568 pub places: Vec<ColoredPlace>,
570 pub transitions: Vec<ColoredTransition>,
572 pub arcs_in: Vec<(usize, usize)>,
574 pub arcs_out: Vec<(usize, usize)>,
576}
577
578impl ColoredPetriNet {
579 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 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 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 pub fn add_arc_in(&mut self, t: usize, p: usize) {
605 self.arcs_in.push((t, p));
606 }
607
608 pub fn add_arc_out(&mut self, t: usize, p: usize) {
610 self.arcs_out.push((t, p));
611 }
612
613 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 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 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 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 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
675fn 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
692fn 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
700pub fn parse_pnml(input: &str) -> PetriNet {
713 let mut net = PetriNet::new();
714
715 let mut place_id_map: HashMap<String, usize> = HashMap::new();
717 let mut trans_id_map: HashMap<String, usize> = HashMap::new();
718
719 #[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 #[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 let Some(rel_open) = input[pos..].find('<') else {
753 break;
754 };
755 let tag_open = pos + rel_open;
756
757 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 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 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 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 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 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 _ => {} }
952 }
953
954 net
955}
956
957fn 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 (Some(&p_idx), Some(&t_idx)) => net.add_arc_in(t_idx, p_idx, weight),
972 _ => {
973 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
983pub 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
1004pub 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
1033pub 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
1053pub 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
1070pub 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 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
1095pub 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
1115pub 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 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 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
1172fn 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
1188fn 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 (a / g).checked_mul(b)
1196}
1197
1198fn 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 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 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 let g = gcd(pivot_val.abs(), target.abs());
1239 let scale_row = pivot_val / g; let scale_pivot = target / g; 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 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
1266pub 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 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 for j in 0..n_t {
1311 row.push(if j == t { 1 } else { 0 });
1312 }
1313 row
1314 })
1315 .collect();
1316
1317 integer_row_reduce(&mut mat, n_p);
1319
1320 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 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 v
1338 } else if has_negative && !has_positive {
1339 v.iter().map(|&x| -x).collect()
1341 } else {
1342 continue;
1344 };
1345
1346 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
1358pub 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#[derive(Debug, Clone)]
1380pub struct PetriNetMetrics {
1381 pub n_places: usize,
1383 pub n_transitions: usize,
1385 pub n_arcs: usize,
1387 pub avg_out_degree: f64,
1389 pub has_source_transition: bool,
1391 pub has_sink_transition: bool,
1393}
1394
1395pub 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
1423pub 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
1450pub 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
1471pub 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
1493pub type SparseMarking = HashMap<String, usize>;
1499
1500pub 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
1509pub 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#[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(); 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 for marking in &reachable {
1606 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 let enabled = net.enabled_transitions();
1619 assert_eq!(enabled.len(), 1); net.fire(enabled[0]).unwrap(); assert_eq!(net.marking_sum(), 2); }
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 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 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 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 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 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 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 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 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 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 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 assert!(is_siphon(&simple, &[0]));
1783 }
1784
1785 #[test]
1786 fn test_find_t_invariant() {
1787 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 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 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 let net = producer_consumer_net();
1831 let invs = t_invariants(&net);
1832 assert!(
1834 !invs.is_empty(),
1835 "producer-consumer net should have T-invariants"
1836 );
1837 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 let net = simple_net();
1864 let invs = t_invariants(&net);
1865 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 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 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 cpn.places[p0].add_token(ColoredToken::new(vec![0.1]));
1967 assert!(cpn.enabled_transitions().is_empty());
1968 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}