Skip to content

Commit 36597f9

Browse files
GiggleLiuclaude
andauthored
Fix #91: [Rule] ClosestVectorProblem to QUBO (#703)
* Add plan for #91: [Rule] ClosestVectorProblem to QUBO * feat(cvp): add bounded encoding helpers for QUBO reduction * docs(plans): fix issue #91 execution steps * feat(rules): add ClosestVectorProblem to QUBO reduction * docs(paper): document ClosestVectorProblem to QUBO * chore: remove plan file after implementation * Remove redundant #[cfg(any(test, feature = "example-db"))] attribute The inner #[cfg(feature = "example-db")] is more restrictive and is the effective gate; the outer cfg was dead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Apply rustfmt formatting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix CI: remove duplicate BibTeX key and fix paper field access - Remove duplicate dreyfuswagner1971 entry introduced by merge - Fix SequencingToMinimizeWeightedCompletionTime example to use optimal_config/optimal_value instead of removed optimal field Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6e5843c commit 36597f9

7 files changed

Lines changed: 427 additions & 11 deletions

File tree

docs/paper/reductions.typ

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4336,6 +4336,65 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead
43364336
_Solution extraction._ Convert binary to spins: $s_i = 2x_i - 1$, i.e.\ $x_i = 1 arrow.r s_i = +1$, $x_i = 0 arrow.r s_i = -1$.
43374337
]
43384338

