1use crate::registry::{ConstructionError, FieldInfo, ProblemSchemaEntry};
14use crate::traits::Problem;
15use crate::types::Or;
16use serde::de::Error as _;
17use serde::{Deserialize, Deserializer, Serialize};
18use std::collections::{HashMap, HashSet, VecDeque};
19
20inventory::submit! {
21 ProblemSchemaEntry {
22 name: "NonLivenessFreePetriNet",
23 display_name: "Non-Liveness Free Petri Net",
24 aliases: &[],
25 dimensions: &[],
26 category: crate::registry::ProblemCategory::Misc,
27 module_path: module_path!(),
28 description: "Determine whether a free-choice Petri net is not live (some transition can become permanently dead)",
29 fields: &[
30 FieldInfo { name: "num_places", type_name: "usize", description: "Number of places |S|" },
31 FieldInfo { name: "num_transitions", type_name: "usize", description: "Number of transitions |T|" },
32 FieldInfo { name: "place_to_transition", type_name: "Vec<(usize,usize)>", description: "Arcs from places to transitions" },
33 FieldInfo { name: "transition_to_place", type_name: "Vec<(usize,usize)>", description: "Arcs from transitions to places" },
34 FieldInfo { name: "initial_marking", type_name: "Vec<i64>", description: "Initial marking M₀ (tokens per place)" },
35 ],
36 }
37}
38
39#[derive(Debug, Clone, Serialize)]
40pub struct NonLivenessFreePetriNet {
41 num_places: usize,
42 num_transitions: usize,
43 place_to_transition: Vec<(usize, usize)>,
44 transition_to_place: Vec<(usize, usize)>,
45 initial_marking: Vec<i64>,
46 #[serde(skip)]
48 globally_dead: Vec<bool>,
49}
50
51impl NonLivenessFreePetriNet {
52 fn validate_inputs(
53 num_places: usize,
54 num_transitions: usize,
55 place_to_transition: &[(usize, usize)],
56 transition_to_place: &[(usize, usize)],
57 initial_marking: &[i64],
58 ) -> Result<(), ConstructionError> {
59 if num_places == 0 {
60 return Err(ConstructionError::Conversion(
61 "NonLivenessFreePetriNet requires at least one place".into(),
62 ));
63 }
64 if num_transitions == 0 {
65 return Err(ConstructionError::Conversion(
66 "NonLivenessFreePetriNet requires at least one transition".into(),
67 ));
68 }
69 if initial_marking.len() != num_places {
70 return Err(ConstructionError::Conversion(format!(
71 "initial_marking length {} does not match num_places {}",
72 initial_marking.len(),
73 num_places
74 )));
75 }
76 if initial_marking.iter().any(|&tokens| tokens < 0) {
77 return Err(ConstructionError::Conversion(
78 "initial_marking must contain non-negative token counts".into(),
79 ));
80 }
81 let total_tokens = initial_marking
82 .iter()
83 .try_fold(0_i64, |total, &tokens| total.checked_add(tokens))
84 .ok_or_else(|| {
85 ConstructionError::IntegerOverflow("summing initial Petri-net tokens".into())
86 })?;
87 usize::try_from(total_tokens).map_err(|_| {
88 ConstructionError::IntegerOverflow(
89 "initial Petri-net token sum does not fit usize".into(),
90 )
91 })?;
92 for (i, &(p, t)) in place_to_transition.iter().enumerate() {
93 if p >= num_places {
94 return Err(ConstructionError::Conversion(format!(
95 "place_to_transition arc {} has place {} out of range 0..{}",
96 i, p, num_places
97 )));
98 }
99 if t >= num_transitions {
100 return Err(ConstructionError::Conversion(format!(
101 "place_to_transition arc {} has transition {} out of range 0..{}",
102 i, t, num_transitions
103 )));
104 }
105 }
106 for (i, &(t, p)) in transition_to_place.iter().enumerate() {
107 if t >= num_transitions {
108 return Err(ConstructionError::Conversion(format!(
109 "transition_to_place arc {} has transition {} out of range 0..{}",
110 i, t, num_transitions
111 )));
112 }
113 if p >= num_places {
114 return Err(ConstructionError::Conversion(format!(
115 "transition_to_place arc {} has place {} out of range 0..{}",
116 i, p, num_places
117 )));
118 }
119 }
120
121 let mut preset: HashMap<usize, HashSet<usize>> = HashMap::new();
124 for &(p, t) in place_to_transition {
125 preset.entry(t).or_default().insert(p);
126 }
127 for &(p, _) in place_to_transition {
129 let transitions_from_p: Vec<usize> = place_to_transition
130 .iter()
131 .filter(|&&(pp, _)| pp == p)
132 .map(|&(_, t)| t)
133 .collect();
134 for i in 0..transitions_from_p.len() {
135 for j in (i + 1)..transitions_from_p.len() {
136 let t1 = transitions_from_p[i];
137 let t2 = transitions_from_p[j];
138 let p1 = preset.get(&t1).cloned().unwrap_or_default();
139 let p2 = preset.get(&t2).cloned().unwrap_or_default();
140 if p1 != p2 {
141 return Err(ConstructionError::Conversion(format!(
142 "Free-choice violation: transitions {} and {} share input place {} but have different presets",
143 t1, t2, p
144 )));
145 }
146 }
147 }
148 }
149
150 Ok(())
151 }
152
153 pub fn new(
155 num_places: usize,
156 num_transitions: usize,
157 place_to_transition: Vec<(usize, usize)>,
158 transition_to_place: Vec<(usize, usize)>,
159 initial_marking: Vec<i64>,
160 ) -> Result<Self, ConstructionError> {
161 Self::validate_inputs(
162 num_places,
163 num_transitions,
164 &place_to_transition,
165 &transition_to_place,
166 &initial_marking,
167 )?;
168 let mut net = Self {
169 num_places,
170 num_transitions,
171 place_to_transition,
172 transition_to_place,
173 initial_marking,
174 globally_dead: Vec::new(),
175 };
176 net.globally_dead = net.compute_globally_dead_transitions()?;
177 Ok(net)
178 }
179
180 pub fn num_places(&self) -> usize {
182 self.num_places
183 }
184
185 pub fn num_transitions(&self) -> usize {
187 self.num_transitions
188 }
189
190 pub fn num_arcs(&self) -> usize {
192 self.place_to_transition.len() + self.transition_to_place.len()
193 }
194
195 pub fn initial_token_sum(&self) -> usize {
197 let total = self
198 .initial_marking
199 .iter()
200 .try_fold(0_i64, |total, &tokens| total.checked_add(tokens))
201 .expect("construction validates the initial token sum");
202 usize::try_from(total).expect("validated initial token sum fits usize")
203 }
204
205 pub fn place_to_transition(&self) -> &[(usize, usize)] {
207 &self.place_to_transition
208 }
209
210 pub fn transition_to_place(&self) -> &[(usize, usize)] {
212 &self.transition_to_place
213 }
214
215 pub fn initial_marking(&self) -> &[i64] {
217 &self.initial_marking
218 }
219
220 fn enabled_transitions(&self, marking: &[i64]) -> Vec<bool> {
222 let mut enabled = vec![true; self.num_transitions];
223 for &(p, t) in &self.place_to_transition {
226 if marking[p] == 0 {
227 enabled[t] = false;
228 }
229 }
230 enabled
231 }
232
233 fn fire(
235 &self,
236 marking: &[i64],
237 transition: usize,
238 ) -> Result<Option<Vec<i64>>, ConstructionError> {
239 let mut new_marking = marking.to_vec();
240 for &(p, t) in &self.place_to_transition {
242 if t == transition {
243 if new_marking[p] == 0 {
244 return Ok(None);
245 }
246 new_marking[p] -= 1;
247 }
248 }
249 for &(t, p) in &self.transition_to_place {
251 if t == transition {
252 new_marking[p] = new_marking[p].checked_add(1).ok_or_else(|| {
253 ConstructionError::IntegerOverflow(
254 "firing a Petri-net transition increments a token count".into(),
255 )
256 })?;
257 }
258 }
259 Ok(Some(new_marking))
260 }
261
262 fn compute_globally_dead_transitions(&self) -> Result<Vec<bool>, ConstructionError> {
270 let token_cap = self
271 .initial_marking
272 .iter()
273 .try_fold(0_i64, |total, &tokens| total.checked_add(tokens))
274 .expect("construction validates the initial token sum");
275 let num_t = self.num_transitions;
276
277 let mut marking_index: HashMap<Vec<i64>, usize> = HashMap::new();
279 let mut markings: Vec<Vec<i64>> = Vec::new();
280 let mut successors: Vec<Vec<(usize, usize)>> = Vec::new();
282 let mut queue: VecDeque<usize> = VecDeque::new();
283
284 let initial = self.initial_marking.clone();
285 marking_index.insert(initial.clone(), 0);
286 markings.push(initial);
287 successors.push(Vec::new());
288 queue.push_back(0);
289
290 while let Some(m_idx) = queue.pop_front() {
291 let enabled = self.enabled_transitions(&markings[m_idx]);
292 for (t, &is_enabled) in enabled.iter().enumerate() {
293 if !is_enabled {
294 continue;
295 }
296 if let Some(new_marking) = self.fire(&markings[m_idx], t)? {
297 if new_marking.iter().any(|&tokens| tokens > token_cap) {
299 continue;
300 }
301 let next_idx = if let Some(&idx) = marking_index.get(&new_marking) {
302 idx
303 } else {
304 let idx = markings.len();
305 marking_index.insert(new_marking.clone(), idx);
306 markings.push(new_marking);
307 successors.push(Vec::new());
308 queue.push_back(idx);
309 idx
310 };
311 successors[m_idx].push((t, next_idx));
312 }
313 }
314 }
315
316 let num_markings = markings.len();
317
318 let mut globally_dead = vec![false; num_t];
326
327 let mut predecessors: Vec<Vec<usize>> = vec![Vec::new(); num_markings];
329 for (m_idx, succs) in successors.iter().enumerate() {
330 for &(_tr, next_idx) in succs {
331 predecessors[next_idx].push(m_idx);
332 }
333 }
334
335 for (t, dead) in globally_dead.iter_mut().enumerate() {
336 let mut can_reach_t = vec![false; num_markings];
339 let mut bfs_queue: VecDeque<usize> = VecDeque::new();
340
341 for (m_idx, succs) in successors.iter().enumerate() {
342 if succs.iter().any(|&(tr, _)| tr == t) {
343 can_reach_t[m_idx] = true;
344 bfs_queue.push_back(m_idx);
345 }
346 }
347
348 while let Some(m_idx) = bfs_queue.pop_front() {
350 for &pred_idx in &predecessors[m_idx] {
351 if !can_reach_t[pred_idx] {
352 can_reach_t[pred_idx] = true;
353 bfs_queue.push_back(pred_idx);
354 }
355 }
356 }
357
358 if can_reach_t.iter().any(|&reached| !reached) {
360 *dead = true;
361 }
362 }
363
364 Ok(globally_dead)
365 }
366}
367
368#[derive(Deserialize)]
369struct NonLivenessFreePetriNetData {
370 num_places: usize,
371 num_transitions: usize,
372 place_to_transition: Vec<(usize, usize)>,
373 transition_to_place: Vec<(usize, usize)>,
374 initial_marking: Vec<i64>,
375}
376
377impl<'de> Deserialize<'de> for NonLivenessFreePetriNet {
378 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
379 where
380 D: Deserializer<'de>,
381 {
382 let data = NonLivenessFreePetriNetData::deserialize(deserializer)?;
383 Self::new(
384 data.num_places,
385 data.num_transitions,
386 data.place_to_transition,
387 data.transition_to_place,
388 data.initial_marking,
389 )
390 .map_err(D::Error::custom)
391 }
392}
393
394impl Problem for NonLivenessFreePetriNet {
395 const NAME: &'static str = "NonLivenessFreePetriNet";
396 type Solution = Vec<bool>;
397 type Value = Or;
398
399 crate::problem_parameters![
400 ("initial_token_sum", initial_token_sum),
401 ("num_arcs", num_arcs),
402 ("num_places", num_places),
403 ("num_transitions", num_transitions),
404 ];
405
406 fn variant() -> Vec<(&'static str, &'static str)> {
407 crate::variant_params![]
408 }
409
410 fn evaluate(&self, config: &Self::Solution) -> Result<Or, crate::traits::EvaluationError> {
411 Ok({
412 if config.len() != self.num_transitions {
413 return Err(crate::traits::EvaluationError::InvalidConfiguration(
414 "transition-selection length does not match the net".into(),
415 ));
416 }
417
418 for (t, &selected) in config.iter().enumerate() {
421 if selected && self.globally_dead[t] {
422 return Ok(Or(true));
423 }
424 }
425
426 Or(false)
427 })
428 }
429}
430
431impl crate::solvers::BruteForceProblem for NonLivenessFreePetriNet {
432 fn dimensions(&self) -> Vec<usize> {
433 vec![2; self.num_transitions]
434 }
435}
436
437crate::declare_variants! {
438 default NonLivenessFreePetriNet => "(initial_token_sum + 1) ^ num_places * num_transitions",
439}
440
441crate::register_brute_force! {
442 NonLivenessFreePetriNet decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
443}
444
445#[cfg(feature = "example-db")]
446pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
447 vec![crate::example_db::specs::ModelExampleSpec {
448 id: "non_liveness_free_petri_net",
449 instance: Box::new(
450 NonLivenessFreePetriNet::new(
451 4,
452 3,
453 vec![(0, 0), (1, 1), (2, 2)],
454 vec![(0, 1), (1, 2), (2, 3)],
455 vec![1, 0, 0, 0],
456 )
457 .unwrap(),
458 ),
459 optimal_config: serde_json::json!(vec![true, true, true]),
460 optimal_value: serde_json::json!(true),
461 }]
462}
463
464#[cfg(test)]
465#[path = "../../unit_tests/models/misc/non_liveness_free_petri_net.rs"]
466mod tests;