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