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::Zero;
14
15type Source = ClosestVectorProblem<i64>;
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>, 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
121    let matrix = rows
122        .iter()
123        .map(|&row| {
124            problem
125                .basis()
126                .iter()
127                .map(|column| column[row])
128                .collect::<Vec<_>>()
129        })
130        .collect::<Vec<_>>();
131    if determinant(&matrix)? == 0 {
132        return Err(
133            crate::rules::ReductionError::invalid_target::<Source, Target>(
134                "selected closest-vector rows are not independent",
135            ),
136        );
137    }
138
139    let target_norm = problem.target().iter().try_fold(0_i64, |total, &value| {
140        total
141            .checked_add(
142                value
143                    .checked_abs()
144                    .ok_or_else(|| overflow("taking a closest-vector target absolute value"))?,
145            )
146            .ok_or_else(|| overflow("computing the closest-vector target one-norm"))
147    })?;
148    let row_bounds = rows
149        .iter()
150        .map(|&row| {
151            problem.target()[row]
152                .checked_abs()
153                .and_then(|value| value.checked_add(target_norm))
154                .ok_or_else(|| overflow("computing a closest-vector selected-row bound"))
155        })
156        .collect::<Result<Vec<_>, _>>()?;
157
158    (0..size)
159        .map(|coefficient| {
160            (0..size).try_fold(0_i64, |bound, selected_row| {
161                let minor = (0..size)
162                    .filter(|&row| row != selected_row)
163                    .map(|row| {
164                        (0..size)
165                            .filter(|&column| column != coefficient)
166                            .map(|column| matrix[row][column])
167                            .collect::<Vec<_>>()
168                    })
169                    .collect::<Vec<_>>();
170                let adjugate_magnitude = determinant(&minor)?
171                    .checked_abs()
172                    .ok_or_else(|| overflow("taking a closest-vector cofactor absolute value"))?;
173                let term = adjugate_magnitude
174                    .checked_mul(row_bounds[selected_row])
175                    .ok_or_else(|| overflow("computing a closest-vector coefficient bound"))?;
176                bound
177                    .checked_add(term)
178                    .ok_or_else(|| overflow("computing a closest-vector coefficient bound"))
179            })
180        })
181        .collect()
182}
183
184fn exact_range_weights(maximum: i64) -> Result<Vec<i64>, crate::rules::ReductionError> {
185    let mut weights = Vec::new();
186    let mut remaining = maximum;
187    let mut power = 1_i64;
188    while remaining > 0 {
189        let weight = power.min(remaining);
190        weights.push(weight);
191        remaining -= weight;
192        if remaining > 0 {
193            power = power
194                .checked_mul(2)
195                .ok_or_else(|| overflow("computing closest-vector encoding weights"))?;
196        }
197    }
198    Ok(weights)
199}
200
201fn encoding_spans(bounds: &[i64]) -> Result<Vec<EncodingSpan>, crate::rules::ReductionError> {
202    let mut start = 0usize;
203    bounds
204        .iter()
205        .map(|&bound| {
206            let maximum = bound
207                .checked_mul(2)
208                .ok_or_else(|| overflow("computing a closest-vector encoding range"))?;
209            let weights = exact_range_weights(maximum)?;
210            let span = EncodingSpan {
211                start,
212                weights,
213                lower: -bound,
214            };
215            start = start
216                .checked_add(span.weights.len())
217                .ok_or_else(|| overflow("computing closest-vector encoding offsets"))?;
218            Ok(span)
219        })
220        .collect()
221}
222
223fn dot(left: &[i64], right: &[i64], operation: &str) -> Result<i64, crate::rules::ReductionError> {
224    left.iter()
225        .zip(right)
226        .try_fold(0_i64, |total, (&left, &right)| {
227            let product = left.checked_mul(right).ok_or_else(|| overflow(operation))?;
228            total
229                .checked_add(product)
230                .ok_or_else(|| overflow(operation))
231        })
232}
233
234#[reduction(transform = unavailable {
235    num_vars = "the exact encoding size depends on the concrete basis and target values",
236})]
237impl ReduceTo<QUBO<i64>> for ClosestVectorProblem<i64> {
238    type Result = ReductionCVPToQUBO;
239
240    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
241        let bounds = coefficient_bounds(self)?;
242        let encodings = encoding_spans(&bounds)?;
243        let total_bits = encodings
244            .last()
245            .map(|encoding| encoding.start + encoding.weights.len())
246            .unwrap_or(0);
247
248        let size = self.num_basis_vectors();
249        let mut gram = vec![vec![0_i64; size]; size];
250        for (i, row) in gram.iter_mut().enumerate() {
251            for (j, entry) in row.iter_mut().enumerate() {
252                *entry = dot(
253                    &self.basis()[i],
254                    &self.basis()[j],
255                    "computing a closest-vector Gram entry",
256                )?;
257            }
258        }
259        let h = self
260            .basis()
261            .iter()
262            .map(|column| {
263                dot(
264                    column,
265                    self.target(),
266                    "computing a closest-vector target projection",
267                )
268            })
269            .collect::<Result<Vec<_>, _>>()?;
270        let linear = (0..size)
271            .map(|i| {
272                let product = (0..size).try_fold(0_i64, |total, j| {
273                    let term = gram[i][j]
274                        .checked_mul(encodings[j].lower)
275                        .ok_or_else(|| overflow("computing a closest-vector linear term"))?;
276                    total
277                        .checked_add(term)
278                        .ok_or_else(|| overflow("computing a closest-vector linear term"))
279                })?;
280                product
281                    .checked_sub(h[i])
282                    .ok_or_else(|| overflow("computing a closest-vector linear term"))
283            })
284            .collect::<Result<Vec<_>, _>>()?;
285
286        let bit_terms = encodings
287            .iter()
288            .enumerate()
289            .flat_map(|(coefficient, encoding)| {
290                encoding
291                    .weights
292                    .iter()
293                    .map(move |&weight| (coefficient, weight))
294            })
295            .collect::<Vec<_>>();
296        let mut integer_matrix = vec![vec![0_i64; total_bits]; total_bits];
297        for u in 0..total_bits {
298            let (coefficient_u, weight_u) = bit_terms[u];
299            let quadratic = gram[coefficient_u][coefficient_u]
300                .checked_mul(weight_u)
301                .and_then(|value| value.checked_mul(weight_u))
302                .ok_or_else(|| overflow("computing a closest-vector QUBO diagonal"))?;
303            let linear_term = linear[coefficient_u]
304                .checked_mul(weight_u)
305                .and_then(|value| value.checked_mul(2))
306                .ok_or_else(|| overflow("computing a closest-vector QUBO diagonal"))?;
307            integer_matrix[u][u] = quadratic
308                .checked_add(linear_term)
309                .ok_or_else(|| overflow("computing a closest-vector QUBO diagonal"))?;
310
311            for v in (u + 1)..total_bits {
312                let (coefficient_v, weight_v) = bit_terms[v];
313                integer_matrix[u][v] = gram[coefficient_u][coefficient_v]
314                    .checked_mul(weight_u)
315                    .and_then(|value| value.checked_mul(weight_v))
316                    .and_then(|value| value.checked_mul(2))
317                    .ok_or_else(|| overflow("computing a closest-vector QUBO interaction"))?;
318            }
319        }
320
321        Ok(ReductionCVPToQUBO {
322            target: QUBO::from_matrix(integer_matrix)
323                .map_err(crate::rules::ReductionError::construction::<Source, Target>)?,
324            encodings,
325        })
326    }
327}
328
329#[cfg(feature = "example-db")]
330fn canonical_cvp_instance() -> Source {
331    ClosestVectorProblem::new(vec![vec![2, 0], vec![1, 2]], vec![3_i64, 2])
332        .expect("canonical closest-vector instance must be valid")
333}
334
335#[cfg(feature = "example-db")]
336pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
337    vec![crate::example_db::specs::RuleExampleSpec {
338        id: "closestvectorproblem_to_qubo",
339        build: || {
340            crate::example_db::specs::rule_example_with_witness::<_, QUBO<i64>>(
341                canonical_cvp_instance(),
342                SolutionPair {
343                    source_config: serde_json::json!(vec![1, 1]),
344                    target_config: serde_json::json!(vec![
345                        false, false, false, true, true, false, false, true, false, false, true,
346                    ]),
347                },
348            )
349        },
350    }]
351}
352
353#[cfg(test)]
354#[path = "../unit_tests/rules/closestvectorproblem_qubo.rs"]
355mod tests;