problemreductions/models/misc/
ensemble_computation.rs1use 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 category: crate::registry::ProblemCategory::Misc,
15 module_path: module_path!(),
16 description: "Find the minimum-length sequence of disjoint unions that builds all required subsets",
17 fields: &[
18 FieldInfo { name: "universe_size", type_name: "usize", description: "Number of elements in the universe A" },
19 FieldInfo { name: "subsets", type_name: "Vec<Vec<usize>>", description: "Required subsets that must appear among the computed z_i values" },
20 FieldInfo { name: "budget", type_name: "usize", description: "Maximum number of union operations J" },
21 ],
22 }
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(try_from = "EnsembleComputationDef")]
27pub struct EnsembleComputation {
28 universe_size: usize,
29 subsets: Vec<Vec<usize>>,
30 budget: usize,
31}
32
33impl EnsembleComputation {
34 pub fn new(universe_size: usize, subsets: Vec<Vec<usize>>, budget: usize) -> Self {
35 Self::try_new(universe_size, subsets, budget).unwrap_or_else(|err| panic!("{err}"))
36 }
37
38 pub fn with_default_budget(universe_size: usize, subsets: Vec<Vec<usize>>) -> Self {
44 let budget = Self::default_budget(&subsets);
45 Self::new(universe_size, subsets, budget)
46 }
47
48 pub fn default_budget(subsets: &[Vec<usize>]) -> usize {
52 subsets.iter().map(|s| s.len()).sum::<usize>().max(1)
53 }
54
55 pub fn try_new(
56 universe_size: usize,
57 subsets: Vec<Vec<usize>>,
58 budget: usize,
59 ) -> Result<Self, crate::registry::ConstructionError> {
60 if budget == 0 {
61 return Err("budget must be positive".to_string().into());
62 }
63 let subsets = subsets
64 .into_iter()
65 .enumerate()
66 .map(|(subset_index, subset)| {
67 Self::normalize_subset(universe_size, subset).ok_or_else(|| {
68 format!(
69 "subset {subset_index} contains element outside universe of size {universe_size}"
70 )
71 })
72 })
73 .collect::<Result<Vec<_>, _>>()?;
74 Ok(Self {
75 universe_size,
76 subsets,
77 budget,
78 })
79 }
80
81 pub fn universe_size(&self) -> usize {
82 self.universe_size
83 }
84
85 pub fn subsets(&self) -> &[Vec<usize>] {
86 &self.subsets
87 }
88
89 pub fn num_subsets(&self) -> usize {
90 self.subsets.len()
91 }
92
93 pub fn budget(&self) -> usize {
94 self.budget
95 }
96
97 fn normalize_subset(universe_size: usize, mut subset: Vec<usize>) -> Option<Vec<usize>> {
98 if subset.iter().any(|&element| element >= universe_size) {
99 return None;
100 }
101 subset.sort_unstable();
102 subset.dedup();
103 Some(subset)
104 }
105
106 fn decode_operand(&self, operand: usize, computed: &[Vec<usize>]) -> Option<Vec<usize>> {
107 if operand < self.universe_size {
108 return Some(vec![operand]);
109 }
110 computed.get(operand - self.universe_size).cloned()
111 }
112
113 fn are_disjoint(left: &[usize], right: &[usize]) -> bool {
114 let mut i = 0;
115 let mut j = 0;
116
117 while i < left.len() && j < right.len() {
118 match left[i].cmp(&right[j]) {
119 std::cmp::Ordering::Less => i += 1,
120 std::cmp::Ordering::Greater => j += 1,
121 std::cmp::Ordering::Equal => return false,
122 }
123 }
124
125 true
126 }
127
128 fn union_disjoint(left: &[usize], right: &[usize]) -> Vec<usize> {
129 let mut union = Vec::with_capacity(left.len() + right.len());
130 let mut i = 0;
131 let mut j = 0;
132
133 while i < left.len() && j < right.len() {
134 if left[i] < right[j] {
135 union.push(left[i]);
136 i += 1;
137 } else {
138 union.push(right[j]);
139 j += 1;
140 }
141 }
142
143 union.extend_from_slice(&left[i..]);
144 union.extend_from_slice(&right[j..]);
145 union
146 }
147
148 fn required_subsets(&self) -> Option<Vec<Vec<usize>>> {
149 self.subsets
150 .iter()
151 .cloned()
152 .map(|subset| Self::normalize_subset(self.universe_size, subset))
153 .collect()
154 }
155
156 fn all_required_subsets_present(
157 required_subsets: &[Vec<usize>],
158 computed: &[Vec<usize>],
159 ) -> bool {
160 required_subsets
161 .iter()
162 .all(|subset| computed.iter().any(|candidate| candidate == subset))
163 }
164}
165
166impl Problem for EnsembleComputation {
167 const NAME: &'static str = "EnsembleComputation";
168 type Solution = Vec<usize>;
169 type Value = Min<i64>;
170
171 crate::problem_parameters![
172 ("budget", budget),
173 ("num_subsets", num_subsets),
174 ("universe_size", universe_size),
175 ];
176
177 fn evaluate(
178 &self,
179 config: &Self::Solution,
180 ) -> Result<Min<i64>, crate::traits::EvaluationError> {
181 Ok({
182 if config.len() != 2 * self.budget {
183 return Err(crate::traits::EvaluationError::InvalidConfiguration(
184 "ensemble program length does not match the operation budget".into(),
185 ));
186 }
187
188 let Some(required_subsets) = self.required_subsets() else {
189 return Ok(Min(None));
190 };
191 if required_subsets.is_empty() {
192 return Ok(Min(Some(0)));
193 }
194
195 let mut computed = Vec::with_capacity(self.budget);
196 for step in 0..self.budget {
197 let left_operand = config[2 * step];
198 let right_operand = config[2 * step + 1];
199
200 let Some(left) = self.decode_operand(left_operand, &computed) else {
201 return Ok(Min(None));
202 };
203 let Some(right) = self.decode_operand(right_operand, &computed) else {
204 return Ok(Min(None));
205 };
206
207 if !Self::are_disjoint(&left, &right) {
208 return Ok(Min(None));
209 }
210
211 computed.push(Self::union_disjoint(&left, &right));
212 if Self::all_required_subsets_present(&required_subsets, &computed) {
213 return Ok(Min(Some(i64::try_from(step + 1).map_err(|_| {
214 crate::traits::EvaluationError::IntegerOverflow(
215 "converting union-operation count to i64".into(),
216 )
217 })?)));
218 }
219 }
220
221 Min(None)
222 })
223 }
224
225 fn variant() -> Vec<(&'static str, &'static str)> {
226 crate::variant_params![]
227 }
228}
229
230impl crate::solvers::BruteForceProblem for EnsembleComputation {
231 fn dimensions(&self) -> Vec<usize> {
232 vec![self.universe_size + self.budget; 2 * self.budget]
233 }
234}
235
236crate::declare_variants! {
237 default EnsembleComputation => "(universe_size + budget)^(2 * budget)",
238}
239
240crate::register_brute_force! {
241 EnsembleComputation,
242}
243
244#[derive(Debug, Clone, Deserialize)]
245struct EnsembleComputationDef {
246 universe_size: usize,
247 subsets: Vec<Vec<usize>>,
248 budget: usize,
249}
250
251impl TryFrom<EnsembleComputationDef> for EnsembleComputation {
252 type Error = crate::registry::ConstructionError;
253
254 fn try_from(value: EnsembleComputationDef) -> Result<Self, Self::Error> {
255 Self::try_new(value.universe_size, value.subsets, value.budget)
256 }
257}
258
259#[cfg(feature = "example-db")]
260pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
261 vec![crate::example_db::specs::ModelExampleSpec {
264 id: "ensemble_computation",
265 instance: Box::new(EnsembleComputation::new(
266 3,
267 vec![vec![0, 1], vec![0, 1, 2]],
268 2,
269 )),
270 optimal_config: serde_json::json!(vec![0, 1, 3, 2]),
271 optimal_value: serde_json::json!(2),
272 }]
273}
274
275#[cfg(test)]
276#[path = "../../unit_tests/models/misc/ensemble_computation.rs"]
277mod tests;