Skip to main content

problemreductions/models/misc/
non_liveness_free_petri_net.rs

1//! Non-Liveness Free Petri Net problem implementation.
2//!
3//! Given a free-choice Petri net P = (S, T, F, M₀), determine whether P is
4//! *not live*: does there exist a transition that can become permanently dead?
5//! A transition t is *dead from marking M* if t never fires in any firing
6//! sequence starting from M. The net is not live iff some transition is
7//! globally dead from some reachable marking.
8//!
9//! The configuration is a binary vector over transitions, selecting which
10//! transitions are claimed to be globally dead. The answer is YES (Or(true))
11//! when at least one selected transition is indeed globally dead.
12
13use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry};
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        module_path: module_path!(),
27        description: "Determine whether a free-choice Petri net is not live (some transition can become permanently dead)",
28        fields: &[
29            FieldInfo { name: "num_places", type_name: "usize", description: "Number of places |S|" },
30            FieldInfo { name: "num_transitions", type_name: "usize", description: "Number of transitions |T|" },
31            FieldInfo { name: "place_to_transition", type_name: "Vec<(usize,usize)>", description: "Arcs from places to transitions" },
32            FieldInfo { name: "transition_to_place", type_name: "Vec<(usize,usize)>", description: "Arcs from transitions to places" },
33            FieldInfo { name: "initial_marking", type_name: "Vec<usize>", description: "Initial marking M₀ (tokens per place)" },
34        ],
35    }
36}
37
38inventory::submit! {
39    ProblemSizeFieldEntry {
40        name: "NonLivenessFreePetriNet",
41        fields: &["num_places", "num_transitions", "num_arcs", "initial_token_sum"],
42    }
43}
44
45#[derive(Debug, Clone, Serialize)]
46pub struct NonLivenessFreePetriNet {
47    num_places: usize,
48    num_transitions: usize,
49    place_to_transition: Vec<(usize, usize)>,
50    transition_to_place: Vec<(usize, usize)>,
51    initial_marking: Vec<usize>,
52    /// Precomputed globally dead transitions (not serialized).
53    #[serde(skip)]
54    globally_dead: Vec<bool>,
55}
56
57impl NonLivenessFreePetriNet {
58    fn validate_inputs(
59        num_places: usize,
60        num_transitions: usize,
61        place_to_transition: &[(usize, usize)],
62        transition_to_place: &[(usize, usize)],
63        initial_marking: &[usize],
64    ) -> Result<(), String> {
65        if num_places == 0 {
66            return Err("NonLivenessFreePetriNet requires at least one place".to_string());
67        }
68        if num_transitions == 0 {
69            return Err("NonLivenessFreePetriNet requires at least one transition".to_string());
70        }
71        if initial_marking.len() != num_places {
72            return Err(format!(
73                "initial_marking length {} does not match num_places {}",
74                initial_marking.len(),
75                num_places
76            ));
77        }
78        for (i, &(p, t)) in place_to_transition.iter().enumerate() {
79            if p >= num_places {
80                return Err(format!(
81                    "place_to_transition arc {} has place {} out of range 0..{}",
82                    i, p, num_places
83                ));
84            }
85            if t >= num_transitions {
86                return Err(format!(
87                    "place_to_transition arc {} has transition {} out of range 0..{}",
88                    i, t, num_transitions
89                ));
90            }
91        }
92        for (i, &(t, p)) in transition_to_place.iter().enumerate() {
93            if t >= num_transitions {
94                return Err(format!(
95                    "transition_to_place arc {} has transition {} out of range 0..{}",
96                    i, t, num_transitions
97                ));
98            }
99            if p >= num_places {
100                return Err(format!(
101                    "transition_to_place arc {} has place {} out of range 0..{}",
102                    i, p, num_places
103                ));
104            }
105        }
106
107        // Validate free-choice property: for any two transitions sharing an
108        // input place, they must share ALL input places (identical preset).
109        let mut preset: HashMap<usize, HashSet<usize>> = HashMap::new();
110        for &(p, t) in place_to_transition {
111            preset.entry(t).or_default().insert(p);
112        }
113        // Group transitions by shared input places
114        for &(p, _) in place_to_transition {
115            let transitions_from_p: Vec<usize> = place_to_transition
116                .iter()
117                .filter(|&&(pp, _)| pp == p)
118                .map(|&(_, t)| t)
119                .collect();
120            for i in 0..transitions_from_p.len() {
121                for j in (i + 1)..transitions_from_p.len() {
122                    let t1 = transitions_from_p[i];
123                    let t2 = transitions_from_p[j];
124                    let p1 = preset.get(&t1).cloned().unwrap_or_default();
125                    let p2 = preset.get(&t2).cloned().unwrap_or_default();
126                    if p1 != p2 {
127                        return Err(format!(
128                            "Free-choice violation: transitions {} and {} share input place {} but have different presets",
129                            t1, t2, p
130                        ));
131                    }
132                }
133            }
134        }
135
136        Ok(())
137    }
138
139    /// Try to create a new `NonLivenessFreePetriNet` instance, returning an error
140    /// if validation fails.
141    pub fn try_new(
142        num_places: usize,
143        num_transitions: usize,
144        place_to_transition: Vec<(usize, usize)>,
145        transition_to_place: Vec<(usize, usize)>,
146        initial_marking: Vec<usize>,
147    ) -> Result<Self, String> {
148        Self::validate_inputs(
149            num_places,
150            num_transitions,
151            &place_to_transition,
152            &transition_to_place,
153            &initial_marking,
154        )?;
155        let mut net = Self {
156            num_places,
157            num_transitions,
158            place_to_transition,
159            transition_to_place,
160            initial_marking,
161            globally_dead: Vec::new(),
162        };
163        net.globally_dead = net.compute_globally_dead_transitions();
164        Ok(net)
165    }
166
167    /// Create a new `NonLivenessFreePetriNet` instance.
168    ///
169    /// # Panics
170    ///
171    /// Panics if validation fails (indices out of range, wrong marking length,
172    /// or free-choice violation).
173    pub fn new(
174        num_places: usize,
175        num_transitions: usize,
176        place_to_transition: Vec<(usize, usize)>,
177        transition_to_place: Vec<(usize, usize)>,
178        initial_marking: Vec<usize>,
179    ) -> Self {
180        Self::try_new(
181            num_places,
182            num_transitions,
183            place_to_transition,
184            transition_to_place,
185            initial_marking,
186        )
187        .unwrap_or_else(|message| panic!("{message}"))
188    }
189
190    /// Number of places |S|.
191    pub fn num_places(&self) -> usize {
192        self.num_places
193    }
194
195    /// Number of transitions |T|.
196    pub fn num_transitions(&self) -> usize {
197        self.num_transitions
198    }
199
200    /// Total number of arcs |F|.
201    pub fn num_arcs(&self) -> usize {
202        self.place_to_transition.len() + self.transition_to_place.len()
203    }
204
205    /// Sum of tokens in the initial marking.
206    pub fn initial_token_sum(&self) -> usize {
207        self.initial_marking.iter().sum()
208    }
209
210    /// Arcs from places to transitions.
211    pub fn place_to_transition(&self) -> &[(usize, usize)] {
212        &self.place_to_transition
213    }
214
215    /// Arcs from transitions to places.
216    pub fn transition_to_place(&self) -> &[(usize, usize)] {
217        &self.transition_to_place
218    }
219
220    /// Initial marking M₀.
221    pub fn initial_marking(&self) -> &[usize] {
222        &self.initial_marking
223    }
224
225    /// Determine which transitions are enabled at the given marking.
226    fn enabled_transitions(&self, marking: &[usize]) -> Vec<bool> {
227        let mut enabled = vec![true; self.num_transitions];
228        // A transition t is enabled iff every input place has at least one token.
229        // Transitions with no input places remain enabled (source transitions).
230        for &(p, t) in &self.place_to_transition {
231            if marking[p] == 0 {
232                enabled[t] = false;
233            }
234        }
235        enabled
236    }
237
238    /// Fire a transition, producing a new marking. Returns None if not enabled.
239    fn fire(&self, marking: &[usize], transition: usize) -> Option<Vec<usize>> {
240        let mut new_marking = marking.to_vec();
241        // Remove tokens from input places
242        for &(p, t) in &self.place_to_transition {
243            if t == transition {
244                if new_marking[p] == 0 {
245                    return None;
246                }
247                new_marking[p] -= 1;
248            }
249        }
250        // Add tokens to output places
251        for &(t, p) in &self.transition_to_place {
252            if t == transition {
253                new_marking[p] += 1;
254            }
255        }
256        Some(new_marking)
257    }
258
259    /// Build the bounded reachability graph and determine which transitions
260    /// are globally dead (i.e., there exists a reachable marking from which
261    /// the transition can never fire again).
262    ///
263    /// For boundedness, we cap exploration at markings where no place exceeds
264    /// `initial_token_sum`. This is sound for free-choice nets under the
265    /// NP-completeness assumption from Garey & Johnson.
266    fn compute_globally_dead_transitions(&self) -> Vec<bool> {
267        let token_cap = self.initial_token_sum();
268        let num_t = self.num_transitions;
269
270        // Build reachability graph: BFS from initial marking.
271        let mut marking_index: HashMap<Vec<usize>, usize> = HashMap::new();
272        let mut markings: Vec<Vec<usize>> = Vec::new();
273        // successors[m_idx] = list of (transition, next_marking_idx)
274        let mut successors: Vec<Vec<(usize, usize)>> = Vec::new();
275        let mut queue: VecDeque<usize> = VecDeque::new();
276
277        let initial = self.initial_marking.clone();
278        marking_index.insert(initial.clone(), 0);
279        markings.push(initial);
280        successors.push(Vec::new());
281        queue.push_back(0);
282
283        while let Some(m_idx) = queue.pop_front() {
284            let enabled = self.enabled_transitions(&markings[m_idx]);
285            for (t, &is_enabled) in enabled.iter().enumerate() {
286                if !is_enabled {
287                    continue;
288                }
289                if let Some(new_marking) = self.fire(&markings[m_idx], t) {
290                    // Check bound: no place exceeds token_cap
291                    if new_marking.iter().any(|&tokens| tokens > token_cap) {
292                        continue;
293                    }
294                    let next_idx = if let Some(&idx) = marking_index.get(&new_marking) {
295                        idx
296                    } else {
297                        let idx = markings.len();
298                        marking_index.insert(new_marking.clone(), idx);
299                        markings.push(new_marking);
300                        successors.push(Vec::new());
301                        queue.push_back(idx);
302                        idx
303                    };
304                    successors[m_idx].push((t, next_idx));
305                }
306            }
307        }
308
309        let num_markings = markings.len();
310
311        // For each transition t, find the set of markings from which t can
312        // eventually fire (via BFS on the reachability graph).
313        // A transition is globally dead iff there exists a reachable marking
314        // NOT in this set.
315        //
316        // We compute this by backward BFS: starting from markings where t fires,
317        // propagate backward through all transitions.
318        let mut globally_dead = vec![false; num_t];
319
320        // Build reverse adjacency once (shared across all transitions).
321        let mut predecessors: Vec<Vec<usize>> = vec![Vec::new(); num_markings];
322        for (m_idx, succs) in successors.iter().enumerate() {
323            for &(_tr, next_idx) in succs {
324                predecessors[next_idx].push(m_idx);
325            }
326        }
327
328        for (t, dead) in globally_dead.iter_mut().enumerate() {
329            // Find markings where transition t is directly fired
330            // (i.e., markings that have an outgoing edge for transition t)
331            let mut can_reach_t = vec![false; num_markings];
332            let mut bfs_queue: VecDeque<usize> = VecDeque::new();
333
334            for (m_idx, succs) in successors.iter().enumerate() {
335                if succs.iter().any(|&(tr, _)| tr == t) {
336                    can_reach_t[m_idx] = true;
337                    bfs_queue.push_back(m_idx);
338                }
339            }
340
341            // Backward BFS: from which markings can we reach a marking where t fires?
342            while let Some(m_idx) = bfs_queue.pop_front() {
343                for &pred_idx in &predecessors[m_idx] {
344                    if !can_reach_t[pred_idx] {
345                        can_reach_t[pred_idx] = true;
346                        bfs_queue.push_back(pred_idx);
347                    }
348                }
349            }
350
351            // t is globally dead iff some reachable marking cannot reach a firing of t
352            if can_reach_t.iter().any(|&reached| !reached) {
353                *dead = true;
354            }
355        }
356
357        globally_dead
358    }
359}
360
361#[derive(Deserialize)]
362struct NonLivenessFreePetriNetData {
363    num_places: usize,
364    num_transitions: usize,
365    place_to_transition: Vec<(usize, usize)>,
366    transition_to_place: Vec<(usize, usize)>,
367    initial_marking: Vec<usize>,
368}
369
370impl<'de> Deserialize<'de> for NonLivenessFreePetriNet {
371    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
372    where
373        D: Deserializer<'de>,
374    {
375        let data = NonLivenessFreePetriNetData::deserialize(deserializer)?;
376        Self::try_new(
377            data.num_places,
378            data.num_transitions,
379            data.place_to_transition,
380            data.transition_to_place,
381            data.initial_marking,
382        )
383        .map_err(D::Error::custom)
384    }
385}
386
387impl Problem for NonLivenessFreePetriNet {
388    const NAME: &'static str = "NonLivenessFreePetriNet";
389    type Value = Or;
390
391    fn variant() -> Vec<(&'static str, &'static str)> {
392        crate::variant_params![]
393    }
394
395    fn dims(&self) -> Vec<usize> {
396        vec![2; self.num_transitions]
397    }
398
399    fn evaluate(&self, config: &[usize]) -> Or {
400        if config.len() != self.num_transitions {
401            return Or(false);
402        }
403
404        // Config selects transitions claimed to be dead.
405        // Return true iff at least one selected transition is indeed globally dead.
406        for (t, &selected) in config.iter().enumerate() {
407            if selected == 1 && self.globally_dead[t] {
408                return Or(true);
409            }
410        }
411
412        Or(false)
413    }
414}
415
416crate::declare_variants! {
417    default NonLivenessFreePetriNet => "(initial_token_sum + 1) ^ num_places * num_transitions",
418}
419
420#[cfg(feature = "example-db")]
421pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
422    vec![crate::example_db::specs::ModelExampleSpec {
423        id: "non_liveness_free_petri_net",
424        instance: Box::new(NonLivenessFreePetriNet::new(
425            4,
426            3,
427            vec![(0, 0), (1, 1), (2, 2)],
428            vec![(0, 1), (1, 2), (2, 3)],
429            vec![1, 0, 0, 0],
430        )),
431        optimal_config: vec![1, 1, 1],
432        optimal_value: serde_json::json!(true),
433    }]
434}
435
436#[cfg(test)]
437#[path = "../../unit_tests/models/misc/non_liveness_free_petri_net.rs"]
438mod tests;