Skip to main content

problemreductions/rules/
ensemblecomputation_ilp.rs

1//! Polynomial-size circuit-slot reduction from EnsembleComputation to `ILP<i64>`.
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<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            // Disjoint union is commutative. Canonically ordering operand
193            // indices removes the two equivalent orientations of every gate.
194            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;