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<i64>,
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<i64>;
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(
84 transform = 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 },
88 unavailable = {
89 num_nonzeros = "depends on the cardinalities and duplicate structure of the required subsets",
90 }
91)]
92impl ReduceTo<ILP<i64>> for EnsembleComputation {
93 type Result = ReductionEnsembleComputationToILP;
94
95 fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
96 let u = self.universe_size();
97 let budget = self.budget();
98 let t = self.num_subsets();
99 let overflow = |operation| {
100 crate::rules::ReductionError::integer_overflow::<EnsembleComputation, ILP<i64>>(
101 operation,
102 )
103 };
104 let pair_count = budget
105 .checked_mul(budget.saturating_sub(1))
106 .and_then(|value| value.checked_div(2))
107 .ok_or_else(|| overflow("counting earlier ensemble-operation pairs"))?;
108 let operand_count = budget
109 .checked_mul(u)
110 .and_then(|value| value.checked_add(pair_count))
111 .ok_or_else(|| overflow("counting ensemble operand selectors"))?;
112 let result_count = budget
113 .checked_mul(u)
114 .ok_or_else(|| overflow("counting ensemble result bits"))?;
115 let product_count = pair_count
116 .checked_mul(u)
117 .ok_or_else(|| overflow("counting ensemble selector-result products"))?;
118 let match_count = t
119 .checked_mul(budget)
120 .ok_or_else(|| overflow("counting ensemble target matches"))?;
121
122 let activity_base = 0;
123 let left_selector_base = budget;
124 let right_selector_base = left_selector_base
125 .checked_add(operand_count)
126 .ok_or_else(|| overflow("laying out left ensemble selectors"))?;
127 let result_base = right_selector_base
128 .checked_add(operand_count)
129 .ok_or_else(|| overflow("laying out right ensemble selectors"))?;
130 let left_product_base = result_base
131 .checked_add(result_count)
132 .ok_or_else(|| overflow("laying out ensemble result bits"))?;
133 let right_product_base = left_product_base
134 .checked_add(product_count)
135 .ok_or_else(|| overflow("laying out left ensemble products"))?;
136 let match_base = right_product_base
137 .checked_add(product_count)
138 .ok_or_else(|| overflow("laying out right ensemble products"))?;
139 let num_vars = match_base
140 .checked_add(match_count)
141 .ok_or_else(|| overflow("counting ensemble ILP variables"))?;
142
143 let operand_offset = |step: usize| step * u + step * step.saturating_sub(1) / 2;
144 let selector = |left: bool, step: usize, operand: usize| {
145 (if left {
146 left_selector_base
147 } else {
148 right_selector_base
149 }) + operand_offset(step)
150 + operand
151 };
152 let result = |step: usize, element: usize| result_base + step * u + element;
153 let pair_index = |step: usize, earlier: usize| step * step.saturating_sub(1) / 2 + earlier;
154 let product = |left: bool, step: usize, earlier: usize, element: usize| {
155 (if left {
156 left_product_base
157 } else {
158 right_product_base
159 }) + pair_index(step, earlier) * u
160 + element
161 };
162 let matched = |target: usize, step: usize| match_base + target * budget + step;
163
164 let mut constraints = Vec::new();
165 for step in 0..budget {
166 constraints.push(LinearConstraint::le(vec![(activity_base + step, 1)], 1));
167 }
168 for step in 0..budget.saturating_sub(1) {
169 constraints.push(LinearConstraint::ge(
170 vec![(activity_base + step, 1), (activity_base + step + 1, -1)],
171 0,
172 ));
173 }
174
175 for step in 0..budget {
176 for left in [true, false] {
177 let mut terms = (0..u + step)
178 .map(|operand| (selector(left, step, operand), 1))
179 .collect::<Vec<_>>();
180 terms.push((activity_base + step, -1));
181 constraints.push(LinearConstraint::eq(terms, 0));
182 for earlier in 0..step {
183 constraints.push(LinearConstraint::le(
184 vec![
185 (selector(left, step, u + earlier), 1),
186 (activity_base + earlier, -1),
187 ],
188 0,
189 ));
190 }
191 }
192 let mut canonical_order = vec![(activity_base + step, 1)];
195 for operand in 0..u + step {
196 let coefficient = i64::try_from(
197 operand
198 .checked_add(1)
199 .ok_or_else(|| overflow("ordering ensemble operands"))?,
200 )
201 .map_err(|_| overflow("ordering ensemble operands"))?;
202 canonical_order.push((selector(true, step, operand), coefficient));
203 canonical_order.push((selector(false, step, operand), -coefficient));
204 }
205 constraints.push(LinearConstraint::le(canonical_order, 0));
206 }
207
208 for step in 0..budget {
209 for earlier in 0..step {
210 for element in 0..u {
211 for left in [true, false] {
212 let value = product(left, step, earlier, element);
213 let selected = selector(left, step, u + earlier);
214 let bit = result(earlier, element);
215 constraints.extend(crate::rules::ilp_helpers::mccormick_product(
216 value, selected, bit,
217 ));
218 }
219 }
220 }
221 }
222
223 for step in 0..budget {
224 for element in 0..u {
225 let membership = |left: bool| {
226 let mut terms = vec![(selector(left, step, element), 1)];
227 terms.extend(
228 (0..step).map(|earlier| (product(left, step, earlier, element), 1)),
229 );
230 terms
231 };
232 let left = membership(true);
233 let right = membership(false);
234 let mut disjoint = left.clone();
235 disjoint.extend(right.clone());
236 constraints.push(LinearConstraint::le(disjoint, 1));
237 let mut union = vec![(result(step, element), 1)];
238 union.extend(
239 left.into_iter()
240 .map(|(variable, coefficient)| (variable, -coefficient)),
241 );
242 union.extend(
243 right
244 .into_iter()
245 .map(|(variable, coefficient)| (variable, -coefficient)),
246 );
247 constraints.push(LinearConstraint::eq(union, 0));
248 }
249 }
250
251 for (target_index, target) in self.subsets().iter().enumerate() {
252 let membership: Vec<bool> = (0..u)
253 .map(|element| target.binary_search(&element).is_ok())
254 .collect();
255 let mut present = Vec::with_capacity(budget);
256 for step in 0..budget {
257 let match_var = matched(target_index, step);
258 present.push((match_var, 1));
259 constraints.push(LinearConstraint::le(vec![(match_var, 1)], 1));
260 constraints.push(LinearConstraint::le(
261 vec![(match_var, 1), (activity_base + step, -1)],
262 0,
263 ));
264 for (element, &contains_element) in membership.iter().enumerate() {
265 if contains_element {
266 constraints.push(LinearConstraint::le(
267 vec![(match_var, 1), (result(step, element), -1)],
268 0,
269 ));
270 } else {
271 constraints.push(LinearConstraint::le(
272 vec![(match_var, 1), (result(step, element), 1)],
273 1,
274 ));
275 }
276 }
277 }
278 constraints.push(LinearConstraint::ge(present, 1));
279 }
280
281 let objective = (0..budget).map(|step| (activity_base + step, 1)).collect();
282 let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize)
283 .map_err(Self::target_construction)?;
284 Ok(ReductionEnsembleComputationToILP {
285 target,
286 universe_size: u,
287 budget,
288 activity_base,
289 left_selector_base,
290 right_selector_base,
291 })
292 }
293}
294
295#[cfg(feature = "example-db")]
296pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
297 vec![crate::example_db::specs::RuleExampleSpec {
298 id: "ensemblecomputation_to_ilp",
299 build: || {
300 let source = EnsembleComputation::new(4, vec![vec![0, 1], vec![0, 1, 2, 3]], 3);
301 crate::example_db::specs::rule_example_via_ilp::<_, i64>(source)
302 },
303 }]
304}
305
306#[cfg(test)]
307#[path = "../unit_tests/rules/ensemblecomputation_ilp.rs"]
308mod tests;