Skip to main content

problemreductions/models/misc/
minimum_fault_detection_test_set.rs

1//! Minimum Fault Detection Test Set problem implementation.
2//!
3//! Given a directed acyclic graph with designated input and output vertices,
4//! find the minimum set of input-output pairs whose coverage sets cover all
5//! internal vertices.
6
7use crate::registry::{FieldInfo, ProblemSchemaEntry};
8use crate::traits::Problem;
9use crate::types::Min;
10use serde::{Deserialize, Deserializer, Serialize};
11use std::collections::{HashSet, VecDeque};
12
13inventory::submit! {
14    ProblemSchemaEntry {
15        name: "MinimumFaultDetectionTestSet",
16        display_name: "Minimum Fault Detection Test Set",
17        aliases: &[],
18        dimensions: &[],
19        module_path: module_path!(),
20        description: "Find minimum set of input-output paths covering all internal DAG vertices",
21        fields: &[
22            FieldInfo { name: "num_vertices", type_name: "usize", description: "Number of vertices in the DAG" },
23            FieldInfo { name: "arcs", type_name: "Vec<(usize, usize)>", description: "Directed arcs (u, v)" },
24            FieldInfo { name: "inputs", type_name: "Vec<usize>", description: "Input vertex indices" },
25            FieldInfo { name: "outputs", type_name: "Vec<usize>", description: "Output vertex indices" },
26        ],
27    }
28}
29
30/// The Minimum Fault Detection Test Set problem.
31///
32/// Given a directed acyclic graph G = (V, A) with designated input vertices
33/// I ⊆ V and output vertices O ⊆ V, find the minimum number of input-output
34/// pairs (i, o) ∈ I × O such that the union of their coverage sets covers
35/// every internal vertex V \ (I ∪ O).
36///
37/// For a pair (i, o), the coverage set is the set of vertices reachable from i
38/// that can also reach o (i.e., vertices on some i-to-o path). Inputs and
39/// outputs themselves are not required to be covered.
40///
41/// The configuration space is binary over all |I| × |O| pairs.
42///
43/// # Example
44///
45/// ```
46/// use problemreductions::models::misc::MinimumFaultDetectionTestSet;
47/// use problemreductions::{Problem, Solver, BruteForce};
48///
49/// let problem = MinimumFaultDetectionTestSet::new(
50///     7,
51///     vec![(0,2),(0,3),(1,3),(1,4),(2,5),(3,5),(3,6),(4,6)],
52///     vec![0, 1],
53///     vec![5, 6],
54/// );
55/// let solver = BruteForce::new();
56/// use problemreductions::solvers::Solver as _;
57/// let optimal = solver.solve(&problem);
58/// assert_eq!(optimal, problemreductions::types::Min(Some(2)));
59/// ```
60#[derive(Debug, Clone, Serialize)]
61pub struct MinimumFaultDetectionTestSet {
62    /// Number of vertices.
63    num_vertices: usize,
64    /// Directed arcs (u, v).
65    arcs: Vec<(usize, usize)>,
66    /// Input vertex indices.
67    inputs: Vec<usize>,
68    /// Output vertex indices.
69    outputs: Vec<usize>,
70    /// Precomputed coverage sets for each (input_idx, output_idx) pair.
71    /// Indexed as coverage[i_idx * num_outputs + o_idx].
72    #[serde(skip)]
73    coverage: Vec<HashSet<usize>>,
74}
75
76#[derive(Deserialize)]
77struct MinimumFaultDetectionTestSetData {
78    num_vertices: usize,
79    arcs: Vec<(usize, usize)>,
80    inputs: Vec<usize>,
81    outputs: Vec<usize>,
82}
83
84impl<'de> Deserialize<'de> for MinimumFaultDetectionTestSet {
85    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
86    where
87        D: Deserializer<'de>,
88    {
89        let data = MinimumFaultDetectionTestSetData::deserialize(deserializer)?;
90        let coverage =
91            Self::build_coverage(data.num_vertices, &data.arcs, &data.inputs, &data.outputs);
92        Ok(Self {
93            num_vertices: data.num_vertices,
94            arcs: data.arcs,
95            inputs: data.inputs,
96            outputs: data.outputs,
97            coverage,
98        })
99    }
100}
101
102impl MinimumFaultDetectionTestSet {
103    /// Create a new Minimum Fault Detection Test Set instance.
104    ///
105    /// # Panics
106    ///
107    /// Panics if any arc index is out of bounds, if any input or output index
108    /// is out of bounds, or if inputs or outputs are empty.
109    pub fn new(
110        num_vertices: usize,
111        arcs: Vec<(usize, usize)>,
112        inputs: Vec<usize>,
113        outputs: Vec<usize>,
114    ) -> Self {
115        assert!(!inputs.is_empty(), "Inputs must not be empty");
116        assert!(!outputs.is_empty(), "Outputs must not be empty");
117        for (i, &(u, v)) in arcs.iter().enumerate() {
118            assert!(
119                u < num_vertices && v < num_vertices,
120                "Arc {} ({}, {}) out of bounds for {} vertices",
121                i,
122                u,
123                v,
124                num_vertices
125            );
126        }
127        for &inp in &inputs {
128            assert!(
129                inp < num_vertices,
130                "Input vertex {} out of bounds for {} vertices",
131                inp,
132                num_vertices
133            );
134        }
135        for &out in &outputs {
136            assert!(
137                out < num_vertices,
138                "Output vertex {} out of bounds for {} vertices",
139                out,
140                num_vertices
141            );
142        }
143        let coverage = Self::build_coverage(num_vertices, &arcs, &inputs, &outputs);
144        Self {
145            num_vertices,
146            arcs,
147            inputs,
148            outputs,
149            coverage,
150        }
151    }
152
153    /// Compute forward reachability from a given vertex using BFS on the DAG.
154    fn forward_reachable(num_vertices: usize, adj: &[Vec<usize>], start: usize) -> HashSet<usize> {
155        let mut visited = HashSet::new();
156        let mut queue = VecDeque::new();
157        visited.insert(start);
158        queue.push_back(start);
159        while let Some(v) = queue.pop_front() {
160            if v < adj.len() {
161                for &w in &adj[v] {
162                    if visited.insert(w) {
163                        queue.push_back(w);
164                    }
165                }
166            }
167        }
168        let _ = num_vertices; // used only to clarify signature
169        visited
170    }
171
172    /// Compute backward reachability from a given vertex using BFS on the reverse DAG.
173    fn backward_reachable(
174        num_vertices: usize,
175        rev_adj: &[Vec<usize>],
176        start: usize,
177    ) -> HashSet<usize> {
178        let mut visited = HashSet::new();
179        let mut queue = VecDeque::new();
180        visited.insert(start);
181        queue.push_back(start);
182        while let Some(v) = queue.pop_front() {
183            if v < rev_adj.len() {
184                for &w in &rev_adj[v] {
185                    if visited.insert(w) {
186                        queue.push_back(w);
187                    }
188                }
189            }
190        }
191        let _ = num_vertices;
192        visited
193    }
194
195    /// Build coverage sets for all input-output pairs.
196    fn build_coverage(
197        num_vertices: usize,
198        arcs: &[(usize, usize)],
199        inputs: &[usize],
200        outputs: &[usize],
201    ) -> Vec<HashSet<usize>> {
202        // Build adjacency lists
203        let mut adj = vec![vec![]; num_vertices];
204        let mut rev_adj = vec![vec![]; num_vertices];
205        for &(u, v) in arcs {
206            adj[u].push(v);
207            rev_adj[v].push(u);
208        }
209
210        // Precompute forward reachability from each input
211        let fwd: Vec<HashSet<usize>> = inputs
212            .iter()
213            .map(|&inp| Self::forward_reachable(num_vertices, &adj, inp))
214            .collect();
215
216        // Precompute backward reachability from each output
217        let bwd: Vec<HashSet<usize>> = outputs
218            .iter()
219            .map(|&out| Self::backward_reachable(num_vertices, &rev_adj, out))
220            .collect();
221
222        let num_outputs = outputs.len();
223        let mut coverage = Vec::with_capacity(inputs.len() * num_outputs);
224        for (i_idx, _) in inputs.iter().enumerate() {
225            for (o_idx, _) in outputs.iter().enumerate() {
226                // Coverage = vertices reachable from input i AND reachable backwards from output o
227                let cov: HashSet<usize> = fwd[i_idx].intersection(&bwd[o_idx]).copied().collect();
228                coverage.push(cov);
229            }
230        }
231        coverage
232    }
233
234    /// Get the number of vertices.
235    pub fn num_vertices(&self) -> usize {
236        self.num_vertices
237    }
238
239    /// Get the number of arcs.
240    pub fn num_arcs(&self) -> usize {
241        self.arcs.len()
242    }
243
244    /// Get the arcs.
245    pub fn arcs(&self) -> &[(usize, usize)] {
246        &self.arcs
247    }
248
249    /// Get the input vertices.
250    pub fn inputs(&self) -> &[usize] {
251        &self.inputs
252    }
253
254    /// Get the output vertices.
255    pub fn outputs(&self) -> &[usize] {
256        &self.outputs
257    }
258
259    /// Get the number of input vertices.
260    pub fn num_inputs(&self) -> usize {
261        self.inputs.len()
262    }
263
264    /// Get the number of output vertices.
265    pub fn num_outputs(&self) -> usize {
266        self.outputs.len()
267    }
268}
269
270impl Problem for MinimumFaultDetectionTestSet {
271    const NAME: &'static str = "MinimumFaultDetectionTestSet";
272    type Value = Min<usize>;
273
274    fn variant() -> Vec<(&'static str, &'static str)> {
275        crate::variant_params![]
276    }
277
278    fn dims(&self) -> Vec<usize> {
279        vec![2; self.inputs.len() * self.outputs.len()]
280    }
281
282    fn evaluate(&self, config: &[usize]) -> Min<usize> {
283        let num_pairs = self.inputs.len() * self.outputs.len();
284        if config.len() != num_pairs {
285            return Min(None);
286        }
287        if config.iter().any(|&c| c > 1) {
288            return Min(None);
289        }
290
291        let mut boundary = vec![false; self.num_vertices];
292        for &input in &self.inputs {
293            boundary[input] = true;
294        }
295        for &output in &self.outputs {
296            boundary[output] = true;
297        }
298        let required_internal_vertices =
299            boundary.iter().filter(|&&is_boundary| !is_boundary).count();
300
301        // Collect union of internal vertices covered by the selected pairs.
302        let mut covered: HashSet<usize> = HashSet::new();
303        let mut count = 0usize;
304        for (idx, &sel) in config.iter().enumerate() {
305            if sel == 1 {
306                count += 1;
307                covered.extend(
308                    self.coverage[idx]
309                        .iter()
310                        .copied()
311                        .filter(|&vertex| !boundary[vertex]),
312                );
313            }
314        }
315
316        // Check all internal vertices are covered.
317        if covered.len() == required_internal_vertices {
318            Min(Some(count))
319        } else {
320            Min(None)
321        }
322    }
323}
324
325crate::declare_variants! {
326    default MinimumFaultDetectionTestSet => "2^(num_inputs * num_outputs)",
327}
328
329#[cfg(feature = "example-db")]
330pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
331    // 7 vertices, inputs={0,1}, outputs={5,6}
332    // Internal vertices are {2,3,4}
333    // Arcs: (0,2),(0,3),(1,3),(1,4),(2,5),(3,5),(3,6),(4,6)
334    // Pairs: (0,5)->{0,2,3,5}, (0,6)->{0,3,6}, (1,5)->{1,3,5}, (1,6)->{1,3,4,6}
335    // Config [1,0,0,1]: select pairs (0,5) and (1,6) -> covers all internal vertices -> Min(2)
336    vec![crate::example_db::specs::ModelExampleSpec {
337        id: "minimum_fault_detection_test_set",
338        instance: Box::new(MinimumFaultDetectionTestSet::new(
339            7,
340            vec![
341                (0, 2),
342                (0, 3),
343                (1, 3),
344                (1, 4),
345                (2, 5),
346                (3, 5),
347                (3, 6),
348                (4, 6),
349            ],
350            vec![0, 1],
351            vec![5, 6],
352        )),
353        optimal_config: vec![1, 0, 0, 1],
354        optimal_value: serde_json::json!(2),
355    }]
356}
357
358#[cfg(test)]
359#[path = "../../unit_tests/models/misc/minimum_fault_detection_test_set.rs"]
360mod tests;