4339+
#let cvp_qubo = load-example("ClosestVectorProblem", "QUBO")
4340+
#let cvp_qubo_sol = cvp_qubo.solutions.at(0)
4341+
#{
4342+
let basis = cvp_qubo.source.instance.basis
4343+
let bounds = cvp_qubo.source.instance.bounds
4344+
let target = cvp_qubo.source.instance.target
4345+
let offsets = cvp_qubo_sol.source_config
4346+
let coords = offsets.enumerate().map(((i, off)) => off + bounds.at(i).lower)
4347+
let matrix = cvp_qubo.target.instance.matrix
4348+
let bits = cvp_qubo_sol.target_config
4349+
let lo = bounds.map(b => b.lower)
4350+
let anchor = range(target.len()).map(d => lo.enumerate().fold(0.0, (acc, (i, x)) => acc + x * basis.at(i).at(d)))
4351+
let constant = range(target.len()).fold(0.0, (acc, d) => acc + calc.pow(anchor.at(d) - target.at(d), 2))
4352+
let qubo-value = range(bits.len()).fold(0.0, (acc, i) => acc + if bits.at(i) == 0 { 0.0 } else {
4353+
range(bits.len() - i).fold(0.0, (row-acc, delta) => row-acc + if bits.at(i + delta) == 0 { 0.0 } else { matrix.at(i).at(i + delta) })
4354+
})
4355+
let fmt-vec(v) = $paren.l #v.map(e => str(e)).join(", ") paren.r^top$
4356+
let rounded-constant = calc.round(constant, digits: 2)
4357+
let rounded-qubo = calc.round(qubo-value, digits: 1)
4358+
let rounded-distance-sq = calc.round(qubo-value + constant, digits: 2)
4359+
[
4360+
#reduction-rule("ClosestVectorProblem", "QUBO",
4361+
example: true,
4362+
example-caption: [2D bounded CVP with two 3-bit exact-range encodings],
4363+
extra: [
4364+
*Step 1 -- Source instance.* The canonical CVP example uses basis columns $bold(b)_1 = #fmt-vec(basis.at(0))$ and $bold(b)_2 = #fmt-vec(basis.at(1))$, target $bold(t) = #fmt-vec(target)$, and bounds $x_1, x_2 in [#bounds.at(0).lower, #bounds.at(0).upper]$.
4365+
4366+
*Step 2 -- Exact bounded encoding.* Each variable has #bounds.at(0).upper - bounds.at(0).lower + 1 admissible values, so the implementation uses the capped binary basis $(1, 2, 3)$ rather than $(1, 2, 4)$: the first two bits are powers of two, and the last weight is capped so every bit pattern reconstructs an offset in ${0, dots, 6}$. Thus
4367+
$ x_1 = #bounds.at(0).lower + z_0 + 2 z_1 + 3 z_2, quad x_2 = #bounds.at(1).lower + z_3 + 2 z_4 + 3 z_5 $
4368+
giving #cvp_qubo.target.instance.num_vars QUBO variables in total.
4369+
4370+
*Step 3 -- Build the QUBO.* For this instance, $G = A^top A = ((4, 2), (2, 5))$ and $h = A^top bold(t) = (5.6, 5.8)^top$. Expanding the shifted quadratic form yields the exported upper-triangular matrix with representative entries $Q_(0,0) = #matrix.at(0).at(0)$, $Q_(0,1) = #matrix.at(0).at(1)$, $Q_(0,2) = #matrix.at(0).at(2)$, $Q_(2,5) = #matrix.at(2).at(5)$, and $Q_(5,5) = #matrix.at(5).at(5)$.
4371+
4372+
*Step 4 -- Verify a solution.* The fixture stores the canonical witness $bold(z) = (#bits.map(str).join(", "))$, which extracts to source offsets $bold(c) = (#offsets.map(str).join(", "))$ and actual lattice coordinates $bold(x) = (#coords.map(str).join(", "))$. The QUBO value is $bold(z)^top Q bold(z) = #rounded-qubo$; adding back the dropped constant #rounded-constant yields the original squared distance #(rounded-distance-sq), so the extracted point is the closest lattice vector #sym.checkmark.
4373+
4374+
*Multiplicity.* Offset $3$ has two bit encodings ($(0, 0, 1)$ and $(1, 1, 0)$), so the fixture stores one canonical witness even though the QUBO has multiple optimal binary assignments representing the same CVP solution.
4375+
],
4376+
)[
4377+
A bounded Closest Vector Problem instance already supplies a finite integer box $x_i in [ell_i, u_i]$ for each coefficient. Following the direct quadratic-form reduction of Canale, Qureshi, and Viola @canale2023qubo, encoding each offset $c_i = x_i - ell_i$ with an exact in-range binary basis turns the squared-distance objective into an unconstrained quadratic over binary variables. Unlike penalty-method encodings, no auxiliary feasibility penalty is needed: every bit pattern decodes to a legal coefficient vector by construction.
4378+
][
4379+
_Construction._ Let $A in ZZ^(m times n)$ be the basis matrix with columns $bold(a)_1, dots, bold(a)_n$, let $bold(t) in RR^m$ be the target, and let $x_i in [ell_i, u_i]$ with range $r_i = u_i - ell_i$. Define $L_i = ceil(log_2(r_i + 1))$ when $r_i > 0$ and omit bits when $r_i = 0$. For each variable, introduce binary variables $z_(i,0), dots, z_(i,L_i-1)$ with exact-range weights
4380+
$ w_(i,p) = 2^p quad (0 <= p < L_i - 1), quad w_(i,L_i-1) = r_i + 1 - 2^(L_i - 1) $
4381+
so that every bit vector represents an offset in ${0, dots, r_i}$. Then
4382+
$ x_i = ell_i + sum_(p=0)^(L_i-1) w_(i,p) z_(i,p) $
4383+
and the total number of QUBO variables is $N = sum_i L_i$, exactly the exported overhead `num_vars = num_encoding_bits`.
4384+
4385+
Let $G = A^top A$ and $h = A^top bold(t)$. Writing $bold(x) = bold(ell) + B bold(z)$ for the encoding matrix $B in RR^(n times N)$ gives
4386+
$ norm(A bold(x) - bold(t))_2^2 = bold(z)^top (B^top G B) bold(z) + 2 bold(z)^top B^top (G bold(ell) - h) + "const" $
4387+
where the constant $norm(A bold(ell) - bold(t))_2^2$ is dropped. Therefore the QUBO coefficients are
4388+
$ Q_(u,u) = (B^top G B)_(u,u) + 2 (B^top (G bold(ell) - h))_u, quad Q_(u,v) = 2 (B^top G B)_(u,v) quad (u < v) $
4389+
using the usual upper-triangular convention.
4390+
4391+
_Correctness._ ($arrow.r.double$) Every binary vector $bold(z) in {0,1}^N$ decodes to a coefficient vector $bold(x)$ inside the prescribed bounds because each exact-range basis reaches only offsets in ${0, dots, r_i}$. Substituting this decoding into the CVP objective yields $bold(z)^top Q bold(z) + "const"$, so any QUBO minimizer maps to a bounded CVP minimizer. ($arrow.l.double$) Every bounded CVP solution $bold(x)$ has at least one bit encoding for each coordinate offset, hence at least one binary vector $bold(z)$ with the same objective value up to the dropped constant. Thus the minimizers correspond exactly, although several binary witnesses may decode to the same CVP solution.
4392+
4393+
_Solution extraction._ For each source variable, sum its selected encoding weights to recover the source configuration offset $c_i = x_i - ell_i$. This is exactly the configuration format expected by the `ClosestVectorProblem` model.
4394+
]
4395+
]
4396+
}
4397+
43394398
== Penalty-Method QUBO Reductions <sec:penalty-method>
43404399

