1#[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#[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>, 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;