Skip to main content

problemreductions/rules/
ensemblecomputation_ilp.rs

1//! Polynomial-size circuit-slot reduction from EnsembleComputation to `ILP<bool>`.
2
3use 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        // Distinct selector/product/result blocks never merge. For b>=1,
88        // sum activity/prefix/selection/order terms, 7 terms per product,
89        // both membership rows, and each target's match rows plus final sum.
90        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            // Disjoint union is commutative. Canonically ordering operand
197            // indices removes the two equivalent orientations of every gate.
198            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;