43414400
The _penalty method_ @glover2019 @lucas2014 converts a constrained optimization problem into an unconstrained QUBO by adding quadratic penalty terms. Given an objective $"obj"(bold(x))$ to minimize and constraints $g_k (bold(x)) = 0$, construct:
@@ -5250,6 +5309,7 @@ The following table shows concrete variable overhead for example instances, take
52505309
(source: "SpinGlass", target: "MaxCut"),
52515310
(source: "SpinGlass", target: "QUBO"),
52525311
(source: "QUBO", target: "SpinGlass"),
5312+
(source: "ClosestVectorProblem", target: "QUBO"),
52535313
(source: "KColoring", target: "QUBO"),
52545314
(source: "MaximumSetPacking", target: "QUBO"),
52555315
(

docs/paper/references.bib

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,15 @@ @article{pan2025
507507
archivePrefix = {arXiv}
508508
}
509509

510+
@article{canale2023qubo,
511+
author = {Eduardo Canale and Claudio Qureshi and Alfredo Viola},
512+
title = {Qubo model for the Closest Vector Problem},
513+
journal = {arXiv preprint},
514+
year = {2023},
515+
eprint = {2304.03616},
516+
archivePrefix = {arXiv}
517+
}
518+
510519
@article{goemans1995,
511520
author = {Michel X. Goemans and David P. Williamson},
512521
title = {Improved Approximation Algorithms for Maximum Cut and Satisfiability Problems Using Semidefinite Programming},
@@ -793,17 +802,6 @@ @article{cygan2014
793802
doi = {10.1137/140990255}
794803
}
795804

796-
@article{dreyfuswagner1971,
797-
author = {Stuart E. Dreyfus and Robert A. Wagner},
798-
title = {The Steiner Problem in Graphs},
799-
journal = {Networks},
800-
volume = {1},
801-
number = {3},
802-
pages = {195--207},
803-
year = {1971},
804-
doi = {10.1002/net.3230010302}
805-
}
806-
807805
@inproceedings{bjorklund2007,
808806
author = {Andreas Bj\"{o}rklund and Thore Husfeldt and Petteri Kaski and Mikko Koivisto},
809807
title = {Fourier Meets M\"{o}bius: Fast Subset Convolution},

src/models/algebraic/closest_vector_problem.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,42 @@ impl VarBounds {
9898
_ => None,
9999
}
100100
}
101+
102+
/// Returns an exact bounded binary basis for offsets in this range.
103+
///
104+
/// For a bounded variable with offsets `0..=hi-lo`, the returned weights
105+
/// ensure that every bit-pattern reconstructs an in-range offset. Low-order
106+
/// weights use powers of two; the final weight is capped so the maximum
107+
/// reachable offset is exactly `hi-lo`.
108+
pub(crate) fn exact_encoding_weights(&self) -> Vec<i64> {
109+
let Some(num_values) = self.num_values() else {
110+
panic!("CVP QUBO encoding requires finite variable bounds");
111+
};
112+
if num_values <= 1 {
113+
return Vec::new();
114+
}
115+
116+
let max_offset = (num_values - 1) as i64;
117+
let num_bits = (usize::BITS - (num_values - 1).leading_zeros()) as usize;
118+
let mut weights = Vec::with_capacity(num_bits);
119+
120+
for bit in 0..num_bits.saturating_sub(1) {
121+
weights.push(1_i64 << bit);
122+
}
123+
124+
let covered_by_lower_bits = if num_bits <= 1 {
125+
0
126+
} else {
127+
(1_i64 << (num_bits - 1)) - 1
128+
};
129+
weights.push(max_offset - covered_by_lower_bits);
130+
weights
131+
}
132+
133+
/// Returns the number of encoding bits needed for the exact bounded basis.
134+
pub(crate) fn num_encoding_bits(&self) -> usize {
135+
self.exact_encoding_weights().len()
136+
}
101137
}
102138

103139
/// Closest Vector Problem (CVP).
@@ -175,6 +211,11 @@ impl<T> ClosestVectorProblem<T> {
175211
&self.bounds
176212
}
177213

214+
/// Returns the total number of bounded-encoding bits used by the QUBO form.
215+
pub fn num_encoding_bits(&self) -> usize {
216+
self.bounds.iter().map(VarBounds::num_encoding_bits).sum()
217+
}
218+
178219
/// Convert a configuration (offsets from lower bounds) to integer values.
179220
fn config_to_values(&self, config: &[usize]) -> Vec<i64> {
180221
config
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
//! Reduction from ClosestVectorProblem to QUBO.
2+
//!
3+
//! Encodes each bounded CVP coefficient with an exact in-range binary basis and
4+
//! expands the squared-distance objective into a QUBO over those bits.
5+
6+
#[cfg(feature = "example-db")]
7+
use crate::export::SolutionPair;
8+
use crate::models::algebraic::{ClosestVectorProblem, QUBO};
9+
use crate::reduction;
10+
use crate::rules::traits::{ReduceTo, ReductionResult};
11+
12+
#[derive(Debug, Clone)]
13+
struct EncodingSpan {
14+
start: usize,
15+
weights: Vec<usize>,
16+
}
17+
18+
/// Result of reducing a bounded ClosestVectorProblem instance to QUBO.
19+
#[derive(Debug, Clone)]
20+
pub struct ReductionCVPToQUBO {
21+
target: QUBO<f64>,
22+
encodings: Vec<EncodingSpan>,
23+
}
24+
25+
impl ReductionResult for ReductionCVPToQUBO {
26+
type Source = ClosestVectorProblem<i32>;
27+
type Target = QUBO<f64>;
28+
29+
fn target_problem(&self) -> &Self::Target {
30+
&self.target
31+
}
32+
33+
/// Reconstruct the source configuration offsets from the encoded QUBO bits.
34+
fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
35+
self.encodings
36+
.iter()
37+
.map(|encoding| {
38+
encoding
39+
.weights
40+
.iter()
41+
.enumerate()
42+
.map(|(offset, weight)| {
43+
target_solution
44+
.get(encoding.start + offset)
45+
.copied()
46+
.unwrap_or(0)
47+
* weight
48+
})
49+
.sum()
50+
})
51+
.collect()
52+
}
53+
}
54+
55+
#[cfg(feature = "example-db")]
56+
fn canonical_cvp_instance() -> ClosestVectorProblem<i32> {
57+
ClosestVectorProblem::new(
58+
vec![vec![2, 0], vec![1, 2]],
59+
vec![2.8, 1.5],
60+
vec![
61+
crate::models::algebraic::VarBounds::bounded(-2, 4),
62+
crate::models::algebraic::VarBounds::bounded(-2, 4),
63+
],
64+
)
65+
}
66+
67+
fn encoding_spans(problem: &ClosestVectorProblem<i32>) -> Vec<EncodingSpan> {
68+
let mut start = 0usize;
69+
let mut spans = Vec::with_capacity(problem.num_basis_vectors());
70+
for bounds in problem.bounds() {
71+
let weights = bounds
72+
.exact_encoding_weights()
73+
.into_iter()
74+
.map(|weight| usize::try_from(weight).expect("encoding weights must be nonnegative"))
75+
.collect::<Vec<_>>();
76+
spans.push(EncodingSpan { start, weights });
77+
start += spans.last().expect("just pushed").weights.len();
78+
}
79+
spans
80+
}
81+
82+
fn gram_matrix(problem: &ClosestVectorProblem<i32>) -> Vec<Vec<f64>> {
83+
let basis = problem.basis();
84+
let n = basis.len();
85+
let mut gram = vec![vec![0.0; n]; n];
86+
for i in 0..n {
87+
for j in i..n {
88+
let dot = basis[i]
89+
.iter()
90+
.zip(&basis[j])
91+
.map(|(&lhs, &rhs)| lhs as f64 * rhs as f64)
92+
.sum::<f64>();
93+
gram[i][j] = dot;
94+
gram[j][i] = dot;
95+
}
96+
}
97+
gram
98+
}
99+
100+
fn at_times_target(problem: &ClosestVectorProblem<i32>) -> Vec<f64> {
101+
problem
102+
.basis()
103+
.iter()
104+
.map(|column| {
105+
column
106+
.iter()
107+
.zip(problem.target())
108+
.map(|(&entry, &target)| entry as f64 * target)
109+
.sum()
110+
})
111+
.collect()
112+
}
113+
114+
#[reduction(overhead = { num_vars = "num_encoding_bits" })]
115+
impl ReduceTo<QUBO<f64>> for ClosestVectorProblem<i32> {
116+
type Result = ReductionCVPToQUBO;
117+
118+
fn reduce_to(&self) -> Self::Result {
119+
let encodings = encoding_spans(self);
120+
let total_bits = encodings
121+
.last()
122+
.map(|encoding| encoding.start + encoding.weights.len())
123+
.unwrap_or(0);
124+
let mut matrix = vec![vec![0.0; total_bits]; total_bits];
125+
126+
if total_bits == 0 {
127+
return ReductionCVPToQUBO {
128+
target: QUBO::from_matrix(matrix),
129+
encodings,
130+
};
131+
}
132+
133+
let gram = gram_matrix(self);
134+
let h = at_times_target(self);
135+
let lowers = self
136+
.bounds()
137+
.iter()
138+
.map(|bounds| {
139+
bounds
140+
.lower
141+
.expect("CVP QUBO reduction requires finite lower bounds")
142+
})
143+
.map(|lower| lower as f64)
144+
.collect::<Vec<_>>();
145+
let g_lo_minus_h = (0..self.num_basis_vectors())
146+
.map(|i| {
147+
(0..self.num_basis_vectors())
148+
.map(|j| gram[i][j] * lowers[j])
149+
.sum::<f64>()
150+
- h[i]
151+
})
152+
.collect::<Vec<_>>();
153+
154+
let mut bit_terms = Vec::with_capacity(total_bits);
155+
for (var_index, encoding) in encodings.iter().enumerate() {
156+
for &weight in &encoding.weights {
157+
bit_terms.push((var_index, weight as f64));
158+
}
159+
}
160+
161+
for u in 0..total_bits {
162+
let (var_u, weight_u) = bit_terms[u];
163+
matrix[u][u] =
164+
gram[var_u][var_u] * weight_u * weight_u + 2.0 * weight_u * g_lo_minus_h[var_u];
165+
166+
for v in (u + 1)..total_bits {
167+
let (var_v, weight_v) = bit_terms[v];
168+
matrix[u][v] = 2.0 * gram[var_u][var_v] * weight_u * weight_v;
169+
}
170+
}
171+
172+
ReductionCVPToQUBO {
173+
target: QUBO::from_matrix(matrix),
174+
encodings,
175+
}
176+
}
177+
}
178+
179+
#[cfg(feature = "example-db")]
180+
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
181+
vec![crate::example_db::specs::RuleExampleSpec {
182+
id: "closestvectorproblem_to_qubo",
183+
build: || {
184+
crate::example_db::specs::rule_example_with_witness::<_, QUBO<f64>>(
185+
canonical_cvp_instance(),
186+
SolutionPair {
187+
source_config: vec![3, 3],
188+
target_config: vec![0, 0, 1, 0, 0, 1],
189+
},
190+
)
191+
},
192+
}]
193+
}
194+
195+
#[cfg(test)]
196+
#[path = "../unit_tests/rules/closestvectorproblem_qubo.rs"]
197+
mod tests;

src/rules/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub use cost::{CustomCost, Minimize, MinimizeSteps, PathCostFn};
77
pub use registry::{ReductionEntry, ReductionOverhead};
88

99
pub(crate) mod circuit_spinglass;
10+
mod closestvectorproblem_qubo;
1011
pub(crate) mod coloring_qubo;
1112
pub(crate) mod factoring_circuit;
1213
mod graph;
@@ -89,6 +90,7 @@ pub use traits::{ReduceTo, ReductionAutoCast, ReductionResult};
8990
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
9091
let mut specs = Vec::new();
9192
specs.extend(circuit_spinglass::canonical_rule_example_specs());
93+
specs.extend(closestvectorproblem_qubo::canonical_rule_example_specs());
9294
specs.extend(coloring_qubo::canonical_rule_example_specs());
9395
specs.extend(factoring_circuit::canonical_rule_example_specs());
9496
specs.extend(knapsack_qubo::canonical_rule_example_specs());

0 commit comments

Comments
 (0)