1use std::collections::{HashMap, HashSet, VecDeque};
2
3use libpetri_core::input::{self, In};
4use libpetri_core::output::enumerate_branches;
5use libpetri_core::petri_net::PetriNet;
6
7use crate::dbm::Dbm;
8use crate::environment::EnvironmentAnalysisMode;
9use crate::marking_state::{MarkingState, MarkingStateBuilder};
10use crate::state_class::StateClass;
11
12#[derive(Debug, Clone)]
14pub struct StateClassEdge {
15 pub from: usize,
16 pub to: usize,
17 pub transition_name: String,
18 pub branch_index: usize,
19}
20
21#[derive(Debug)]
25pub struct StateClassGraph {
26 classes: Vec<StateClass>,
27 edges: Vec<StateClassEdge>,
28 successors: HashMap<usize, Vec<usize>>,
29 predecessors: HashMap<usize, Vec<usize>>,
30 complete: bool,
31}
32
33impl StateClassGraph {
34 pub fn new() -> Self {
35 Self {
36 classes: Vec::new(),
37 edges: Vec::new(),
38 successors: HashMap::new(),
39 predecessors: HashMap::new(),
40 complete: true,
41 }
42 }
43
44 pub fn build(net: &PetriNet, initial_marking: &MarkingState, max_classes: usize) -> Self {
46 Self::build_with_env(
47 net,
48 initial_marking,
49 max_classes,
50 &[],
51 &EnvironmentAnalysisMode::Ignore,
52 )
53 }
54
55 pub fn build_with_env(
57 net: &PetriNet,
58 initial_marking: &MarkingState,
59 max_classes: usize,
60 env_places: &[&str],
61 env_mode: &EnvironmentAnalysisMode,
62 ) -> Self {
63 let env_set: HashSet<&str> = env_places.iter().copied().collect();
64 let enabled = find_enabled_transitions(net, initial_marking, &env_set, env_mode);
65 let clock_names: Vec<String> = enabled.clone();
66 let lower_bounds: Vec<f64> = enabled
67 .iter()
68 .map(|name| {
69 let t = net
70 .transitions()
71 .iter()
72 .find(|t| t.name() == name.as_str())
73 .unwrap();
74 t.timing().earliest() as f64 / 1000.0
75 })
76 .collect();
77 let upper_bounds: Vec<f64> = enabled
78 .iter()
79 .map(|name| {
80 let t = net
81 .transitions()
82 .iter()
83 .find(|t| t.name() == name.as_str())
84 .unwrap();
85 t.timing().latest() as f64 / 1000.0
86 })
87 .collect();
88
89 let initial_dbm = Dbm::create(clock_names, &lower_bounds, &upper_bounds);
90 let initial_dbm = initial_dbm.let_time_pass();
91 let initial_class = StateClass::new(initial_marking.clone(), initial_dbm, enabled);
92
93 let mut graph = StateClassGraph::new();
94 let initial_key = initial_class.canonical_key();
95
96 graph.classes.push(initial_class);
97 let mut class_keys: HashMap<String, usize> = HashMap::new();
98 class_keys.insert(initial_key, 0);
99 graph.successors.insert(0, Vec::new());
100 graph.predecessors.insert(0, Vec::new());
101
102 let mut queue: VecDeque<usize> = VecDeque::new();
103 queue.push_back(0);
104
105 while let Some(current_idx) = queue.pop_front() {
106 if graph.classes.len() >= max_classes {
107 graph.complete = false;
108 break;
109 }
110
111 let current = graph.classes[current_idx].clone();
112
113 for (clock_idx, transition_name) in current.enabled_transitions.iter().enumerate() {
114 let transition = net
115 .transitions()
116 .iter()
117 .find(|t| t.name() == transition_name.as_str())
118 .unwrap();
119
120 let virtual_transitions = expand_transition(transition);
122
123 for (branch_index, output_places) in virtual_transitions {
124 let successor = compute_successor(
125 net,
126 ¤t,
127 clock_idx,
128 transition_name,
129 &output_places,
130 &env_set,
131 env_mode,
132 );
133
134 if successor.is_empty() {
135 continue;
136 }
137
138 let key = successor.canonical_key();
139
140 let target_idx = if let Some(&existing_idx) = class_keys.get(&key) {
141 existing_idx
142 } else {
143 let idx = graph.classes.len();
144 class_keys.insert(key, idx);
145 graph.classes.push(successor);
146 graph.successors.insert(idx, Vec::new());
147 graph.predecessors.insert(idx, Vec::new());
148 queue.push_back(idx);
149 idx
150 };
151
152 graph.edges.push(StateClassEdge {
153 from: current_idx,
154 to: target_idx,
155 transition_name: transition_name.clone(),
156 branch_index,
157 });
158 graph
159 .successors
160 .entry(current_idx)
161 .or_default()
162 .push(target_idx);
163 graph
164 .predecessors
165 .entry(target_idx)
166 .or_default()
167 .push(current_idx);
168 }
169 }
170 }
171
172 graph
173 }
174
175 pub fn class_count(&self) -> usize {
177 self.classes.len()
178 }
179
180 pub fn classes(&self) -> &[StateClass] {
182 &self.classes
183 }
184
185 pub fn edges(&self) -> &[StateClassEdge] {
187 &self.edges
188 }
189
190 pub fn is_complete(&self) -> bool {
192 self.complete
193 }
194
195 pub fn successors(&self, class_idx: usize) -> &[usize] {
197 self.successors
198 .get(&class_idx)
199 .map_or(&[], |v| v.as_slice())
200 }
201
202 pub fn predecessors(&self, class_idx: usize) -> &[usize] {
204 self.predecessors
205 .get(&class_idx)
206 .map_or(&[], |v| v.as_slice())
207 }
208
209 pub fn is_reachable(&self, marking: &MarkingState) -> bool {
211 self.classes.iter().any(|sc| sc.marking == *marking)
212 }
213
214 pub fn reachable_markings(&self) -> Vec<&MarkingState> {
216 let mut seen = HashSet::new();
217 let mut result = Vec::new();
218 for sc in &self.classes {
219 let key = sc.marking.canonical_key();
220 if seen.insert(key) {
221 result.push(&sc.marking);
222 }
223 }
224 result
225 }
226
227 pub fn edge_count(&self) -> usize {
229 self.edges.len()
230 }
231
232 pub fn enabled_transitions(&self, class_idx: usize) -> &[String] {
234 &self.classes[class_idx].enabled_transitions
235 }
236
237 pub fn branch_edges(&self, class_idx: usize, transition_name: &str) -> Vec<&StateClassEdge> {
239 self.edges
240 .iter()
241 .filter(|e| e.from == class_idx && e.transition_name == transition_name)
242 .collect()
243 }
244}
245
246impl Default for StateClassGraph {
247 fn default() -> Self {
248 Self::new()
249 }
250}
251
252fn find_enabled_transitions(
255 net: &PetriNet,
256 marking: &MarkingState,
257 env_places: &HashSet<&str>,
258 env_mode: &EnvironmentAnalysisMode,
259) -> Vec<String> {
260 let mut enabled = Vec::new();
261 for t in net.transitions() {
262 if is_enabled(t, marking, env_places, env_mode) {
263 enabled.push(t.name().to_string());
264 }
265 }
266 enabled
267}
268
269fn is_enabled(
270 transition: &libpetri_core::transition::Transition,
271 marking: &MarkingState,
272 env_places: &HashSet<&str>,
273 env_mode: &EnvironmentAnalysisMode,
274) -> bool {
275 for spec in transition.input_specs() {
276 let place_name = spec.place_name();
277 let is_env = env_places.contains(place_name);
278
279 if is_env {
280 match env_mode {
281 EnvironmentAnalysisMode::AlwaysAvailable => continue, EnvironmentAnalysisMode::Bounded { max_tokens } => {
283 let required = input::required_count(spec);
285 let available = marking.count(place_name) + max_tokens;
286 if available < required {
287 return false;
288 }
289 }
290 EnvironmentAnalysisMode::Ignore => {
291 let required = input::required_count(spec);
292 if marking.count(place_name) < required {
293 return false;
294 }
295 }
296 }
297 } else {
298 let required = input::required_count(spec);
299 if marking.count(place_name) < required {
300 return false;
301 }
302 }
303 }
304 for arc in transition.reads() {
305 if marking.count(arc.place.name()) < 1 {
306 return false;
307 }
308 }
309 for arc in transition.inhibitors() {
310 if marking.count(arc.place.name()) > 0 {
311 return false;
312 }
313 }
314 true
315}
316
317fn expand_transition(
318 transition: &libpetri_core::transition::Transition,
319) -> Vec<(usize, HashSet<String>)> {
320 if let Some(out_spec) = transition.output_spec() {
321 let branches = enumerate_branches(out_spec);
322 if branches.len() <= 1 {
323 let places: HashSet<String> = branches
324 .into_iter()
325 .flat_map(|b| b.into_iter().map(|p| p.name().to_string()))
326 .collect();
327 vec![(0, places)]
328 } else {
329 branches
330 .into_iter()
331 .enumerate()
332 .map(|(i, b)| {
333 let places: HashSet<String> =
334 b.into_iter().map(|p| p.name().to_string()).collect();
335 (i, places)
336 })
337 .collect()
338 }
339 } else {
340 vec![(0, HashSet::new())]
341 }
342}
343
344fn compute_successor(
345 net: &PetriNet,
346 current: &StateClass,
347 fired_clock: usize,
348 fired_name: &str,
349 output_places: &HashSet<String>,
350 env_places: &HashSet<&str>,
351 env_mode: &EnvironmentAnalysisMode,
352) -> StateClass {
353 let transition = net
354 .transitions()
355 .iter()
356 .find(|t| t.name() == fired_name)
357 .unwrap();
358
359 let new_marking = fire_transition_marking(
361 ¤t.marking,
362 transition,
363 output_places,
364 env_places,
365 env_mode,
366 );
367
368 let new_enabled_all = find_enabled_transitions(net, &new_marking, env_places, env_mode);
370
371 let mut persistent = Vec::new();
372 let mut persistent_indices = Vec::new();
373 for (i, name) in current.enabled_transitions.iter().enumerate() {
374 if name != fired_name && new_enabled_all.contains(name) {
375 persistent.push(name.clone());
376 persistent_indices.push(i);
377 }
378 }
379
380 let persistent_set: HashSet<&String> = persistent.iter().collect();
381 let newly_enabled: Vec<String> = new_enabled_all
382 .into_iter()
383 .filter(|n| !persistent_set.contains(n))
384 .collect();
385
386 let new_lower_bounds: Vec<f64> = newly_enabled
388 .iter()
389 .map(|name| {
390 let t = net
391 .transitions()
392 .iter()
393 .find(|t| t.name() == name.as_str())
394 .unwrap();
395 t.timing().earliest() as f64 / 1000.0
396 })
397 .collect();
398 let new_upper_bounds: Vec<f64> = newly_enabled
399 .iter()
400 .map(|name| {
401 let t = net
402 .transitions()
403 .iter()
404 .find(|t| t.name() == name.as_str())
405 .unwrap();
406 t.timing().latest() as f64 / 1000.0
407 })
408 .collect();
409
410 let new_dbm = current.dbm.fire_transition(
411 fired_clock,
412 &newly_enabled,
413 &new_lower_bounds,
414 &new_upper_bounds,
415 &persistent_indices,
416 );
417 let new_dbm = new_dbm.let_time_pass();
418
419 let mut all_enabled = persistent;
420 all_enabled.extend(newly_enabled);
421
422 StateClass::new(new_marking, new_dbm, all_enabled)
423}
424
425fn fire_transition_marking(
426 marking: &MarkingState,
427 transition: &libpetri_core::transition::Transition,
428 output_places: &HashSet<String>,
429 env_places: &HashSet<&str>,
430 env_mode: &EnvironmentAnalysisMode,
431) -> MarkingState {
432 let mut builder = MarkingStateBuilder::new();
433
434 for (place, count) in marking.places() {
436 builder = builder.tokens(place, count);
437 }
438
439 for spec in transition.input_specs() {
441 let place_name = spec.place_name();
442 let is_env = env_places.contains(place_name);
443
444 if is_env && *env_mode != EnvironmentAnalysisMode::Ignore {
445 continue;
447 }
448
449 let to_consume = input_consume_count(spec);
450 let current = marking.count(place_name);
451 let remaining = current.saturating_sub(to_consume);
452 builder = builder.tokens(place_name, remaining);
453 }
454
455 for arc in transition.resets() {
457 builder = builder.tokens(arc.place.name(), 0);
458 }
459
460 let result = builder.build();
461
462 let mut output_builder = MarkingStateBuilder::new();
464 for (place, count) in result.places() {
465 output_builder = output_builder.tokens(place, count);
466 }
467 for place in output_places {
468 let current = result.count(place);
469 output_builder = output_builder.tokens(place.as_str(), current + 1);
470 }
471
472 output_builder.build()
473}
474
475fn input_consume_count(spec: &In) -> usize {
476 match spec {
477 In::One { .. } => 1,
478 In::Exactly { count, .. } => *count,
479 In::All { .. } => 1, In::AtLeast { minimum, .. } => *minimum,
481 }
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487 use libpetri_core::input::one;
488 use libpetri_core::output::out_place;
489 use libpetri_core::place::Place;
490 use libpetri_core::transition::Transition;
491
492 #[test]
493 fn build_simple_chain() {
494 let p1 = Place::<i32>::new("p1");
495 let p2 = Place::<i32>::new("p2");
496 let t = Transition::builder("t1")
497 .input(one(&p1))
498 .output(out_place(&p2))
499 .build();
500 let net = PetriNet::builder("chain").transition(t).build();
501
502 let initial = MarkingStateBuilder::new().tokens("p1", 1).build();
503 let scg = StateClassGraph::build(&net, &initial, 1000);
504
505 assert!(scg.is_complete());
506 assert!(scg.class_count() >= 2); assert!(!scg.edges().is_empty());
508
509 let target = MarkingStateBuilder::new().tokens("p2", 1).build();
511 assert!(scg.is_reachable(&target));
512 }
513
514 #[test]
515 fn build_cycle() {
516 let p1 = Place::<i32>::new("p1");
517 let p2 = Place::<i32>::new("p2");
518 let t1 = Transition::builder("t1")
519 .input(one(&p1))
520 .output(out_place(&p2))
521 .build();
522 let t2 = Transition::builder("t2")
523 .input(one(&p2))
524 .output(out_place(&p1))
525 .build();
526 let net = PetriNet::builder("cycle").transitions([t1, t2]).build();
527
528 let initial = MarkingStateBuilder::new().tokens("p1", 1).build();
529 let scg = StateClassGraph::build(&net, &initial, 1000);
530
531 assert!(scg.is_complete());
532 assert_eq!(scg.reachable_markings().len(), 2);
534 }
535
536 #[test]
537 fn build_truncated() {
538 let p1 = Place::<i32>::new("p1");
539 let p2 = Place::<i32>::new("p2");
540 let t1 = Transition::builder("t1")
541 .input(one(&p1))
542 .output(out_place(&p2))
543 .build();
544 let t2 = Transition::builder("t2")
545 .input(one(&p2))
546 .output(out_place(&p1))
547 .build();
548 let net = PetriNet::builder("cycle").transitions([t1, t2]).build();
549
550 let initial = MarkingStateBuilder::new().tokens("p1", 1).build();
551 let scg = StateClassGraph::build(&net, &initial, 1); assert!(!scg.is_complete());
554 }
555
556 #[test]
557 fn build_no_enabled_transitions() {
558 let p1 = Place::<i32>::new("p1");
559 let p2 = Place::<i32>::new("p2");
560 let t = Transition::builder("t1")
561 .input(one(&p1))
562 .output(out_place(&p2))
563 .build();
564 let net = PetriNet::builder("test").transition(t).build();
565
566 let initial = MarkingState::new();
568 let scg = StateClassGraph::build(&net, &initial, 1000);
569
570 assert!(scg.is_complete());
571 assert_eq!(scg.class_count(), 1); assert!(scg.edges().is_empty());
573 }
574
575 #[test]
576 fn build_fork_net() {
577 let p = Place::<i32>::new("p");
578 let a = Place::<i32>::new("a");
579 let b = Place::<i32>::new("b");
580
581 let t = Transition::builder("t1")
582 .input(one(&p))
583 .output(libpetri_core::output::and(vec![
584 out_place(&a),
585 out_place(&b),
586 ]))
587 .build();
588 let net = PetriNet::builder("fork").transition(t).build();
589
590 let initial = MarkingStateBuilder::new().tokens("p", 1).build();
591 let scg = StateClassGraph::build(&net, &initial, 1000);
592
593 let target = MarkingStateBuilder::new()
594 .tokens("a", 1)
595 .tokens("b", 1)
596 .build();
597 assert!(scg.is_reachable(&target));
598 }
599
600 #[test]
601 fn build_xor_net() {
602 let p = Place::<i32>::new("p");
603 let a = Place::<i32>::new("a");
604 let b = Place::<i32>::new("b");
605
606 let t = Transition::builder("t1")
607 .input(one(&p))
608 .output(libpetri_core::output::xor(vec![
609 out_place(&a),
610 out_place(&b),
611 ]))
612 .build();
613 let net = PetriNet::builder("xor").transition(t).build();
614
615 let initial = MarkingStateBuilder::new().tokens("p", 1).build();
616 let scg = StateClassGraph::build(&net, &initial, 1000);
617
618 let target_a = MarkingStateBuilder::new().tokens("a", 1).build();
620 let target_b = MarkingStateBuilder::new().tokens("b", 1).build();
621 assert!(scg.is_reachable(&target_a));
622 assert!(scg.is_reachable(&target_b));
623 }
624
625 #[test]
626 fn build_inhibitor_net() {
627 let p1 = Place::<i32>::new("p1");
628 let p2 = Place::<i32>::new("p2");
629 let p_inh = Place::<i32>::new("inh");
630
631 let t = Transition::builder("t1")
632 .input(one(&p1))
633 .inhibitor(libpetri_core::arc::inhibitor(&p_inh))
634 .output(out_place(&p2))
635 .build();
636 let net = PetriNet::builder("test").transition(t).build();
637
638 let initial_blocked = MarkingStateBuilder::new()
640 .tokens("p1", 1)
641 .tokens("inh", 1)
642 .build();
643 let scg = StateClassGraph::build(&net, &initial_blocked, 1000);
644 assert_eq!(scg.class_count(), 1); assert!(scg.edges().is_empty());
646
647 let initial_free = MarkingStateBuilder::new().tokens("p1", 1).build();
649 let scg2 = StateClassGraph::build(&net, &initial_free, 1000);
650 assert!(scg2.class_count() >= 2);
651 let target = MarkingStateBuilder::new().tokens("p2", 1).build();
652 assert!(scg2.is_reachable(&target));
653 }
654
655 #[test]
656 fn build_with_timing_constraints() {
657 let p1 = Place::<i32>::new("p1");
658 let p2 = Place::<i32>::new("p2");
659 let p3 = Place::<i32>::new("p3");
660
661 let t1 = Transition::builder("t1")
662 .input(one(&p1))
663 .output(out_place(&p2))
664 .timing(libpetri_core::timing::delayed(100))
665 .build();
666 let t2 = Transition::builder("t2")
667 .input(one(&p2))
668 .output(out_place(&p3))
669 .timing(libpetri_core::timing::window(50, 200))
670 .build();
671
672 let net = PetriNet::builder("timed").transitions([t1, t2]).build();
673
674 let initial = MarkingStateBuilder::new().tokens("p1", 1).build();
675 let scg = StateClassGraph::build(&net, &initial, 1000);
676
677 assert!(scg.is_complete());
678 let target = MarkingStateBuilder::new().tokens("p3", 1).build();
679 assert!(scg.is_reachable(&target));
680
681 let initial_class = &scg.classes()[0];
683 assert!(!initial_class.dbm.is_empty());
684 }
685
686 #[test]
687 fn build_concurrent_transitions() {
688 let p1 = Place::<i32>::new("p1");
690 let p2 = Place::<i32>::new("p2");
691 let out_a = Place::<i32>::new("a");
692 let out_b = Place::<i32>::new("b");
693
694 let t1 = Transition::builder("t1")
695 .input(one(&p1))
696 .output(out_place(&out_a))
697 .timing(libpetri_core::timing::delayed(100))
698 .build();
699 let t2 = Transition::builder("t2")
700 .input(one(&p2))
701 .output(out_place(&out_b))
702 .timing(libpetri_core::timing::delayed(200))
703 .build();
704
705 let net = PetriNet::builder("concurrent")
706 .transitions([t1, t2])
707 .build();
708
709 let initial = MarkingStateBuilder::new()
710 .tokens("p1", 1)
711 .tokens("p2", 1)
712 .build();
713 let scg = StateClassGraph::build(&net, &initial, 1000);
714
715 assert!(scg.is_complete());
716 let target = MarkingStateBuilder::new()
718 .tokens("a", 1)
719 .tokens("b", 1)
720 .build();
721 assert!(scg.is_reachable(&target));
722 }
723
724 #[test]
725 fn build_env_always_available() {
726 let p_env = Place::<i32>::new("env");
727 let p_ready = Place::<i32>::new("ready");
728 let p_out = Place::<i32>::new("out");
729
730 let t = Transition::builder("t1")
731 .input(one(&p_env))
732 .input(one(&p_ready))
733 .output(out_place(&p_out))
734 .build();
735
736 let net = PetriNet::builder("env").transition(t).build();
737
738 let initial = MarkingStateBuilder::new().tokens("ready", 1).build();
739
740 let scg_no_env = StateClassGraph::build(&net, &initial, 100);
742 assert_eq!(scg_no_env.class_count(), 1);
743
744 let scg_env = StateClassGraph::build_with_env(
746 &net,
747 &initial,
748 100,
749 &["env"],
750 &EnvironmentAnalysisMode::AlwaysAvailable,
751 );
752 assert!(scg_env.class_count() >= 2);
753 let target = MarkingStateBuilder::new().tokens("out", 1).build();
754 assert!(scg_env.is_reachable(&target));
755 }
756
757 #[test]
758 fn build_env_always_available_cycles() {
759 let p_env = Place::<i32>::new("env");
761 let p_ready = Place::<i32>::new("ready");
762 let p_out = Place::<i32>::new("out");
763
764 let t1 = Transition::builder("process")
765 .input(one(&p_env))
766 .input(one(&p_ready))
767 .output(out_place(&p_out))
768 .build();
769 let t2 = Transition::builder("reset")
770 .input(one(&p_out))
771 .output(out_place(&p_ready))
772 .build();
773
774 let net = PetriNet::builder("env-cycle").transitions([t1, t2]).build();
775
776 let initial = MarkingStateBuilder::new().tokens("ready", 1).build();
777
778 let scg = StateClassGraph::build_with_env(
779 &net,
780 &initial,
781 100,
782 &["env"],
783 &EnvironmentAnalysisMode::AlwaysAvailable,
784 );
785
786 assert!(scg.is_complete());
787 assert!(scg.class_count() >= 2);
789 let target = MarkingStateBuilder::new().tokens("out", 1).build();
790 assert!(scg.is_reachable(&target));
791 }
792
793 #[test]
794 fn edge_count_matches_edges() {
795 let p1 = Place::<i32>::new("p1");
796 let p2 = Place::<i32>::new("p2");
797 let t = Transition::builder("t1")
798 .input(one(&p1))
799 .output(out_place(&p2))
800 .build();
801 let net = PetriNet::builder("test").transition(t).build();
802
803 let initial = MarkingStateBuilder::new().tokens("p1", 1).build();
804 let scg = StateClassGraph::build(&net, &initial, 1000);
805
806 assert_eq!(scg.edge_count(), scg.edges().len());
807 }
808
809 #[test]
810 fn enabled_transitions_returns_names() {
811 let p1 = Place::<i32>::new("p1");
812 let p2 = Place::<i32>::new("p2");
813 let t = Transition::builder("t1")
814 .input(one(&p1))
815 .output(out_place(&p2))
816 .build();
817 let net = PetriNet::builder("test").transition(t).build();
818
819 let initial = MarkingStateBuilder::new().tokens("p1", 1).build();
820 let scg = StateClassGraph::build(&net, &initial, 1000);
821
822 let enabled = scg.enabled_transitions(0);
823 assert!(enabled.contains(&"t1".to_string()));
824 }
825
826 #[test]
827 fn branch_edges_for_xor() {
828 let p = Place::<i32>::new("p");
829 let a = Place::<i32>::new("a");
830 let b = Place::<i32>::new("b");
831
832 let t = Transition::builder("t1")
833 .input(one(&p))
834 .output(libpetri_core::output::xor(vec![
835 out_place(&a),
836 out_place(&b),
837 ]))
838 .build();
839 let net = PetriNet::builder("xor").transition(t).build();
840
841 let initial = MarkingStateBuilder::new().tokens("p", 1).build();
842 let scg = StateClassGraph::build(&net, &initial, 1000);
843
844 let edges = scg.branch_edges(0, "t1");
845 assert_eq!(edges.len(), 2); let branch_indices: HashSet<usize> = edges.iter().map(|e| e.branch_index).collect();
847 assert!(branch_indices.contains(&0));
848 assert!(branch_indices.contains(&1));
849 }
850
851 #[test]
852 fn predecessors_and_successors() {
853 let p1 = Place::<i32>::new("p1");
854 let p2 = Place::<i32>::new("p2");
855 let t = Transition::builder("t1")
856 .input(one(&p1))
857 .output(out_place(&p2))
858 .build();
859 let net = PetriNet::builder("test").transition(t).build();
860
861 let initial = MarkingStateBuilder::new().tokens("p1", 1).build();
862 let scg = StateClassGraph::build(&net, &initial, 1000);
863
864 assert!(!scg.successors(0).is_empty());
866 let succ = scg.successors(0)[0];
868 assert!(scg.predecessors(succ).contains(&0));
869 }
870}