Skip to main content

problemreductions/rules/
ksatisfiability_simultaneousincongruences.rs

1//! Reduction from 3-SAT to Simultaneous Incongruences.
2//!
3//! Uses distinct odd primes to encode variable assignments via residues
4//! 1 (true) and 2 (false), then forbids each clause's unique falsifying
5//! residue class via the Chinese Remainder Theorem.
6
7use std::collections::BTreeMap;
8
9use crate::models::algebraic::simultaneous_incongruences::MAX_LCM;
10use crate::models::algebraic::SimultaneousIncongruences;
11use crate::models::formula::{ksat::first_n_odd_primes, CNFClause, KSatisfiability};
12use crate::reduction;
13use crate::rules::traits::{ReduceTo, ReductionResult};
14use crate::variant::K3;
15
16#[derive(Debug, Clone)]
17pub struct Reduction3SATToSimultaneousIncongruences {
18    target: SimultaneousIncongruences,
19    variable_primes: Vec<u64>,
20}
21
22impl ReductionResult for Reduction3SATToSimultaneousIncongruences {
23    type Source = KSatisfiability<K3>;
24    type Target = SimultaneousIncongruences;
25
26    fn target_problem(&self) -> &Self::Target {
27        &self.target
28    }
29
30    fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
31        let x = target_solution.first().copied().unwrap_or(0) as u64;
32        self.variable_primes
33            .iter()
34            .map(|&prime| if x % prime == 1 { 1 } else { 0 })
35            .collect()
36    }
37}
38
39fn falsifying_residue(literal: i32) -> u64 {
40    if literal > 0 {
41        2
42    } else {
43        1
44    }
45}
46
47fn modular_inverse(value: u64, modulus: u64) -> u64 {
48    let mut t = 0i128;
49    let mut new_t = 1i128;
50    let mut r = modulus as i128;
51    let mut new_r = value as i128;
52
53    while new_r != 0 {
54        let quotient = r / new_r;
55        (t, new_t) = (new_t, t - quotient * new_t);
56        (r, new_r) = (new_r, r - quotient * new_r);
57    }
58
59    assert_eq!(r, 1, "value and modulus must be coprime");
60    if t < 0 {
61        t += modulus as i128;
62    }
63    t as u64
64}
65
66fn crt_residue(congruences: &[(u64, u64)]) -> (u64, u64) {
67    let modulus = congruences.iter().fold(1u64, |product, &(m, _)| {
68        product
69            .checked_mul(m)
70            .expect("CRT modulus product overflow")
71    });
72
73    let residue = congruences
74        .iter()
75        .fold(0u128, |acc, &(modulus_i, residue_i)| {
76            let partial = modulus / modulus_i;
77            let inverse = modular_inverse(partial % modulus_i, modulus_i);
78            acc + residue_i as u128 * partial as u128 * inverse as u128
79        })
80        % modulus as u128;
81
82    (residue as u64, modulus)
83}
84
85fn clause_bad_residue(clause: &CNFClause, variable_primes: &[u64]) -> (u64, u64) {
86    let mut residue_by_var = BTreeMap::new();
87    let mut contradictory_var = None;
88
89    for &literal in &clause.literals {
90        let var_index = literal.unsigned_abs() as usize - 1;
91        let residue = falsifying_residue(literal);
92
93        match residue_by_var.insert(var_index, residue) {
94            Some(existing) if existing != residue => {
95                contradictory_var = Some(var_index);
96                residue_by_var.insert(var_index, 0);
97                break;
98            }
99            Some(existing) => {
100                residue_by_var.insert(var_index, existing);
101            }
102            None => {}
103        }
104    }
105
106    if let Some(var_index) = contradictory_var {
107        for &literal in &clause.literals {
108            let candidate = literal.unsigned_abs() as usize - 1;
109            if candidate != var_index {
110                residue_by_var
111                    .entry(candidate)
112                    .or_insert_with(|| falsifying_residue(literal));
113            }
114        }
115    }
116
117    let congruences = residue_by_var
118        .into_iter()
119        .map(|(var_index, residue)| {
120            (
121                *variable_primes
122                    .get(var_index)
123                    .expect("clause variable index must be within num_vars"),
124                residue,
125            )
126        })
127        .collect::<Vec<_>>();
128
129    crt_residue(&congruences)
130}
131
132fn ensure_prime_product_within_lcm_cap(variable_primes: &[u64]) {
133    let mut product = 1u128;
134    for &prime in variable_primes {
135        product = product.checked_mul(prime as u128).unwrap_or_else(|| {
136            panic!(
137                "3-SAT -> SimultaneousIncongruences requires the variable-prime product to fit within the target model's LCM cap ({MAX_LCM}); num_vars={} overflows while multiplying primes",
138                variable_primes.len()
139            )
140        });
141        if product > MAX_LCM {
142            panic!(
143                "3-SAT -> SimultaneousIncongruences requires the variable-prime product to fit within the target model's LCM cap ({MAX_LCM}); num_vars={} yields prime product {product}",
144                variable_primes.len()
145            );
146        }
147    }
148}
149
150#[reduction(overhead = {
151    num_pairs = "simultaneous_incongruences_num_incongruences",
152})]
153impl ReduceTo<SimultaneousIncongruences> for KSatisfiability<K3> {
154    type Result = Reduction3SATToSimultaneousIncongruences;
155
156    fn reduce_to(&self) -> Self::Result {
157        let variable_primes = first_n_odd_primes(self.num_vars());
158        ensure_prime_product_within_lcm_cap(&variable_primes);
159
160        let mut pairs = Vec::new();
161
162        for &prime in &variable_primes {
163            // Use (prime, prime) to forbid x ≡ 0 (mod prime), since the
164            // model requires a ≥ 1. Note: prime % prime = 0, so this is
165            // equivalent to forbidding residue 0.
166            pairs.push((prime, prime));
167            for residue in 3..prime {
168                pairs.push((residue, prime));
169            }
170        }
171
172        for clause in self.clauses() {
173            let (bad_residue, clause_modulus) = clause_bad_residue(clause, &variable_primes);
174            // The model requires a >= 1. Use modulus instead of 0 since
175            // modulus % modulus = 0, achieving the same incongruence.
176            let a = if bad_residue == 0 {
177                clause_modulus
178            } else {
179                bad_residue
180            };
181            pairs.push((a, clause_modulus));
182        }
183
184        Reduction3SATToSimultaneousIncongruences {
185            target: SimultaneousIncongruences::new(pairs)
186                .expect("reduction produces valid incongruences"),
187            variable_primes,
188        }
189    }
190}
191
192#[cfg(feature = "example-db")]
193pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
194    use crate::export::SolutionPair;
195
196    vec![crate::example_db::specs::RuleExampleSpec {
197        id: "ksatisfiability_to_simultaneous_incongruences",
198        build: || {
199            let source = KSatisfiability::<K3>::new(
200                2,
201                vec![
202                    CNFClause::new(vec![1, 2, 2]),
203                    CNFClause::new(vec![-1, 2, 2]),
204                ],
205            );
206            crate::example_db::specs::rule_example_with_witness::<_, SimultaneousIncongruences>(
207                source,
208                SolutionPair {
209                    source_config: vec![1, 1],
210                    target_config: vec![1],
211                },
212            )
213        },
214    }]
215}
216
217#[cfg(test)]
218#[path = "../unit_tests/rules/ksatisfiability_simultaneousincongruences.rs"]
219mod tests;