Skip to main content

problemreductions/models/misc/
ensemble_computation.rs

1//! Ensemble Computation problem implementation.
2
3use crate::registry::{FieldInfo, ProblemSchemaEntry};
4use crate::traits::Problem;
5use crate::types::Min;
6use serde::{Deserialize, Serialize};
7
8inventory::submit! {
9    ProblemSchemaEntry {
10        name: "EnsembleComputation",
11        display_name: "Ensemble Computation",
12        aliases: &[],
13        dimensions: &[],
14        module_path: module_path!(),
15        description: "Find the minimum-length sequence of disjoint unions that builds all required subsets",
16        fields: &[
17            FieldInfo { name: "universe_size", type_name: "usize", description: "Number of elements in the universe A" },
18            FieldInfo { name: "subsets", type_name: "Vec<Vec<usize>>", description: "Required subsets that must appear among the computed z_i values" },
19            FieldInfo { name: "budget", type_name: "usize", description: "Maximum number of union operations J" },
20        ],
21    }
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[serde(try_from = "EnsembleComputationDef")]
26pub struct EnsembleComputation {
27    universe_size: usize,
28    subsets: Vec<Vec<usize>>,
29    budget: usize,
30}
31
32impl EnsembleComputation {
33    pub fn new(universe_size: usize, subsets: Vec<Vec<usize>>, budget: usize) -> Self {
34        Self::try_new(universe_size, subsets, budget).unwrap_or_else(|err| panic!("{err}"))
35    }
36
37    /// Create with an automatically derived search-space bound.
38    ///
39    /// The default budget is the sum of all subset sizes (worst-case without
40    /// intermediate-set reuse). This is always sufficient for the optimal
41    /// solution to fit within the search space.
42    pub fn with_default_budget(universe_size: usize, subsets: Vec<Vec<usize>>) -> Self {
43        let budget = Self::default_budget(&subsets);
44        Self::new(universe_size, subsets, budget)
45    }
46
47    /// Compute a default search-space bound from the subsets.
48    ///
49    /// Returns the sum of all subset sizes, clamped to at least 1.
50    pub fn default_budget(subsets: &[Vec<usize>]) -> usize {
51        subsets.iter().map(|s| s.len()).sum::<usize>().max(1)
52    }
53
54    pub fn try_new(
55        universe_size: usize,
56        subsets: Vec<Vec<usize>>,
57        budget: usize,
58    ) -> Result<Self, String> {
59        if budget == 0 {
60            return Err("budget must be positive".to_string());
61        }
62        let subsets = subsets
63            .into_iter()
64            .enumerate()
65            .map(|(subset_index, subset)| {
66                Self::normalize_subset(universe_size, subset).ok_or_else(|| {
67                    format!(
68                        "subset {subset_index} contains element outside universe of size {universe_size}"
69                    )
70                })
71            })
72            .collect::<Result<Vec<_>, _>>()?;
73        Ok(Self {
74            universe_size,
75            subsets,
76            budget,
77        })
78    }
79
80    pub fn universe_size(&self) -> usize {
81        self.universe_size
82    }
83
84    pub fn subsets(&self) -> &[Vec<usize>] {
85        &self.subsets
86    }
87
88    pub fn num_subsets(&self) -> usize {
89        self.subsets.len()
90    }
91
92    pub fn budget(&self) -> usize {
93        self.budget
94    }
95
96    fn normalize_subset(universe_size: usize, mut subset: Vec<usize>) -> Option<Vec<usize>> {
97        if subset.iter().any(|&element| element >= universe_size) {
98            return None;
99        }
100        subset.sort_unstable();
101        subset.dedup();
102        Some(subset)
103    }
104
105    fn decode_operand(&self, operand: usize, computed: &[Vec<usize>]) -> Option<Vec<usize>> {
106        if operand < self.universe_size {
107            return Some(vec![operand]);
108        }
109        computed.get(operand - self.universe_size).cloned()
110    }
111
112    fn are_disjoint(left: &[usize], right: &[usize]) -> bool {
113        let mut i = 0;
114        let mut j = 0;
115
116        while i < left.len() && j < right.len() {
117            match left[i].cmp(&right[j]) {
118                std::cmp::Ordering::Less => i += 1,
119                std::cmp::Ordering::Greater => j += 1,
120                std::cmp::Ordering::Equal => return false,
121            }
122        }
123
124        true
125    }
126
127    fn union_disjoint(left: &[usize], right: &[usize]) -> Vec<usize> {
128        let mut union = Vec::with_capacity(left.len() + right.len());
129        let mut i = 0;
130        let mut j = 0;
131
132        while i < left.len() && j < right.len() {
133            if left[i] < right[j] {
134                union.push(left[i]);
135                i += 1;
136            } else {
137                union.push(right[j]);
138                j += 1;
139            }
140        }
141
142        union.extend_from_slice(&left[i..]);
143        union.extend_from_slice(&right[j..]);
144        union
145    }
146
147    fn required_subsets(&self) -> Option<Vec<Vec<usize>>> {
148        self.subsets
149            .iter()
150            .cloned()
151            .map(|subset| Self::normalize_subset(self.universe_size, subset))
152            .collect()
153    }
154
155    fn all_required_subsets_present(
156        required_subsets: &[Vec<usize>],
157        computed: &[Vec<usize>],
158    ) -> bool {
159        required_subsets
160            .iter()
161            .all(|subset| computed.iter().any(|candidate| candidate == subset))
162    }
163}
164
165impl Problem for EnsembleComputation {
166    const NAME: &'static str = "EnsembleComputation";
167    type Value = Min<usize>;
168
169    fn dims(&self) -> Vec<usize> {
170        vec![self.universe_size + self.budget; 2 * self.budget]
171    }
172
173    fn evaluate(&self, config: &[usize]) -> Min<usize> {
174        if config.len() != 2 * self.budget {
175            return Min(None);
176        }
177
178        let Some(required_subsets) = self.required_subsets() else {
179            return Min(None);
180        };
181        if required_subsets.is_empty() {
182            return Min(Some(0));
183        }
184
185        let mut computed = Vec::with_capacity(self.budget);
186        for step in 0..self.budget {
187            let left_operand = config[2 * step];
188            let right_operand = config[2 * step + 1];
189
190            let Some(left) = self.decode_operand(left_operand, &computed) else {
191                return Min(None);
192            };
193            let Some(right) = self.decode_operand(right_operand, &computed) else {
194                return Min(None);
195            };
196
197            if !Self::are_disjoint(&left, &right) {
198                return Min(None);
199            }
200
201            computed.push(Self::union_disjoint(&left, &right));
202            if Self::all_required_subsets_present(&required_subsets, &computed) {
203                return Min(Some(step + 1));
204            }
205        }
206
207        Min(None)
208    }
209
210    fn variant() -> Vec<(&'static str, &'static str)> {
211        crate::variant_params![]
212    }
213}
214
215crate::declare_variants! {
216    default EnsembleComputation => "(universe_size + budget)^(2 * budget)",
217}
218
219#[derive(Debug, Clone, Deserialize)]
220struct EnsembleComputationDef {
221    universe_size: usize,
222    subsets: Vec<Vec<usize>>,
223    budget: usize,
224}
225
226impl TryFrom<EnsembleComputationDef> for EnsembleComputation {
227    type Error = String;
228
229    fn try_from(value: EnsembleComputationDef) -> Result<Self, Self::Error> {
230        Self::try_new(value.universe_size, value.subsets, value.budget)
231    }
232}
233
234#[cfg(feature = "example-db")]
235pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
236    // Keep the canonical example small enough for the example-db optimality check to solve
237    // it via brute force, while still demonstrating reuse of a previously computed set.
238    vec![crate::example_db::specs::ModelExampleSpec {
239        id: "ensemble_computation",
240        instance: Box::new(EnsembleComputation::new(
241            3,
242            vec![vec![0, 1], vec![0, 1, 2]],
243            2,
244        )),
245        optimal_config: vec![0, 1, 3, 2],
246        optimal_value: serde_json::json!(2),
247    }]
248}
249
250#[cfg(test)]
251#[path = "../../unit_tests/models/misc/ensemble_computation.rs"]
252mod tests;