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::{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    /// Precomputed globally dead transitions (not serialized).
47    #[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        // Validate free-choice property: for any two transitions sharing an
122        // input place, they must share ALL input places (identical preset).
123        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        // Group transitions by shared input places
128        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    /// Create a new `NonLivenessFreePetriNet` instance.
154    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    /// Number of places |S|.
181    pub fn num_places(&self) -> usize {
182        self.num_places
183    }
184
185    /// Number of transitions |T|.
186    pub fn num_transitions(&self) -> usize {
187        self.num_transitions
188    }
189
190    /// Total number of arcs |F|.
191    pub fn num_arcs(&self) -> usize {
192        self.place_to_transition.len() + self.transition_to_place.len()
193    }
194
195    /// Sum of tokens in the initial marking.
196    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    /// Arcs from places to transitions.
206    pub fn place_to_transition(&self) -> &[(usize, usize)] {
207        &self.place_to_transition
208    }
209
210    /// Arcs from transitions to places.
211    pub fn transition_to_place(&self) -> &[(usize, usize)] {
212        &self.transition_to_place
213    }
214
215    /// Initial marking M₀.
216    pub fn initial_marking(&self) -> &[i64] {
217        &self.initial_marking
218    }
219
220    /// Determine which transitions are enabled at the given marking.
221    fn enabled_transitions(&self, marking: &[i64]) -> Vec<bool> {
222        let mut enabled = vec![true; self.num_transitions];
223        // A transition t is enabled iff every input place has at least one token.
224        // Transitions with no input places remain enabled (source transitions).
225        for &(p, t) in &self.place_to_transition {
226            if marking[p] == 0 {
227                enabled[t] = false;
228            }
229        }
230        enabled
231    }
232
233    /// Fire a transition, producing a new marking. Returns None if not enabled.
234    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        // Remove tokens from input places
241        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        // Add tokens to output places
250        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    /// Build the bounded reachability graph and determine which transitions
263    /// are globally dead (i.e., there exists a reachable marking from which
264    /// the transition can never fire again).
265    ///
266    /// For boundedness, we cap exploration at markings where no place exceeds
267    /// `initial_token_sum`. This is sound for free-choice nets under the
268    /// NP-completeness assumption from Garey & Johnson.
269    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        // Build reachability graph: BFS from initial marking.
278        let mut marking_index: HashMap<Vec<i64>, usize> = HashMap::new();
279        let mut markings: Vec<Vec<i64>> = Vec::new();
280        // successors[m_idx] = list of (transition, next_marking_idx)
281        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                    // Check bound: no place exceeds token_cap
298                    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        // For each transition t, find the set of markings from which t can
319        // eventually fire (via BFS on the reachability graph).
320        // A transition is globally dead iff there exists a reachable marking
321        // NOT in this set.
322        //
323        // We compute this by backward BFS: starting from markings where t fires,
324        // propagate backward through all transitions.
325        let mut globally_dead = vec![false; num_t];
326
327        // Build reverse adjacency once (shared across all transitions).
328        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            // Find markings where transition t is directly fired
337            // (i.e., markings that have an outgoing edge for transition t)
338            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            // Backward BFS: from which markings can we reach a marking where t fires?
349            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            // t is globally dead iff some reachable marking cannot reach a firing of t
359            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            // Config selects transitions claimed to be dead.
419            // Return true iff at least one selected transition is indeed globally dead.
420            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;