problemreductions/rules/
closestvectorproblem_qubo.rs1#[cfg(feature = "example-db")]
8use crate::export::SolutionPair;
9use crate::models::algebraic::{ClosestVectorProblem, QUBO};
10use crate::reduction;
11use crate::rules::traits::{ReduceTo, ReductionResult};
12use num_bigint::BigInt;
13use num_traits::{Signed, Zero};
14
15type Source = ClosestVectorProblem;
16type Target = QUBO<i64>;
17
18#[derive(Debug, Clone)]
19struct EncodingSpan {
20 start: usize,
21 weights: Vec<i64>,
22 lower: i64,
23}
24
25#[derive(Debug, Clone)]
27pub struct ReductionCVPToQUBO {
28 target: Target,
29 encodings: Vec<EncodingSpan>,
30}
31
32impl ReductionResult for ReductionCVPToQUBO {
33 type Source = Source;
34 type Target = Target;
35
36 fn target_problem(&self) -> &Self::Target {
37 &self.target
38 }
39
40 fn extract_solution(
41 &self,
42 target_solution: &<Self::Target as crate::traits::Problem>::Solution,
43 ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
44 crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?;
45
46 self.encodings
47 .iter()
48 .map(|encoding| {
49 let offset = encoding.weights.iter().enumerate().try_fold(
50 0_i64,
51 |offset, (index, &weight)| {
52 if target_solution[encoding.start + index] {
53 offset.checked_add(weight)
54 } else {
55 Some(offset)
56 }
57 },
58 );
59 offset
60 .and_then(|offset| encoding.lower.checked_add(offset))
61 .ok_or_else(|| {
62 crate::rules::ExtractionError::invalid(
63 "decoded closest-vector coefficient overflows i64",
64 )
65 })
66 })
67 .collect()
68 }
69}
70
71fn overflow(operation: &str) -> crate::rules::ReductionError {
72 crate::rules::ReductionError::integer_overflow::<Source, Target>(operation)
73}
74
75fn determinant(matrix: &[Vec<i64>]) -> Result<i64, crate::rules::ReductionError> {
76 let size = matrix.len();
77 if size == 0 {
78 return Ok(1);
79 }
80 let mut matrix: Vec<Vec<BigInt>> = matrix
83 .iter()
84 .map(|row| row.iter().copied().map(BigInt::from).collect())
85 .collect();
86 let mut previous_pivot = BigInt::from(1);
87 let mut negative = false;
88 for column in 0..size - 1 {
89 let Some(pivot_row) = (column..size).find(|&row| !matrix[row][column].is_zero()) else {
90 return Ok(0);
91 };
92 if pivot_row != column {
93 matrix.swap(column, pivot_row);
94 negative = !negative;
95 }
96 let pivot = matrix[column][column].clone();
97 for row in column + 1..size {
98 for next_column in column + 1..size {
99 matrix[row][next_column] = (&matrix[row][next_column] * &pivot
100 - &matrix[row][column] * &matrix[column][next_column])
101 / &previous_pivot;
102 }
103 matrix[row][column] = BigInt::zero();
104 }
105 previous_pivot = pivot;
106 }
107 let value = matrix[size - 1][size - 1].clone();
108 i64::try_from(if negative { -value } else { value })
109 .map_err(|_| overflow("computing a closest-vector determinant"))
110}
111
112fn coefficient_bounds(problem: &Source) -> Result<Vec<(i64, i64)>, crate::rules::ReductionError> {
113 let rows = problem
114 .independent_rows()
115 .map_err(crate::rules::ReductionError::construction::<Source, Target>)?;
116 let size = problem.num_basis_vectors();
117 if size == 0 {
118 return Ok(Vec::new());
119 }
120 let matrix: Vec<Vec<_>> = rows
121 .iter()
122 .map(|&row| problem.basis().iter().map(|column| column[row]).collect())
123 .collect();
124 let determinant = determinant(&matrix)?;
125 if determinant == 0 {
126 return Err(
127 crate::rules::ReductionError::invalid_target::<Source, Target>(
128 "selected closest-vector rows are not independent",
129 ),
130 );
131 }
132 let denominator = BigInt::from(determinant).abs();
133 let mut adjugate = Vec::new();
134 for coefficient in 0..size {
135 let mut row = Vec::new();
136 for selected_row in 0..size {
137 let minor: Vec<Vec<_>> = (0..size)
138 .filter(|&row| row != selected_row)
139 .map(|row| {
140 (0..size)
141 .filter(|&column| column != coefficient)
142 .map(|column| matrix[row][column])
143 .collect()
144 })
145 .collect();
146 let mut entry = BigInt::from(self::determinant(&minor)?);
147 if (coefficient + selected_row) % 2 == 1 {
148 entry = -entry;
149 }
150 if determinant < 0 {
151 entry = -entry;
152 }
153 row.push(entry);
154 }
155 adjugate.push(row);
156 }
157 let centers: Vec<BigInt> = adjugate
160 .iter()
161 .map(|row| {
162 row.iter()
163 .zip(&rows)
164 .map(|(entry, &coordinate)| entry * problem.target()[coordinate])
165 .sum()
166 })
167 .collect();
168 let candidate: Vec<BigInt> = centers
169 .iter()
170 .map(|center| {
171 let mut quotient = center / &denominator;
172 let remainder = center % &denominator;
173 if remainder.abs() * 2 >= denominator {
174 quotient += remainder.signum();
175 }
176 quotient
177 })
178 .collect();
179 let mut radius_squared = BigInt::zero();
180 let mut zero_distance = BigInt::zero();
181 for coordinate in 0..problem.ambient_dimension() {
182 let target = BigInt::from(problem.target()[coordinate]);
183 let lattice: BigInt = problem
184 .basis()
185 .iter()
186 .zip(&candidate)
187 .map(|(column, coefficient)| coefficient * column[coordinate])
188 .sum();
189 let residual = lattice - ⌖
190 radius_squared += &residual * &residual;
191 zero_distance += &target * ⌖
192 }
193 radius_squared = radius_squared.min(zero_distance);
194 let floor = |numerator: &BigInt| {
195 let quotient = numerator / &denominator;
196 if numerator.is_negative() && !(numerator % &denominator).is_zero() {
197 quotient - 1
198 } else {
199 quotient
200 }
201 };
202 centers
203 .iter()
204 .zip(&adjugate)
205 .map(|(center, row)| {
206 let norm: BigInt = row.iter().map(|entry| entry * entry).sum();
209 let radius = BigInt::from(
210 (norm * &radius_squared)
211 .to_biguint()
212 .expect("squared radius is nonnegative")
213 .sqrt(),
214 );
215 let lower = -floor(&(&radius - center));
216 let upper = floor(&(center + radius));
217 Ok((
218 i64::try_from(lower)
219 .map_err(|_| overflow("bounding closest-vector coefficients"))?,
220 i64::try_from(upper)
221 .map_err(|_| overflow("bounding closest-vector coefficients"))?,
222 ))
223 })
224 .collect()
225}
226
227fn exact_range_weights(maximum: i64) -> Result<Vec<i64>, crate::rules::ReductionError> {
228 let mut weights = Vec::new();
229 let mut remaining = maximum;
230 let mut power = 1_i64;
231 while remaining > 0 {
232 let weight = power.min(remaining);
233 weights.push(weight);
234 remaining -= weight;
235 if remaining > 0 {
236 power = power
237 .checked_mul(2)
238 .ok_or_else(|| overflow("computing closest-vector encoding weights"))?;
239 }
240 }
241 Ok(weights)
242}
243
244fn encoding_spans(
245 bounds: &[(i64, i64)],
246) -> Result<Vec<EncodingSpan>, crate::rules::ReductionError> {
247 let mut start = 0usize;
248 bounds
249 .iter()
250 .map(|&(lower, upper)| {
251 let maximum = upper
252 .checked_sub(lower)
253 .ok_or_else(|| overflow("computing a closest-vector encoding range"))?;
254 let weights = exact_range_weights(maximum)?;
255 let span = EncodingSpan {
256 start,
257 weights,
258 lower,
259 };
260 start = start
261 .checked_add(span.weights.len())
262 .ok_or_else(|| overflow("computing closest-vector encoding offsets"))?;
263 Ok(span)
264 })
265 .collect()
266}
267
268fn dot(left: &[i64], right: &[i64], operation: &str) -> Result<i64, crate::rules::ReductionError> {
269 left.iter()
270 .zip(right)
271 .try_fold(0_i64, |total, (&left, &right)| {
272 let product = left.checked_mul(right).ok_or_else(|| overflow(operation))?;
273 total
274 .checked_add(product)
275 .ok_or_else(|| overflow(operation))
276 })
277}
278
279#[reduction(transform = upper_bound {
285 num_vars = "coefficient_box_bits",
286 num_quadratic_terms = "coefficient_box_bits^2 / 2",
287})]
288impl ReduceTo<QUBO<i64>> for ClosestVectorProblem {
289 type Result = ReductionCVPToQUBO;
290
291 fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
292 let bounds = coefficient_bounds(self)?;
293 let encodings = encoding_spans(&bounds)?;
294 let total_bits = encodings
295 .last()
296 .map(|encoding| encoding.start + encoding.weights.len())
297 .unwrap_or(0);
298
299 let size = self.num_basis_vectors();
300 let mut gram = vec![vec![0_i64; size]; size];
301 for (i, row) in gram.iter_mut().enumerate() {
302 for (j, entry) in row.iter_mut().enumerate() {
303 *entry = dot(
304 &self.basis()[i],
305 &self.basis()[j],
306 "computing a closest-vector Gram entry",
307 )?;
308 }
309 }
310 let h = self
311 .basis()
312 .iter()
313 .map(|column| {
314 dot(
315 column,
316 self.target(),
317 "computing a closest-vector target projection",
318 )
319 })
320 .collect::<Result<Vec<_>, _>>()?;
321 let linear = (0..size)
322 .map(|i| {
323 let product = (0..size).try_fold(0_i64, |total, j| {
324 let term = gram[i][j]
325 .checked_mul(encodings[j].lower)
326 .ok_or_else(|| overflow("computing a closest-vector linear term"))?;
327 total
328 .checked_add(term)
329 .ok_or_else(|| overflow("computing a closest-vector linear term"))
330 })?;
331 product
332 .checked_sub(h[i])
333 .ok_or_else(|| overflow("computing a closest-vector linear term"))
334 })
335 .collect::<Result<Vec<_>, _>>()?;
336
337 let bit_terms = encodings
338 .iter()
339 .enumerate()
340 .flat_map(|(coefficient, encoding)| {
341 encoding
342 .weights
343 .iter()
344 .map(move |&weight| (coefficient, weight))
345 })
346 .collect::<Vec<_>>();
347 let mut integer_matrix = vec![vec![0_i64; total_bits]; total_bits];
348 for u in 0..total_bits {
349 let (coefficient_u, weight_u) = bit_terms[u];
350 let quadratic = gram[coefficient_u][coefficient_u]
351 .checked_mul(weight_u)
352 .and_then(|value| value.checked_mul(weight_u))
353 .ok_or_else(|| overflow("computing a closest-vector QUBO diagonal"))?;
354 let linear_term = linear[coefficient_u]
355 .checked_mul(weight_u)
356 .and_then(|value| value.checked_mul(2))
357 .ok_or_else(|| overflow("computing a closest-vector QUBO diagonal"))?;
358 integer_matrix[u][u] = quadratic
359 .checked_add(linear_term)
360 .ok_or_else(|| overflow("computing a closest-vector QUBO diagonal"))?;
361
362 for v in (u + 1)..total_bits {
363 let (coefficient_v, weight_v) = bit_terms[v];
364 integer_matrix[u][v] = gram[coefficient_u][coefficient_v]
365 .checked_mul(weight_u)
366 .and_then(|value| value.checked_mul(weight_v))
367 .and_then(|value| value.checked_mul(2))
368 .ok_or_else(|| overflow("computing a closest-vector QUBO interaction"))?;
369 }
370 }
371
372 Ok(ReductionCVPToQUBO {
373 target: QUBO::from_matrix(integer_matrix)
374 .map_err(crate::rules::ReductionError::construction::<Source, Target>)?,
375 encodings,
376 })
377 }
378}
379
380#[cfg(feature = "example-db")]
381fn canonical_cvp_instance() -> Source {
382 ClosestVectorProblem::new(vec![vec![2, 0], vec![1, 2]], vec![3_i64, 1])
383 .expect("canonical closest-vector instance must be valid")
384}
385
386#[cfg(feature = "example-db")]
387pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
388 vec![crate::example_db::specs::RuleExampleSpec {
389 id: "closestvectorproblem_to_qubo",
390 build: || {
391 crate::example_db::specs::rule_example_with_witness::<_, QUBO<i64>>(
392 canonical_cvp_instance(),
393 SolutionPair {
394 source_config: serde_json::json!(vec![1, 1]),
395 target_config: serde_json::json!(vec![true]),
396 },
397 )
398 },
399 }]
400}
401
402#[cfg(test)]
403#[path = "../unit_tests/rules/closestvectorproblem_qubo.rs"]
404mod tests;