Skip to content

Commit 9827a1e

Browse files
GiggleLiuclaude
andcommitted
feat: add ClosestVectorProblem model (#90)
Add CVP as a new optimization model parameterized by element type T (i32/f64). Implements Problem and OptimizationProblem traits, registers in CLI dispatch with "CVP" alias, and adds problem definition to paper. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0c534dd commit 9827a1e

7 files changed

Lines changed: 384 additions & 2 deletions

File tree

docs/paper/reductions.typ

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"PaintShop": [Paint Shop],
5151
"BicliqueCover": [Biclique Cover],
5252
"BinPacking": [Bin Packing],
53+
"ClosestVectorProblem": [Closest Vector Problem],
5354
)
5455

5556
// Definition label: "def:<ProblemName>" — each definition block must have a matching label
@@ -637,6 +638,14 @@ Integer Linear Programming is a universal modeling framework: virtually every NP
637638
) <fig:ilp-example>
638639
]
639640

641+
#problem-def("ClosestVectorProblem")[
642+
Given a lattice basis $bold(B) in RR^(m times n)$ (columns $bold(b)_1, dots, bold(b)_n in RR^m$ spanning lattice $cal(L)(bold(B)) = {bold(B) bold(x) : bold(x) in ZZ^n}$) and target $bold(t) in RR^m$, find $bold(x) in ZZ^n$ minimizing $norm(bold(B) bold(x) - bold(t))_2$.
643+
][
644+
The Closest Vector Problem is a fundamental lattice problem, proven NP-hard by van Emde Boas @vanemde1981. CVP plays a central role in lattice-based cryptography and the geometry of numbers. Kannan's algorithm @kannan1987 solves CVP in $O^*(n^n)$ time using the Hermite normal form, later improved to $O^*(2^n)$ via the randomized sieve of Micciancio and Voulgaris @micciancio2010. CVP is closely related to the Shortest Vector Problem (SVP) and integer programming: Lenstra's algorithm for fixed-dimensional ILP @lenstra1983 proceeds via CVP in the dual lattice.
645+
646+
*Example.* Consider the 2D lattice with basis $bold(b)_1 = (2, 0)^top$, $bold(b)_2 = (1, 2)^top$ and target $bold(t) = (2.8, 1.5)^top$. The lattice points near $bold(t)$ include $bold(B)(1, 0)^top = (2, 0)^top$, $bold(B)(1, 1)^top = (3, 2)^top$, and $bold(B)(0, 1)^top = (1, 2)^top$. The closest is $bold(B)(1, 1)^top = (3, 2)^top$ with distance $norm((0.2, 0.5))_2 approx 0.539$.
647+
]
648+
640649
== Satisfiability Problems
641650

642651
#problem-def("Satisfiability")[

problemreductions-cli/src/dispatch.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use anyhow::{bail, Context, Result};
2-
use problemreductions::models::optimization::{BinPacking, ILP};
2+
use problemreductions::models::optimization::{BinPacking, ClosestVectorProblem, ILP};
33
use problemreductions::prelude::*;
44
use problemreductions::rules::{MinimizeSteps, ReductionGraph};
55
use problemreductions::solvers::{BruteForce, ILPSolver, Solver};
@@ -239,6 +239,10 @@ pub fn load_problem(
239239
Some("f64") => deser_opt::<BinPacking<f64>>(data),
240240
_ => deser_opt::<BinPacking<i32>>(data),
241241
},
242+
"ClosestVectorProblem" => match variant.get("weight").map(|s| s.as_str()) {
243+
Some("f64") => deser_opt::<ClosestVectorProblem<f64>>(data),
244+
_ => deser_opt::<ClosestVectorProblem<i32>>(data),
245+
},
242246
_ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)),
243247
}
244248
}
@@ -294,6 +298,10 @@ pub fn serialize_any_problem(
294298
Some("f64") => try_ser::<BinPacking<f64>>(any),
295299
_ => try_ser::<BinPacking<i32>>(any),
296300
},
301+
"ClosestVectorProblem" => match variant.get("weight").map(|s| s.as_str()) {
302+
Some("f64") => try_ser::<ClosestVectorProblem<f64>>(any),
303+
_ => try_ser::<ClosestVectorProblem<i32>>(any),
304+
},
297305
_ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)),
298306
}
299307
}

problemreductions-cli/src/problem_name.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub const ALIASES: &[(&str, &str)] = &[
2020
("KSAT", "KSatisfiability"),
2121
("TSP", "TravelingSalesman"),
2222
("BP", "BinPacking"),
23+
("CVP", "ClosestVectorProblem"),
2324
];
2425

