1use std::collections::{BTreeMap, BTreeSet};
7
8use crate::models::algebraic::QuadraticCongruences;
9use crate::models::formula::KSatisfiability;
10use crate::reduction;
11use crate::rules::traits::{ReduceTo, ReductionResult};
12use crate::variant::K3;
13use num_bigint::{BigInt, BigUint};
14use num_traits::{One, Signed, Zero};
15
16#[derive(Debug, Clone)]
17pub struct Reduction3SATToQuadraticCongruences {
18 target: QuadraticCongruences,
19 source_num_vars: usize,
20 active_to_source: Vec<usize>,
21 standard_clause_count: usize,
22 h: BigUint,
23 prime_powers: Vec<BigUint>,
24}
25
26impl ReductionResult for Reduction3SATToQuadraticCongruences {
27 type Source = KSatisfiability<K3>;
28 type Target = QuadraticCongruences;
29
30 fn target_problem(&self) -> &Self::Target {
31 &self.target
32 }
33
34 fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
35 let mut source_assignment = vec![0; self.source_num_vars];
36 let Some(x) = self.target.decode_witness(target_solution) else {
37 return source_assignment;
38 };
39 if x > self.h {
40 return source_assignment;
41 }
42
43 let h_minus_x = &self.h - &x;
44 let h_plus_x = &self.h + &x;
45 let mut alpha = vec![0i8; self.prime_powers.len()];
46
47 for (j, prime_power) in self.prime_powers.iter().enumerate() {
48 if (&h_minus_x % prime_power).is_zero() {
49 alpha[j] = 1;
50 } else if (&h_plus_x % prime_power).is_zero() {
51 alpha[j] = -1;
52 }
53 }
54
55 for (active_index, &source_index) in self.active_to_source.iter().enumerate() {
56 let alpha_index = 2 * self.standard_clause_count + active_index + 1;
57 source_assignment[source_index] = if alpha.get(alpha_index) == Some(&-1) {
58 1
59 } else {
60 0
61 };
62 }
63
64 source_assignment
65 }
66}
67
68#[cfg_attr(not(any(test, feature = "example-db")), allow(dead_code))]
69#[derive(Debug, Clone)]
70struct MandersAdlemanConstruction {
71 target: QuadraticCongruences,
72 source_num_vars: usize,
73 active_to_source: Vec<usize>,
74 remapped_clause_set: BTreeSet<Vec<i32>>,
75 standard_clauses: Vec<Vec<i32>>,
76 standard_clause_count: usize,
77 active_var_count: usize,
78 #[cfg_attr(not(test), allow(dead_code))]
79 doubled_coefficients: Vec<BigInt>,
80 #[cfg_attr(not(test), allow(dead_code))]
81 tau_2: BigInt,
82 thetas: Vec<BigUint>,
83 h: BigUint,
84 prime_powers: Vec<BigUint>,
85}
86
87fn is_prime(candidate: u64) -> bool {
88 if candidate < 2 {
89 return false;
90 }
91 if candidate == 2 {
92 return true;
93 }
94 if candidate.is_multiple_of(2) {
95 return false;
96 }
97
98 let mut divisor = 3u64;
99 while divisor * divisor <= candidate {
100 if candidate.is_multiple_of(divisor) {
101 return false;
102 }
103 divisor += 2;
104 }
105 true
106}
107
108fn admissible_primes(count: usize) -> Vec<u64> {
109 let mut primes = Vec::with_capacity(count);
110 let mut candidate = 13u64;
111 while primes.len() < count {
112 if is_prime(candidate) {
113 primes.push(candidate);
114 }
115 candidate += 1;
116 }
117 primes
118}
119
120fn pow_biguint_u64(base: u64, exp: usize) -> BigUint {
121 let mut result = BigUint::one();
122 let factor = BigUint::from(base);
123 for _ in 0..exp {
124 result *= &factor;
125 }
126 result
127}
128
129fn bigint_mod_to_biguint(value: &BigInt, modulus: &BigUint) -> BigUint {
130 let modulus_bigint = BigInt::from(modulus.clone());
131 let reduced = ((value % &modulus_bigint) + &modulus_bigint) % &modulus_bigint;
132 reduced
133 .to_biguint()
134 .expect("nonnegative reduced residue must fit BigUint")
135}
136
137fn modular_inverse(value: &BigUint, modulus: &BigUint) -> BigUint {
138 let mut t = BigInt::zero();
139 let mut new_t = BigInt::one();
140 let mut r = BigInt::from(modulus.clone());
141 let mut new_r = BigInt::from(value.clone() % modulus);
142
143 while !new_r.is_zero() {
144 let quotient = &r / &new_r;
145 let next_t = &t - "ient * &new_t;
146 let next_r = &r - "ient * &new_r;
147 t = new_t;
148 new_t = next_t;
149 r = new_r;
150 new_r = next_r;
151 }
152
153 assert_eq!(r, BigInt::one(), "value and modulus must be coprime");
154 if t.is_negative() {
155 t += BigInt::from(modulus.clone());
156 }
157 t.to_biguint().expect("inverse must be nonnegative")
158}
159
160fn normalize_clause(clause: &[i32]) -> Option<Vec<i32>> {
161 let mut lits = BTreeSet::new();
162 for &lit in clause {
163 if lits.contains(&-lit) {
164 return None;
165 }
166 lits.insert(lit);
167 }
168 Some(lits.into_iter().collect())
169}
170
171fn preprocess_formula(source: &KSatisfiability<K3>) -> (Vec<Vec<i32>>, Vec<usize>) {
172 let mut seen = BTreeSet::new();
173 let mut normalized_clauses = Vec::new();
174 let mut active_vars = BTreeSet::new();
175
176 for clause in source.clauses() {
177 let Some(normalized) = normalize_clause(&clause.literals) else {
178 continue;
179 };
180 if normalized.len() != 3 {
181 panic!(
182 "3-SAT -> QuadraticCongruences requires each non-tautological clause to use three distinct literals; got {:?}",
183 clause.literals
184 );
185 }
186
187 let distinct_vars: BTreeSet<_> = normalized
188 .iter()
189 .map(|lit| lit.unsigned_abs() as usize)
190 .collect();
191 if distinct_vars.len() != 3 {
192 panic!(
193 "3-SAT -> QuadraticCongruences requires each non-tautological clause to use three distinct variables; got {:?}",
194 clause.literals
195 );
196 }
197
198 if seen.insert(normalized.clone()) {
199 for &lit in &normalized {
200 active_vars.insert(lit.unsigned_abs() as usize);
201 }
202 normalized_clauses.push(normalized);
203 }
204 }
205
206 (normalized_clauses, active_vars.into_iter().collect())
207}
208
209fn build_standard_clauses(num_active_vars: usize) -> (Vec<Vec<i32>>, BTreeMap<Vec<i32>, usize>) {
210 let mut clauses = Vec::new();
211 let mut index = BTreeMap::new();
212
213 if num_active_vars < 3 {
214 return (clauses, index);
215 }
216
217 for i in 1..=num_active_vars - 2 {
218 for j in i + 1..=num_active_vars - 1 {
219 for k in j + 1..=num_active_vars {
220 for s1 in [1i32, -1] {
221 for s2 in [1i32, -1] {
222 for s3 in [1i32, -1] {
223 let mut clause = vec![s1 * i as i32, s2 * j as i32, s3 * k as i32];
224 clause.sort_unstable();
225 index.insert(clause.clone(), clauses.len() + 1);
226 clauses.push(clause);
227 }
228 }
229 }
230 }
231 }
232 }
233
234 (clauses, index)
235}
236
237fn pow8_table(max_power: usize) -> Vec<BigUint> {
238 let mut table = Vec::with_capacity(max_power + 1);
239 table.push(BigUint::one());
240 for _ in 0..max_power {
241 let next = table.last().expect("pow8 table is nonempty") * BigUint::from(8u32);
242 table.push(next);
243 }
244 table
245}
246
247fn build_construction(source: &KSatisfiability<K3>) -> MandersAdlemanConstruction {
248 let (normalized_clauses, active_vars) = preprocess_formula(source);
249 let active_var_count = active_vars.len();
250 let var_map: BTreeMap<usize, usize> = active_vars
251 .iter()
252 .enumerate()
253 .map(|(new_index, &old_index)| (old_index, new_index + 1))
254 .collect();
255
256 let remapped_clauses = normalized_clauses
257 .iter()
258 .map(|clause| {
259 let mut remapped = clause
260 .iter()
261 .map(|&lit| {
262 let var = lit.unsigned_abs() as usize;
263 let new_var = *var_map
264 .get(&var)
265 .expect("active variable must be present in the remapping");
266 if lit > 0 {
267 new_var as i32
268 } else {
269 -(new_var as i32)
270 }
271 })
272 .collect::<Vec<_>>();
273 remapped.sort_unstable();
274 remapped
275 })
276 .collect::<Vec<_>>();
277 let remapped_clause_set = remapped_clauses.iter().cloned().collect::<BTreeSet<_>>();
278
279 let (standard_clauses, standard_index) = build_standard_clauses(active_var_count);
280 let standard_clause_count = standard_clauses.len();
281 let aux_dimension = 2 * standard_clause_count + active_var_count;
282 let pow8 = pow8_table(standard_clause_count + 1);
283
284 let mut tau_phi = BigInt::zero();
285 for clause in &remapped_clauses {
286 if let Some(&j) = standard_index.get(clause) {
287 tau_phi -= BigInt::from(pow8[j].clone());
288 }
289 }
290
291 let mut positive_occurrences = vec![BigInt::zero(); active_var_count + 1];
292 let mut negative_occurrences = vec![BigInt::zero(); active_var_count + 1];
293 for (j, clause) in standard_clauses.iter().enumerate() {
294 let weight = BigInt::from(pow8[j + 1].clone());
295 for &lit in clause {
296 let var = lit.unsigned_abs() as usize;
297 if lit > 0 {
298 positive_occurrences[var] += &weight;
299 } else {
300 negative_occurrences[var] += &weight;
301 }
302 }
303 }
304
305 let mut doubled_coefficients = vec![BigInt::zero(); aux_dimension + 1];
306 doubled_coefficients[0] = BigInt::from(2u32);
307 for k in 1..=standard_clause_count {
308 let weight = BigInt::from(pow8[k].clone());
309 doubled_coefficients[2 * k - 1] = -weight.clone();
310 doubled_coefficients[2 * k] = -(weight * BigInt::from(2u32));
311 }
312 for i in 1..=active_var_count {
313 doubled_coefficients[2 * standard_clause_count + i] =
314 &positive_occurrences[i] - &negative_occurrences[i];
315 }
316
317 let sum_coefficients = doubled_coefficients
318 .iter()
319 .cloned()
320 .fold(BigInt::zero(), |acc, value| acc + value);
321 let sum_negative_occurrences = negative_occurrences
322 .iter()
323 .skip(1)
324 .cloned()
325 .fold(BigInt::zero(), |acc, value| acc + value);
326 let tau_2 = BigInt::from(2u32) * tau_phi
327 + sum_coefficients
328 + BigInt::from(2u32) * sum_negative_occurrences;
329 let mod_val = BigUint::from(2u32) * pow8[standard_clause_count + 1].clone();
330
331 let primes = admissible_primes(aux_dimension + 1);
332 let prime_powers = primes
333 .iter()
334 .map(|&prime| pow_biguint_u64(prime, aux_dimension + 1))
335 .collect::<Vec<_>>();
336 let k = prime_powers
337 .iter()
338 .cloned()
339 .fold(BigUint::one(), |acc, value| acc * value);
340
341 let mut thetas = Vec::with_capacity(aux_dimension + 1);
342 for j in 0..=aux_dimension {
343 let other = &k / &prime_powers[j];
344 let lcm = &other * &mod_val;
345 let residue = bigint_mod_to_biguint(&doubled_coefficients[j], &mod_val);
346 let inverse = modular_inverse(&(other.clone() % &mod_val), &mod_val);
347 let mut theta = (&other * ((&residue * inverse) % &mod_val)) % &lcm;
348 if theta.is_zero() {
349 theta = lcm.clone();
350 }
351 let prime = BigUint::from(primes[j]);
352 while (&theta % &prime).is_zero() {
353 theta += &lcm;
354 }
355 thetas.push(theta);
356 }
357
358 let h = thetas
359 .iter()
360 .cloned()
361 .fold(BigUint::zero(), |acc, value| acc + value);
362 let beta = &mod_val * &k;
363 let inverse_factor = &mod_val + &k;
364 let inverse = modular_inverse(&(inverse_factor % &beta), &beta);
365 let tau_2_squared = (&tau_2 * &tau_2)
366 .to_biguint()
367 .expect("squared doubled target must be nonnegative");
368 let alpha = (&inverse * ((&k * tau_2_squared) + (&mod_val * (&h * &h)))) % β
369 let target = QuadraticCongruences::new(alpha, beta, &h + BigUint::one());
370
371 MandersAdlemanConstruction {
372 target,
373 source_num_vars: source.num_vars(),
374 active_to_source: active_vars.into_iter().map(|var| var - 1).collect(),
375 remapped_clause_set,
376 standard_clauses,
377 standard_clause_count,
378 active_var_count,
379 doubled_coefficients,
380 tau_2,
381 thetas,
382 h,
383 prime_powers,
384 }
385}
386
387#[cfg(any(test, feature = "example-db"))]
388fn build_alphas(
389 construction: &MandersAdlemanConstruction,
390 assignment: &[usize],
391) -> Option<Vec<i8>> {
392 if assignment.len() != construction.source_num_vars {
393 return None;
394 }
395
396 let mut active_assignment = vec![0i8; construction.active_var_count + 1];
397 for (active_index, &source_index) in construction.active_to_source.iter().enumerate() {
398 active_assignment[active_index + 1] = if assignment[source_index] == 0 { 0 } else { 1 };
399 }
400
401 let mut alphas =
402 vec![0i8; 2 * construction.standard_clause_count + construction.active_var_count + 1];
403 alphas[0] = 1;
404
405 for i in 1..=construction.active_var_count {
406 alphas[2 * construction.standard_clause_count + i] = 1 - 2 * active_assignment[i];
407 }
408
409 for k in 1..=construction.standard_clause_count {
410 let clause = &construction.standard_clauses[k - 1];
411 let mut y = 0i32;
412 for &lit in clause {
413 let var = lit.unsigned_abs() as usize;
414 if lit > 0 {
415 y += i32::from(active_assignment[var]);
416 } else {
417 y += 1 - i32::from(active_assignment[var]);
418 }
419 }
420 if construction.remapped_clause_set.contains(clause) {
421 y -= 1;
422 }
423
424 match 3 - 2 * y {
425 3 => {
426 alphas[2 * k - 1] = 1;
427 alphas[2 * k] = 1;
428 }
429 1 => {
430 alphas[2 * k - 1] = -1;
431 alphas[2 * k] = 1;
432 }
433 -1 => {
434 alphas[2 * k - 1] = 1;
435 alphas[2 * k] = -1;
436 }
437 -3 => {
438 alphas[2 * k - 1] = -1;
439 alphas[2 * k] = -1;
440 }
441 _ => return None,
442 }
443 }
444
445 Some(alphas)
446}
447
448#[cfg(any(test, feature = "example-db"))]
449fn witness_value_from_alphas(alphas: &[i8], thetas: &[BigUint]) -> BigUint {
450 let signed_sum = alphas
451 .iter()
452 .zip(thetas)
453 .fold(BigInt::zero(), |acc, (&alpha, theta)| {
454 if alpha == 1 {
455 acc + BigInt::from(theta.clone())
456 } else {
457 acc - BigInt::from(theta.clone())
458 }
459 });
460 signed_sum
461 .abs()
462 .to_biguint()
463 .expect("absolute witness must be nonnegative")
464}
465
466#[cfg(any(test, feature = "example-db"))]
467fn witness_config_for_assignment(
468 source: &KSatisfiability<K3>,
469 assignment: &[usize],
470) -> Option<Vec<usize>> {
471 let construction = build_construction(source);
472 let alphas = build_alphas(&construction, assignment)?;
473 let witness = witness_value_from_alphas(&alphas, &construction.thetas);
474 construction.target.encode_witness(&witness)
475}
476
477#[cfg(test)]
478fn exhaustive_alpha_solution(source: &KSatisfiability<K3>) -> Option<Vec<i8>> {
479 let construction = build_construction(source);
480 let n = construction.doubled_coefficients.len();
481 if n >= usize::BITS as usize {
482 return None;
483 }
484
485 for bits in 0usize..(1usize << n) {
486 let alphas = (0..n)
487 .map(|j| if (bits >> j) & 1 == 1 { 1i8 } else { -1i8 })
488 .collect::<Vec<_>>();
489 let sum = construction
490 .doubled_coefficients
491 .iter()
492 .zip(&alphas)
493 .fold(BigInt::zero(), |acc, (coefficient, &alpha)| {
494 acc + coefficient * BigInt::from(alpha)
495 });
496 if sum == construction.tau_2 {
497 return Some(alphas);
498 }
499 }
500
501 None
502}
503
504#[reduction(overhead = {
505 bit_length_a = "(num_vars + num_clauses)^2 * log(num_vars + num_clauses + 1)",
506 bit_length_b = "(num_vars + num_clauses)^2 * log(num_vars + num_clauses + 1)",
507 bit_length_c = "(num_vars + num_clauses)^2 * log(num_vars + num_clauses + 1)",
508})]
509impl ReduceTo<QuadraticCongruences> for KSatisfiability<K3> {
510 type Result = Reduction3SATToQuadraticCongruences;
511
512 fn reduce_to(&self) -> Self::Result {
513 let construction = build_construction(self);
514 Reduction3SATToQuadraticCongruences {
515 target: construction.target,
516 source_num_vars: construction.source_num_vars,
517 active_to_source: construction.active_to_source,
518 standard_clause_count: construction.standard_clause_count,
519 h: construction.h,
520 prime_powers: construction.prime_powers,
521 }
522 }
523}
524
525#[cfg(feature = "example-db")]
526pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
527 use crate::export::SolutionPair;
528
529 vec![crate::example_db::specs::RuleExampleSpec {
530 id: "ksatisfiability_to_quadraticcongruences",
531 build: || {
532 let source = KSatisfiability::<K3>::new(
533 3,
534 vec![crate::models::formula::CNFClause::new(vec![1, 2, 3])],
535 );
536 let target_config = witness_config_for_assignment(&source, &[1, 0, 0])
537 .expect("canonical satisfying assignment should lift to a QC witness");
538 crate::example_db::specs::rule_example_with_witness::<_, QuadraticCongruences>(
539 source,
540 SolutionPair {
541 source_config: vec![1, 0, 0],
542 target_config,
543 },
544 )
545 },
546 }]
547}
548
549#[cfg(test)]
550#[path = "../unit_tests/rules/ksatisfiability_quadraticcongruences.rs"]
551mod tests;