1use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP};
4use crate::models::misc::EnsembleComputation;
5use crate::reduction;
6use crate::rules::traits::{ReduceTo, ReductionResult};
7
8#[derive(Debug, Clone)]
9pub struct ReductionEnsembleComputationToILP {
10 target: ILP<bool>,
11 universe_size: usize,
12 budget: usize,
13 activity_base: usize,
14 left_selector_base: usize,
15 right_selector_base: usize,
16}
17
18impl ReductionEnsembleComputationToILP {
19 fn operand_offset(&self, step: usize) -> usize {
20 step * self.universe_size + step * step.saturating_sub(1) / 2
21 }
22
23 fn selector_var(&self, left: bool, step: usize, operand: usize) -> usize {
24 let base = if left {
25 self.left_selector_base
26 } else {
27 self.right_selector_base
28 };
29 base + self.operand_offset(step) + operand
30 }
31}
32
33impl ReductionResult for ReductionEnsembleComputationToILP {
34 type Source = EnsembleComputation;
35 type Target = ILP<bool>;
36
37 fn target_problem(&self) -> &Self::Target {
38 &self.target
39 }
40
41 fn extract_solution(
42 &self,
43 target_solution: &<Self::Target as crate::traits::Problem>::Solution,
44 ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
45 crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?;
46 let mut config = Vec::with_capacity(2 * self.budget);
47 let mut inactive = false;
48 for step in 0..self.budget {
49 let active = target_solution[self.activity_base + step];
50 if active == 0 {
51 inactive = true;
52 continue;
53 }
54 if active != 1 || inactive {
55 return Err(crate::rules::ExtractionError::invalid(
56 "active ensemble-operation slots must form a binary prefix",
57 ));
58 }
59 for left in [true, false] {
60 let selected = (0..self.universe_size + step)
61 .filter(|&operand| target_solution[self.selector_var(left, step, operand)] == 1)
62 .collect::<Vec<_>>();
63 if selected.len() != 1 {
64 return Err(crate::rules::ExtractionError::invalid(
65 "each active ensemble operation must select exactly one operand per side",
66 ));
67 }
68 config.push(selected[0]);
69 }
70 }
71 let filler = if self.universe_size >= 2 {
72 [0, 1]
73 } else {
74 [0, 0]
75 };
76 while config.len() < 2 * self.budget {
77 config.extend(filler);
78 }
79 Ok(config)
80 }
81}
82
83#[reduction(transform = {
84 exact {
85 num_vars = "3 * budget * universe_size + budget * (budget - 1) * (universe_size + 1) + num_subsets * budget + budget",
86 num_constraints = "5 * budget - 1 + budget * (budget - 1) * (1 + 3 * universe_size) + 2 * budget * universe_size + num_subsets * budget * (universe_size + 2) + num_subsets",
87 num_nonzeros = "6 * budget - 2 + 9 * budget * universe_size + (4 + 9 * universe_size) * budget * (budget - 1) + num_subsets * budget * (4 + 2 * universe_size)",
91 },
92 upper_bound {
93 max_constraint_magnitude_bits = "universe_size + budget + 1",
94 },
95})]
96impl ReduceTo<ILP<bool>> for EnsembleComputation {
97 type Result = ReductionEnsembleComputationToILP;
98
99 fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
100 let u = self.universe_size();
101 let budget = self.budget();
102 let t = self.num_subsets();
103 let overflow = |operation| {
104 crate::rules::ReductionError::integer_overflow::<EnsembleComputation, ILP<bool>>(
105 operation,
106 )
107 };
108 let pair_count = budget
109 .checked_mul(budget.saturating_sub(1))
110 .and_then(|value| value.checked_div(2))
111 .ok_or_else(|| overflow("counting earlier ensemble-operation pairs"))?;
112 let operand_count = budget
113 .checked_mul(u)
114 .and_then(|value| value.checked_add(pair_count))
115 .ok_or_else(|| overflow("counting ensemble operand selectors"))?;
116 let result_count = budget
117 .checked_mul(u)
118 .ok_or_else(|| overflow("counting ensemble result bits"))?;
119 let product_count = pair_count
120 .checked_mul(u)
121 .ok_or_else(|| overflow("counting ensemble selector-result products"))?;
122 let match_count = t
123 .checked_mul(budget)
124 .ok_or_else(|| overflow("counting ensemble target matches"))?;
125
126 let activity_base = 0;
127 let left_selector_base = budget;
128 let right_selector_base = left_selector_base
129 .checked_add(operand_count)
130 .ok_or_else(|| overflow("laying out left ensemble selectors"))?;
131 let result_base = right_selector_base
132 .checked_add(operand_count)
133 .ok_or_else(|| overflow("laying out right ensemble selectors"))?;
134 let left_product_base = result_base
135 .checked_add(result_count)
136 .ok_or_else(|| overflow("laying out ensemble result bits"))?;
137 let right_product_base = left_product_base
138 .checked_add(product_count)
139 .ok_or_else(|| overflow("laying out left ensemble products"))?;
140 let match_base = right_product_base
141 .checked_add(product_count)
142 .ok_or_else(|| overflow("laying out right ensemble products"))?;
143 let num_vars = match_base
144 .checked_add(match_count)
145 .ok_or_else(|| overflow("counting ensemble ILP variables"))?;
146
147 let operand_offset = |step: usize| step * u + step * step.saturating_sub(1) / 2;
148 let selector = |left: bool, step: usize, operand: usize| {
149 (if left {
150 left_selector_base
151 } else {
152 right_selector_base
153 }) + operand_offset(step)
154 + operand
155 };
156 let result = |step: usize, element: usize| result_base + step * u + element;
157 let pair_index = |step: usize, earlier: usize| step * step.saturating_sub(1) / 2 + earlier;
158 let product = |left: bool, step: usize, earlier: usize, element: usize| {
159 (if left {
160 left_product_base
161 } else {
162 right_product_base
163 }) + pair_index(step, earlier) * u
164 + element
165 };
166 let matched = |target: usize, step: usize| match_base + target * budget + step;
167
168 let mut constraints = Vec::new();
169 for step in 0..budget {
170 constraints.push(LinearConstraint::le(vec![(activity_base + step, 1)], 1));
171 }
172 for step in 0..budget.saturating_sub(1) {
173 constraints.push(LinearConstraint::ge(
174 vec![(activity_base + step, 1), (activity_base + step + 1, -1)],
175 0,
176 ));
177 }
178
179 for step in 0..budget {
180 for left in [true, false] {
181 let mut terms = (0..u + step)
182 .map(|operand| (selector(left, step, operand), 1))
183 .collect::<Vec<_>>();
184 terms.push((activity_base + step, -1));
185 constraints.push(LinearConstraint::eq(terms, 0));
186 for earlier in 0..step {
187 constraints.push(LinearConstraint::le(
188 vec![
189 (selector(left, step, u + earlier), 1),
190 (activity_base + earlier, -1),
191 ],
192 0,
193 ));
194 }
195 }
196 let mut canonical_order = vec![(activity_base + step, 1)];
199 for operand in 0..u + step {
200 let coefficient = i64::try_from(
201 operand
202 .checked_add(1)
203 .ok_or_else(|| overflow("ordering ensemble operands"))?,
204 )
205 .map_err(|_| overflow("ordering ensemble operands"))?;
206 canonical_order.push((selector(true, step, operand), coefficient));
207 canonical_order.push((selector(false, step, operand), -coefficient));
208 }
209 constraints.push(LinearConstraint::le(canonical_order, 0));
210 }
211
212 for step in 0..budget {
213 for earlier in 0..step {
214 for element in 0..u {
215 for left in [true, false] {
216 let value = product(left, step, earlier, element);
217 let selected = selector(left, step, u + earlier);
218 let bit = result(earlier, element);
219 constraints.extend(crate::rules::ilp_helpers::mccormick_product(
220 value, selected, bit,
221 ));
222 }
223 }
224 }
225 }
226
227 for step in 0..budget {
228 for element in 0..u {
229 let membership = |left: bool| {
230 let mut terms = vec![(selector(left, step, element), 1)];
231 terms.extend(
232 (0..step).map(|earlier| (product(left, step, earlier, element), 1)),
233 );
234 terms
235 };
236 let left = membership(true);
237 let right = membership(false);
238 let mut disjoint = left.clone();
239 disjoint.extend(right.clone());
240 constraints.push(LinearConstraint::le(disjoint, 1));
241 let mut union = vec![(result(step, element), 1)];
242 union.extend(
243 left.into_iter()
244 .map(|(variable, coefficient)| (variable, -coefficient)),
245 );
246 union.extend(
247 right
248 .into_iter()
249 .map(|(variable, coefficient)| (variable, -coefficient)),
250 );
251 constraints.push(LinearConstraint::eq(union, 0));
252 }
253 }
254
255 for (target_index, target) in self.subsets().iter().enumerate() {
256 let membership: Vec<bool> = (0..u)
257 .map(|element| target.binary_search(&element).is_ok())
258 .collect();
259 let mut present = Vec::with_capacity(budget);
260 for step in 0..budget {
261 let match_var = matched(target_index, step);
262 present.push((match_var, 1));
263 constraints.push(LinearConstraint::le(vec![(match_var, 1)], 1));
264 constraints.push(LinearConstraint::le(
265 vec![(match_var, 1), (activity_base + step, -1)],
266 0,
267 ));
268 for (element, &contains_element) in membership.iter().enumerate() {
269 if contains_element {
270 constraints.push(LinearConstraint::le(
271 vec![(match_var, 1), (result(step, element), -1)],
272 0,
273 ));
274 } else {
275 constraints.push(LinearConstraint::le(
276 vec![(match_var, 1), (result(step, element), 1)],
277 1,
278 ));
279 }
280 }
281 }
282 constraints.push(LinearConstraint::ge(present, 1));
283 }
284
285 let objective = (0..budget).map(|step| (activity_base + step, 1)).collect();
286 let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize)
287 .map_err(Self::target_construction)?;
288 Ok(ReductionEnsembleComputationToILP {
289 target,
290 universe_size: u,
291 budget,
292 activity_base,
293 left_selector_base,
294 right_selector_base,
295 })
296 }
297}
298
299#[cfg(feature = "example-db")]
300pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
301 vec![crate::example_db::specs::RuleExampleSpec {
302 id: "ensemblecomputation_to_ilp",
303 build: || {
304 let source = EnsembleComputation::new(4, vec![vec![0, 1], vec![0, 1, 2, 3]], 3);
305 crate::example_db::specs::rule_example_via_ilp::<_, bool>(source)
306 },
307 }]
308}
309
310#[cfg(test)]
311#[path = "../unit_tests/rules/ensemblecomputation_ilp.rs"]
312mod tests;