2526
/// Resolve a short alias to the canonical problem name.
@@ -49,6 +50,7 @@ pub fn resolve_alias(input: &str) -> String {
4950
"bmf" => "BMF".to_string(),
5051
"bicliquecover" => "BicliqueCover".to_string(),
5152
"bp" | "binpacking" => "BinPacking".to_string(),
53+
"cvp" | "closestvectorproblem" => "ClosestVectorProblem".to_string(),
5254
_ => input.to_string(), // pass-through for exact names
5355
}
5456
}

src/models/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub use graph::{
1313
KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching,
1414
MinimumDominatingSet, MinimumVertexCover, TravelingSalesman,
1515
};
16-
pub use optimization::{BinPacking, SpinGlass, ILP, QUBO};
16+
pub use optimization::{BinPacking, ClosestVectorProblem, SpinGlass, ILP, QUBO};
1717
pub use satisfiability::{CNFClause, KSatisfiability, Satisfiability};
1818
pub use set::{MaximumSetPacking, MinimumSetCovering};
1919
pub use specialized::{BicliqueCover, CircuitSAT, Factoring, PaintShop, BMF};
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
//! Closest Vector Problem (CVP) implementation.
2+
//!
3+
//! Given a lattice basis B and target vector t, find integer coefficients x
4+
//! minimizing ‖Bx - t‖₂.
5+
6+
use crate::models::optimization::VarBounds;
7+
use crate::registry::{FieldInfo, ProblemSchemaEntry};
8+
use crate::traits::{OptimizationProblem, Problem};
9+
use crate::types::{Direction, SolutionSize};
10+
use serde::{Deserialize, Serialize};
11+
12+
inventory::submit! {
13+
ProblemSchemaEntry {
14+
name: "ClosestVectorProblem",
15+
module_path: module_path!(),
16+
description: "Find the closest lattice point to a target vector",
17+
fields: &[
18+
FieldInfo { name: "basis", type_name: "Vec<Vec<T>>", description: "Basis matrix B as column vectors" },
19+
FieldInfo { name: "target", type_name: "Vec<f64>", description: "Target vector t" },
20+
FieldInfo { name: "bounds", type_name: "Vec<VarBounds>", description: "Integer bounds per variable" },
21+
],
22+
}
23+
}
24+
25+
/// Closest Vector Problem (CVP).
26+
///
27+
/// Given a lattice basis B ∈ R^{m×n} and target t ∈ R^m,
28+
/// find integer x ∈ Z^n minimizing ‖Bx - t‖₂.
29+
///
30+
/// Variables are integer coefficients with explicit bounds for enumeration.
31+
/// The configuration encoding follows ILP: config[i] is an offset from bounds[i].lower.
32+
#[derive(Debug, Clone, Serialize, Deserialize)]
33+
pub struct ClosestVectorProblem<T> {
34+
/// Basis matrix B stored as n column vectors, each of dimension m.
35+
basis: Vec<Vec<T>>,
36+
/// Target vector t ∈ R^m.
37+
target: Vec<f64>,
38+
/// Integer bounds per variable for enumeration.
39+
bounds: Vec<VarBounds>,
40+
}
41+
42+
impl<T> ClosestVectorProblem<T> {
43+
/// Create a new CVP instance.
44+
///
45+
/// # Arguments
46+
/// * `basis` - n column vectors of dimension m
47+
/// * `target` - target vector of dimension m
48+
/// * `bounds` - integer bounds per variable (length n)
49+
///
50+
/// # Panics
51+
/// Panics if basis/bounds lengths mismatch or dimensions are inconsistent.
52+
pub fn new(basis: Vec<Vec<T>>, target: Vec<f64>, bounds: Vec<VarBounds>) -> Self {
53+
let n = basis.len();
54+
assert_eq!(
55+
bounds.len(),
56+
n,
57+
"bounds length must match number of basis vectors"
58+
);
59+
let m = target.len();
60+
for (i, col) in basis.iter().enumerate() {
61+
assert_eq!(
62+
col.len(),
63+
m,
64+
"basis vector {i} has length {}, expected {m}",
65+
col.len()
66+
);
67+
}
68+
Self {
69+
basis,
70+
target,
71+
bounds,
72+
}
73+
}
74+
75+
/// Number of basis vectors (lattice dimension n).
76+
pub fn num_basis_vectors(&self) -> usize {
77+
self.basis.len()
78+
}
79+
80+
/// Dimension of the ambient space (m).
81+
pub fn ambient_dimension(&self) -> usize {
82+
self.target.len()
83+
}
84+
85+
/// Access the basis matrix.
86+
pub fn basis(&self) -> &[Vec<T>] {
87+
&self.basis
88+
}
89+
90+
/// Access the target vector.
91+
pub fn target(&self) -> &[f64] {
92+
&self.target
93+
}
94+
95+
/// Access the variable bounds.
96+
pub fn bounds(&self) -> &[VarBounds] {
97+
&self.bounds
98+
}
99+
100+
/// Convert a configuration (offsets from lower bounds) to integer values.
101+
fn config_to_values(&self, config: &[usize]) -> Vec<i64> {
102+
config
103+
.iter()
104+
.enumerate()
105+
.map(|(i, &c)| {
106+
let lo = self.bounds.get(i).and_then(|b| b.lower).unwrap_or(0);
107+
lo + c as i64
108+
})
109+
.collect()
110+
}
111+
}
112+
113+
impl<T> Problem for ClosestVectorProblem<T>
114+
where
115+
T: Clone
116+
+ Into<f64>
117+
+ crate::variant::VariantParam
118+
+ Serialize
119+
+ for<'de> Deserialize<'de>
120+
+ std::fmt::Debug
121+
+ 'static,
122+
{
123+
const NAME: &'static str = "ClosestVectorProblem";
124+
type Metric = SolutionSize<f64>;
125+
126+
fn dims(&self) -> Vec<usize> {
127+
self.bounds
128+
.iter()
129+
.map(|b| {
130+
b.num_values().expect(
131+
"CVP brute-force enumeration requires all variables to have finite bounds",
132+
)
133+
})
134+
.collect()
135+
}
136+
137+
fn evaluate(&self, config: &[usize]) -> SolutionSize<f64> {
138+
let values = self.config_to_values(config);
139+
let m = self.ambient_dimension();
140+
let mut diff = vec![0.0f64; m];
141+
for (i, &x_i) in values.iter().enumerate() {
142+
for (j, b_ji) in self.basis[i].iter().enumerate() {
143+
diff[j] += x_i as f64 * b_ji.clone().into();
144+
}
145+
}
146+
for (d, t) in diff.iter_mut().zip(self.target.iter()) {
147+
*d -= t;
148+
}
149+
let norm = diff.iter().map(|d| d * d).sum::<f64>().sqrt();
150+
SolutionSize::Valid(norm)
151+
}
152+
153+
fn variant() -> Vec<(&'static str, &'static str)> {
154+
crate::variant_params![T]
155+
}
156+
}
157+
158+
impl<T> OptimizationProblem for ClosestVectorProblem<T>
159+
where
160+
T: Clone
161+
+ Into<f64>
162+
+ crate::variant::VariantParam
163+
+ Serialize
164+
+ for<'de> Deserialize<'de>
165+
+ std::fmt::Debug
166+
+ 'static,
167+
{
168+
type Value = f64;
169+
170+
fn direction(&self) -> Direction {
171+
Direction::Minimize
172+
}
173+
}
174+
175+
crate::declare_variants! {
176+
ClosestVectorProblem<i32> => "exp(num_basis_vectors)",
177+
ClosestVectorProblem<f64> => "exp(num_basis_vectors)",
178+
}
179+
180+
#[cfg(test)]
181+
#[path = "../../unit_tests/models/optimization/closest_vector_problem.rs"]
182+
mod tests;

src/models/optimization/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22
//!
33
//! This module contains optimization problems:
44
//! - [`BinPacking`]: Bin Packing (minimize bins)
5+
//! - [`ClosestVectorProblem`]: Closest Vector Problem (minimize lattice distance)
56
//! - [`SpinGlass`]: Ising model Hamiltonian
67
//! - [`QUBO`]: Quadratic Unconstrained Binary Optimization
78
//! - [`ILP`]: Integer Linear Programming
89
910
mod bin_packing;
11+
mod closest_vector_problem;
1012
mod ilp;
1113
mod qubo;
1214
mod spin_glass;
1315

1416
pub use bin_packing::BinPacking;
17+
pub use closest_vector_problem::ClosestVectorProblem;
1518
pub use ilp::{Comparison, LinearConstraint, ObjectiveSense, VarBounds, ILP};
1619
pub use qubo::QUBO;
1720
pub use spin_glass::SpinGlass;

0 commit comments

Comments
 (0)