1use crate::models::algebraic::{Comparison, ObjectiveSense, ILP, QUBO};
15use crate::reduction;
16use crate::rules::traits::{ReduceTo, ReductionResult};
17
18#[derive(Debug, Clone)]
20pub struct ReductionILPToQUBO {
21 target: QUBO<i64>,
22 num_original_vars: usize,
23 sense: ObjectiveSense,
24 penalty_constant: i64,
25 feasible_energy_lower: i64,
26 feasible_energy_upper: i64,
27}
28
29impl ReductionResult for ReductionILPToQUBO {
30 type Source = ILP<bool>;
31 type Target = QUBO<i64>;
32
33 fn target_problem(&self) -> &Self::Target {
34 &self.target
35 }
36
37 fn extract_solution(
39 &self,
40 target_solution: &<Self::Target as crate::traits::Problem>::Solution,
41 ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
42 crate::rules::traits::validate_target_witness(
43 self.target_problem(),
44 target_solution,
45 |value| crate::rules::AggregateReductionResult::extract_value(self, value).is_valid(),
46 "target QUBO configuration does not certify a feasible ILP assignment",
47 )?;
48
49 Ok(target_solution[..self.num_original_vars]
50 .iter()
51 .map(|&value| i64::from(value))
52 .collect())
53 }
54}
55
56#[crate::aggregate_reduction]
57impl crate::rules::AggregateReductionResult for ReductionILPToQUBO {
58 type Source = ILP<bool>;
59 type Target = QUBO<i64>;
60
61 fn target_problem(&self) -> &Self::Target {
62 &self.target
63 }
64
65 fn extract_value(&self, value: crate::types::Min<i64>) -> crate::types::Extremum<i64> {
66 let objective = value
67 .0
68 .filter(|&energy| {
69 self.feasible_energy_lower <= energy && energy <= self.feasible_energy_upper
70 })
71 .map(|energy| energy + self.penalty_constant);
73 match self.sense {
74 ObjectiveSense::Minimize => crate::types::Extremum::minimize(objective),
75 ObjectiveSense::Maximize => crate::types::Extremum::maximize(objective.map(|v| -v)),
77 }
78 }
79}
80
81#[reduction(transform = upper_bound {
85 num_vars = "num_vars + num_constraints * (num_vars + max_constraint_magnitude_bits)",
86 num_quadratic_terms = "(num_vars + num_constraints * (num_vars + max_constraint_magnitude_bits))^2",
87})]
88impl ReduceTo<QUBO<i64>> for ILP<bool> {
89 type Result = ReductionILPToQUBO;
90
91 fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
92 let n = self.num_vars();
93
94 let num_constraints = self.constraints().len();
99 let mut a_dense = vec![vec![0_i64; n]; num_constraints];
100 let mut b_vec = vec![0_i64; num_constraints];
101 let mut slack_sizes = vec![0usize; num_constraints];
102
103 for (k, constraint) in self.constraints().iter().enumerate() {
104 for &(var, coef) in constraint.terms() {
105 a_dense[k][var] = coef;
106 }
107 b_vec[k] = constraint.rhs();
108
109 match constraint.comparison() {
113 Comparison::Eq => {} Comparison::Le => {
115 let min_lhs = a_dense[k]
117 .iter()
118 .try_fold(0_i64, |sum, &coefficient| {
119 sum.checked_add(coefficient.min(0))
120 })
121 .ok_or_else(|| {
122 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
123 "computing an inequality's minimum left-hand side",
124 )
125 })?;
126 let slack_range = constraint.rhs().checked_sub(min_lhs).ok_or_else(|| {
127 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
128 "computing a less-than inequality's slack range",
129 )
130 })?;
131 if slack_range > 0 {
132 slack_sizes[k] = i64::BITS as usize - slack_range.leading_zeros() as usize;
133 }
134 }
135 Comparison::Ge => {
136 let max_lhs = a_dense[k]
138 .iter()
139 .try_fold(0_i64, |sum, &coefficient| {
140 sum.checked_add(coefficient.max(0))
141 })
142 .ok_or_else(|| {
143 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
144 "computing an inequality's maximum left-hand side",
145 )
146 })?;
147 let slack_range = max_lhs.checked_sub(constraint.rhs()).ok_or_else(|| {
148 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
149 "computing a greater-than inequality's slack range",
150 )
151 })?;
152 if slack_range > 0 {
153 slack_sizes[k] = i64::BITS as usize - slack_range.leading_zeros() as usize;
154 }
155 }
156 }
157 }
158
159 let total_slack = slack_sizes.iter().try_fold(0_usize, |total, &size| {
160 total.checked_add(size).ok_or_else(|| {
161 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
162 "counting QUBO slack variables",
163 )
164 })
165 })?;
166 let nq = qubo_num_variables(n, total_slack)?;
167
168 let mut a_ext = vec![vec![0_i64; nq]; num_constraints];
170 for k in 0..num_constraints {
171 for j in 0..n {
172 a_ext[k][j] = a_dense[k][j];
173 }
174 }
175
176 let mut slack_col = n;
178 for (k, &ns) in slack_sizes.iter().enumerate() {
179 if ns > 0 {
180 let sign = match self.constraints()[k].comparison() {
181 Comparison::Le => 1, Comparison::Ge => -1, Comparison::Eq => 0,
184 };
185 for s in 0..ns {
186 let bit = u32::try_from(s).map_err(|_| {
187 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
188 "encoding a QUBO slack bit",
189 )
190 })?;
191 a_ext[k][slack_col + s] = 1_i64.checked_shl(bit).ok_or_else(|| {
192 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
193 "encoding a QUBO slack bit",
194 )
195 })? * sign;
196 }
197 slack_col += ns;
198 }
199 }
200
201 let mut c_vec = vec![0_i64; nq];
203 for &(var, coef) in self.objective() {
204 c_vec[var] = coef;
205 }
206
207 if self.sense() == ObjectiveSense::Minimize {
209 for c in c_vec.iter_mut() {
210 *c = c.checked_neg().ok_or_else(|| {
211 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
212 "negating an ILP objective coefficient",
213 )
214 })?;
215 }
216 }
217
218 let objective_magnitude = c_vec.iter().try_fold(0_i64, |total, &coefficient| {
220 let magnitude = coefficient.checked_abs().ok_or_else(|| {
221 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
222 "taking the magnitude of an ILP objective coefficient",
223 )
224 })?;
225 total.checked_add(magnitude).ok_or_else(|| {
226 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
227 "summing ILP objective coefficient magnitudes",
228 )
229 })
230 })?;
231 let rhs_magnitude = b_vec.iter().try_fold(0_i64, |total, &rhs| {
232 let magnitude = rhs.checked_abs().ok_or_else(|| {
233 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
234 "taking the magnitude of an ILP right-hand side",
235 )
236 })?;
237 total.checked_add(magnitude).ok_or_else(|| {
238 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
239 "summing ILP right-hand-side magnitudes",
240 )
241 })
242 })?;
243 let penalty = objective_magnitude
244 .checked_add(rhs_magnitude)
245 .and_then(|sum| sum.checked_add(1))
246 .ok_or_else(|| {
247 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
248 "computing the QUBO constraint penalty",
249 )
250 })?;
251
252 let (penalty_constant, feasible_energy_lower, feasible_energy_upper) =
253 feasible_energy_range(&c_vec, &b_vec, penalty)?;
254
255 let mut matrix = vec![vec![0_i64; nq]; nq];
257
258 let mut ba = vec![0_i64; nq];
260 for (j, ba_j) in ba.iter_mut().enumerate() {
261 for (k, &b_k) in b_vec.iter().enumerate() {
262 let term = b_k.checked_mul(a_ext[k][j]).ok_or_else(|| {
263 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
264 "multiplying a right-hand side by a row coefficient",
265 )
266 })?;
267 *ba_j = ba_j.checked_add(term).ok_or_else(|| {
268 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
269 "computing the right-hand-side row product",
270 )
271 })?;
272 }
273 }
274
275 for j in 0..nq {
277 let penalty_term = penalty
278 .checked_mul(ba[j])
279 .and_then(|value| value.checked_mul(2))
280 .ok_or_else(|| {
281 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
282 "computing a QUBO diagonal penalty",
283 )
284 })?;
285 matrix[j][j] = c_vec[j]
286 .checked_add(penalty_term)
287 .and_then(i64::checked_neg)
288 .ok_or_else(|| {
289 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
290 "computing a QUBO diagonal coefficient",
291 )
292 })?;
293 }
294
295 for row in &a_ext {
299 for (i, row_i) in matrix.iter_mut().enumerate() {
300 if row[i] == 0 {
301 continue;
302 }
303 let diagonal = penalty
305 .checked_mul(row[i])
306 .and_then(|value| value.checked_mul(row[i]))
307 .ok_or_else(|| {
308 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
309 "computing a quadratic QUBO diagonal penalty",
310 )
311 })?;
312 row_i[i] = row_i[i].checked_add(diagonal).ok_or_else(|| {
313 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
314 "adding a quadratic QUBO diagonal penalty",
315 )
316 })?;
317 for j in (i + 1)..nq {
319 let interaction = penalty
320 .checked_mul(row[i])
321 .and_then(|value| value.checked_mul(row[j]))
322 .and_then(|value| value.checked_mul(2))
323 .ok_or_else(|| {
324 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
325 "computing a quadratic QUBO interaction penalty",
326 )
327 })?;
328 row_i[j] = row_i[j].checked_add(interaction).ok_or_else(|| {
329 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
330 "adding a quadratic QUBO interaction penalty",
331 )
332 })?;
333 }
334 }
335 }
336
337 Ok(ReductionILPToQUBO {
338 target: QUBO::from_matrix(matrix)
339 .map_err(crate::rules::ReductionError::construction::<ILP<bool>, QUBO<i64>>)?,
340 num_original_vars: n,
341 sense: self.sense(),
342 penalty_constant,
343 feasible_energy_lower,
344 feasible_energy_upper,
345 })
346 }
347}
348
349fn qubo_num_variables(n: usize, slack: usize) -> Result<usize, crate::rules::ReductionError> {
351 let overflow = || {
352 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
353 "counting dense QUBO entries",
354 )
355 };
356 let total = n.checked_add(slack).ok_or_else(overflow)?;
357 total.checked_mul(total).ok_or_else(overflow)?;
358 Ok(total)
359}
360
361fn feasible_energy_range(
364 cost: &[i64],
365 rhs: &[i64],
366 penalty: i64,
367) -> Result<(i64, i64, i64), crate::rules::ReductionError> {
368 let overflow = || {
369 crate::rules::ReductionError::integer_overflow::<ILP<bool>, QUBO<i64>>(
370 "encoding the QUBO feasible energy interval",
371 )
372 };
373 let constant = rhs.iter().try_fold(0_i64, |sum, &b| {
374 b.checked_mul(b)
375 .and_then(|square| square.checked_mul(penalty))
376 .and_then(|term| sum.checked_add(term))
377 .ok_or_else(overflow)
378 })?;
379 let (lower, upper) = cost.iter().try_fold((0_i64, 0_i64), |(low, high), &c| {
380 Ok::<_, crate::rules::ReductionError>((
381 low.checked_sub(c.max(0)).ok_or_else(overflow)?,
382 high.checked_sub(c.min(0)).ok_or_else(overflow)?,
383 ))
384 })?;
385 Ok((
386 constant,
387 lower.checked_sub(constant).ok_or_else(overflow)?,
388 upper.checked_sub(constant).ok_or_else(overflow)?,
389 ))
390}
391
392#[cfg(feature = "example-db")]
393pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
394 use crate::export::SolutionPair;
395 use crate::models::algebraic::{LinearConstraint, ObjectiveSense};
396
397 vec![crate::example_db::specs::RuleExampleSpec {
398 id: "ilp_to_qubo",
399 build: || {
400 let source = ILP::new(
401 6,
402 vec![
403 LinearConstraint::le(vec![(0, 3), (1, 2), (2, 5), (3, 4), (4, 2), (5, 3)], 10),
404 LinearConstraint::le(vec![(0, 1), (1, 1), (2, 1)], 2),
405 LinearConstraint::le(vec![(3, 1), (4, 1), (5, 1)], 2),
406 ],
407 vec![(0, 10), (1, 7), (2, 12), (3, 8), (4, 6), (5, 9)],
408 ObjectiveSense::Maximize,
409 )
410 .expect("canonical ILP example must satisfy construction invariants");
411 crate::example_db::specs::rule_example_with_witness::<_, QUBO<i64>>(
412 source,
413 SolutionPair {
414 source_config: serde_json::json!(vec![1, 1, 0, 0, 1, 1]),
415 target_config: serde_json::json!(vec![
416 true, true, false, false, true, true, false, false, false, false, false,
417 false, false, false
418 ]),
419 },
420 )
421 },
422 }]
423}
424
425#[cfg(test)]
426#[path = "../unit_tests/rules/ilp_qubo.rs"]
427mod tests;