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::SimultaneousIncongruences;
10use crate::models::formula::{ksat::first_n_odd_primes, CNFClause, KSatisfiability};
11use crate::reduction;
12use crate::rules::traits::{ReduceTo, ReductionResult};
13use crate::variant::K3;
14
15#[derive(Debug, Clone)]
16pub struct Reduction3SATToSimultaneousIncongruences {
17    target: SimultaneousIncongruences,
18    variable_primes: Vec<u64>,
19}
20
21impl ReductionResult for Reduction3SATToSimultaneousIncongruences {
22    type Source = KSatisfiability<K3>;
23    type Target = SimultaneousIncongruences;
24
25    fn target_problem(&self) -> &Self::Target {
26        &self.target
27    }
28
29    fn extract_solution(
30        &self,
31        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
32    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
33        crate::rules::traits::validate_target_witness(
34            self.target_problem(),
35            target_solution,
36            |value| value.0,
37            "target witness does not satisfy the target problem",
38        )?;
39
40        Ok({
41            let x = u64::try_from(*target_solution).map_err(|_| {
42                crate::rules::ExtractionError::invalid(
43                    "target value cannot be represented in the CRT implementation domain",
44                )
45            })?;
46            self.variable_primes
47                .iter()
48                .map(|&prime| x % prime == 1)
49                .collect()
50        })
51    }
52}
53
54fn falsifying_residue(literal: i64) -> u64 {
55    if literal > 0 {
56        2
57    } else {
58        1
59    }
60}
61
62fn modular_inverse(value: u64, modulus: u64) -> Option<u64> {
63    let mut t = 0i128;
64    let mut new_t = 1i128;
65    let mut r = modulus as i128;
66    let mut new_r = value as i128;
67
68    while new_r != 0 {
69        let quotient = r / new_r;
70        (t, new_t) = (new_t, t - quotient * new_t);
71        (r, new_r) = (new_r, r - quotient * new_r);
72    }
73
74    if r != 1 {
75        return None;
76    }
77    if t < 0 {
78        t += modulus as i128;
79    }
80    Some(t as u64)
81}
82
83fn crt_residue(congruences: &[(u64, u64)]) -> Result<(u64, u64), &'static str> {
84    let modulus = congruences
85        .iter()
86        .try_fold(1u64, |product, &(m, _)| product.checked_mul(m))
87        .ok_or("CRT modulus product overflow")?;
88
89    let residue = congruences
90        .iter()
91        .try_fold(0u128, |acc, &(modulus_i, residue_i)| {
92            let partial = modulus / modulus_i;
93            let inverse = modular_inverse(partial % modulus_i, modulus_i)
94                .ok_or("CRT moduli must be pairwise coprime")?;
95            let term = u128::from(residue_i)
96                .checked_mul(u128::from(partial))
97                .and_then(|value| value.checked_mul(u128::from(inverse)))
98                .ok_or("CRT residue term overflow")?;
99            acc.checked_add(term).ok_or("CRT residue sum overflow")
100        })?
101        % modulus as u128;
102
103    Ok((residue as u64, modulus))
104}
105
106fn clause_bad_residue(
107    clause: &CNFClause,
108    variable_primes: &[u64],
109) -> Result<(u64, u64), &'static str> {
110    let mut residue_by_var = BTreeMap::new();
111    let mut contradictory_var = None;
112
113    for &literal in &clause.literals {
114        let var_index =
115            usize::try_from(literal.unsigned_abs()).map_err(|_| "literal index exceeds usize")? - 1;
116        let residue = falsifying_residue(literal);
117
118        match residue_by_var.insert(var_index, residue) {
119            Some(existing) if existing != residue => {
120                contradictory_var = Some(var_index);
121                residue_by_var.insert(var_index, 0);
122                break;
123            }
124            Some(existing) => {
125                residue_by_var.insert(var_index, existing);
126            }
127            None => {}
128        }
129    }
130
131    if let Some(var_index) = contradictory_var {
132        for &literal in &clause.literals {
133            let candidate = usize::try_from(literal.unsigned_abs())
134                .map_err(|_| "literal index exceeds usize")?
135                - 1;
136            if candidate != var_index {
137                residue_by_var
138                    .entry(candidate)
139                    .or_insert_with(|| falsifying_residue(literal));
140            }
141        }
142    }
143
144    let congruences = residue_by_var
145        .into_iter()
146        .map(|(var_index, residue)| {
147            variable_primes
148                .get(var_index)
149                .copied()
150                .map(|prime| (prime, residue))
151                .ok_or("clause variable index exceeds num_vars")
152        })
153        .collect::<Result<Vec<_>, _>>()?;
154
155    crt_residue(&congruences)
156}
157
158fn ensure_prime_product_fits_target(
159    variable_primes: &[u64],
160) -> Result<(), crate::registry::ConstructionError> {
161    let mut product = 1u128;
162    for &prime in variable_primes {
163        product = product.checked_mul(prime as u128).ok_or_else(|| {
164            format!(
165                "variable-prime product overflows for {} variables",
166                variable_primes.len()
167            )
168        })?;
169        if product > i64::MAX as u128 {
170            return Err(format!(
171                "variable-prime product {product} for {} variables exceeds the target i64 domain",
172                variable_primes.len()
173            )
174            .into());
175        }
176    }
177    Ok(())
178}
179
180#[crate::aggregate_reduction(identity)]
181impl crate::rules::AggregateReductionResult for Reduction3SATToSimultaneousIncongruences {}
182
183#[reduction(
184    transform = upper_bound {
185        num_pairs = "2 * num_vars * (num_vars + 1)^2 + num_clauses",
186    }
187)]
188impl ReduceTo<SimultaneousIncongruences> for KSatisfiability<K3> {
189    type Result = Reduction3SATToSimultaneousIncongruences;
190
191    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
192        let variable_primes = first_n_odd_primes(self.num_vars());
193        ensure_prime_product_fits_target(&variable_primes).map_err(|message| {
194            crate::rules::ReductionError::invalid_target::<
195                KSatisfiability<K3>,
196                SimultaneousIncongruences,
197            >(message.to_string())
198        })?;
199
200        let mut pairs = Vec::new();
201
202        for &prime in &variable_primes {
203            // Use (prime, prime) to forbid x ≡ 0 (mod prime), since the
204            // model requires a ≥ 1. Note: prime % prime = 0, so this is
205            // equivalent to forbidding residue 0.
206            pairs.push((prime, prime));
207            for residue in 3..prime {
208                pairs.push((residue, prime));
209            }
210        }
211
212        for clause in self.clauses() {
213            let (bad_residue, clause_modulus) = clause_bad_residue(clause, &variable_primes)
214                .map_err(|message| {
215                    crate::rules::ReductionError::invalid_target::<
216                        KSatisfiability<K3>,
217                        SimultaneousIncongruences,
218                    >(message)
219                })?;
220            // The model requires a >= 1. Use modulus instead of 0 since
221            // modulus % modulus = 0, achieving the same incongruence.
222            let a = if bad_residue == 0 {
223                clause_modulus
224            } else {
225                bad_residue
226            };
227            pairs.push((a, clause_modulus));
228        }
229
230        let pairs = pairs
231            .into_iter()
232            .map(|(residue, modulus)| {
233                Ok((
234                    i64::try_from(residue).map_err(|_| "residue exceeds i64")?,
235                    i64::try_from(modulus).map_err(|_| "modulus exceeds i64")?,
236                ))
237            })
238            .collect::<Result<Vec<_>, &str>>()
239            .map_err(|message| {
240                crate::rules::ReductionError::invalid_target::<
241                    KSatisfiability<K3>,
242                    SimultaneousIncongruences,
243                >(message)
244            })?;
245        let target = SimultaneousIncongruences::new(pairs).map_err(|message| {
246            crate::rules::ReductionError::construction::<
247                KSatisfiability<K3>,
248                SimultaneousIncongruences,
249            >(message)
250        })?;
251        Ok(Reduction3SATToSimultaneousIncongruences {
252            target,
253            variable_primes,
254        })
255    }
256}
257
258#[cfg(feature = "example-db")]
259pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
260    use crate::export::SolutionPair;
261
262    vec![crate::example_db::specs::RuleExampleSpec {
263        id: "ksatisfiability_to_simultaneous_incongruences",
264        build: || {
265            let source = KSatisfiability::<K3>::new(
266                2,
267                vec![
268                    CNFClause::new(vec![1, 2, 2]),
269                    CNFClause::new(vec![-1, 2, 2]),
270                ],
271            );
272            crate::example_db::specs::rule_example_with_witness::<_, SimultaneousIncongruences>(
273                source,
274                SolutionPair {
275                    source_config: serde_json::json!(vec![true, true]),
276                    target_config: serde_json::json!(1),
277                },
278            )
279        },
280    }]
281}
282
283#[cfg(test)]
284#[path = "../unit_tests/rules/ksatisfiability_simultaneousincongruences.rs"]
285mod tests;