Skip to main content

problemreductions/rules/
closestvectorproblem_qubo.rs

1//! Reduction from integer-target CVP to QUBO.
2//!
3//! The reduction derives a finite coefficient box from the lattice basis and
4//! target, then expands the squared Euclidean distance over exact-range binary
5//! encodings.
6
7#[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/// Result of reducing an integer-target CVP instance to QUBO.
26#[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    // Pivoted Bareiss elimination uses exact division and cubic arithmetic work.
81    // BigInt preserves cancellation when intermediate products exceed i64.
82    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    // Cramer's rule gives a rational center. A rounded center supplies a
158    // feasible lattice point, so its squared residual bounds the optimum.
159    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 - &target;
190        radius_squared += &residual * &residual;
191        zero_distance += &target * &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            // Cauchy-Schwarz: |det(A) z_i - center_i|² <= ||adj_i||² R².
207            // The left side is integer, so the integer square root is exact here.
208            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// The source's cached geometric bound sums the bits of individual coefficient intervals,
280// including the zero-distance and empty-basis cases. It uses the same
281// selected determinant and a Hadamard bound, without computing the intervals.
282// QUBO counts only off-diagonal terms: at most N(N-1)/2<=N^2/2;
283// the latter bound also composes monotonically through bounded dimensions.
284#[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;