Skip to main content

logicaffeine_proof/
lll.rs

1//! Satisfiability **from sparsity** — the Lovász Local Lemma certificate and its constructive
2//! Moser–Tardos witness. The lower bookend to the first-moment bound.
3//!
4//! The first-moment bound ([`crate::families::ksat_threshold_first_moment_upper`]) says: *above* a clause
5//! density the expected number of solutions vanishes, so the formula is UNSAT with high probability. This
6//! module is the opposite side: a formula that is *locally sparse* — every clause shares variables with
7//! few others — is **satisfiable**, and the witness can be **constructed**.
8//!
9//! Symmetric LLL: a uniform random assignment violates a width-`w` clause with probability `2⁻ʷ`. If each
10//! clause shares a variable with at most `d` others and `e · p · (d+1) ≤ 1` (where `p = 2^{−w_min}` is the
11//! worst-case violation probability), then some assignment satisfies *all* clauses. This is:
12//! - **sound** — the LLL theorem (and we fuzz it against brute force: a certificate never lies);
13//! - **re-checkable** — recompute `w_min` and `d`;
14//! - **constructive** — Moser–Tardos resampling reaches a model in expected `O(m)` steps under the
15//!   condition, so the certificate is also a *witness recipe*.
16
17use crate::cdcl::Lit;
18
19/// A re-checkable SAT certificate from the Lovász Local Lemma: the minimum clause width `w_min` and the
20/// maximum dependency degree `d` that together satisfy `e · 2^{−w_min} · (d+1) ≤ 1`.
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct LllSatCert {
23    pub min_width: usize,
24    pub max_degree: usize,
25}
26
27/// The LLL **dependency degree**: the maximum, over all clauses, of the number of *other* clauses that
28/// share at least one variable with it. Clauses over disjoint variable sets are independent events and do
29/// not count toward the degree.
30pub fn lll_dependency_degree(clauses: &[Vec<Lit>]) -> usize {
31    use std::collections::{HashMap, HashSet};
32    let mut occ: HashMap<u32, Vec<usize>> = HashMap::new();
33    for (i, c) in clauses.iter().enumerate() {
34        for l in c {
35            occ.entry(l.var()).or_default().push(i);
36        }
37    }
38    let mut max_d = 0;
39    for (i, c) in clauses.iter().enumerate() {
40        let mut neigh: HashSet<usize> = HashSet::new();
41        for l in c {
42            for &j in &occ[&l.var()] {
43                if j != i {
44                    neigh.insert(j);
45                }
46            }
47        }
48        max_d = max_d.max(neigh.len());
49    }
50    max_d
51}
52
53/// Certify satisfiability from sparsity via the symmetric LLL. Returns the witnessing degrees when
54/// `e · 2^{−w_min} · (d+1) ≤ 1`; otherwise `None` (the condition is *sufficient, not necessary* — `None`
55/// means "this certificate does not apply", never "UNSAT"). An empty clause set is vacuously SAT; a set
56/// containing an empty clause can never be certified (it is UNSAT).
57pub fn lll_certifies_sat(clauses: &[Vec<Lit>]) -> Option<LllSatCert> {
58    if clauses.is_empty() {
59        return Some(LllSatCert { min_width: usize::MAX, max_degree: 0 });
60    }
61    let min_width = clauses.iter().map(|c| c.len()).min().unwrap();
62    if min_width == 0 {
63        return None; // an empty clause is unsatisfiable
64    }
65    let d = lll_dependency_degree(clauses);
66    let p = 2f64.powi(-(min_width as i32));
67    if std::f64::consts::E * p * (d as f64 + 1.0) <= 1.0 {
68        Some(LllSatCert { min_width, max_degree: d })
69    } else {
70        None
71    }
72}
73
74/// Constructively find a satisfying assignment by **Moser–Tardos resampling**: start from a random
75/// assignment and, while some clause is violated, resample the variables of one violated clause. Under
76/// the LLL condition this terminates in expected `O(m)` resamples; the `max_resamples` cap is a safety
77/// net (only reachable when the condition does not hold). Returns a model, or `None` if the cap is hit.
78pub fn moser_tardos_witness(
79    num_vars: usize,
80    clauses: &[Vec<Lit>],
81    seed: u64,
82    max_resamples: usize,
83) -> Option<Vec<bool>> {
84    let mut rng = seed;
85    let mut next = move || {
86        rng = rng.wrapping_add(0x9E3779B97F4A7C15);
87        let mut z = rng;
88        z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
89        z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
90        z ^ (z >> 31)
91    };
92    let mut assign: Vec<bool> = (0..num_vars).map(|_| next() & 1 == 0).collect();
93    let violated = |a: &[bool], c: &[Lit]| c.iter().all(|l| a[l.var() as usize] != l.is_positive());
94    for _ in 0..max_resamples {
95        match clauses.iter().find(|c| violated(&assign, c)) {
96            None => return Some(assign),
97            Some(c) => {
98                for l in c {
99                    assign[l.var() as usize] = next() & 1 == 0;
100                }
101            }
102        }
103    }
104    clauses.iter().all(|c| !violated(&assign, c)).then_some(assign)
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    fn brute_sat(nv: usize, clauses: &[Vec<Lit>]) -> bool {
112        (0u64..(1u64 << nv))
113            .any(|x| clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive())))
114    }
115
116    // A decorrelated instance seed — never seed along SplitMix64's own increment γ (see the
117    // seed-collapse lesson: `s·γ` makes consecutive trials the same stream shifted by one).
118    fn seed_of(tag: u64, i: u64) -> u64 {
119        let mut z = tag.wrapping_mul(0xD1B5_4A32_D192_ED03).wrapping_add(i).wrapping_add(0x9E3779B97F4A7C15);
120        z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
121        z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
122        z ^ (z >> 31)
123    }
124
125    /// **The LLL certificate never lies — soundness against brute force.** Over a sweep of sparse-to-dense
126    /// random k-SAT, whenever `lll_certifies_sat` fires, brute force must confirm the instance is
127    /// satisfiable. The certificate is a *sufficient* condition, so we only check the one direction it
128    /// claims (Some ⟹ SAT); and we require it to fire at least once so the test is not vacuous.
129    #[test]
130    fn lll_certificate_is_sound_against_brute_force() {
131        let n = 14usize;
132        let mut fired = 0usize;
133        for k in 3..=5usize {
134            // very low densities — this is where local sparsity holds
135            for &num in &[2usize, 3, 4, 6, 8, 10, 14, 20] {
136                for t in 0..12u64 {
137                    let tag = (k as u64) << 40 ^ (num as u64) << 20;
138                    let cnf = crate::families::random_ksat(k, n, num, seed_of(tag, t));
139                    if let Some(cert) = lll_certifies_sat(&cnf.clauses) {
140                        fired += 1;
141                        assert!(
142                            brute_sat(n, &cnf.clauses),
143                            "LLL certified SAT but brute force says UNSAT: k={k} m={num} cert={cert:?}"
144                        );
145                        // the certificate re-checks: recomputing degree and width reproduces it
146                        assert_eq!(lll_dependency_degree(&cnf.clauses), cert.max_degree);
147                    }
148                }
149            }
150        }
151        assert!(fired >= 5, "the LLL certificate fired on too few instances to be meaningful: {fired}");
152    }
153
154    /// **Moser–Tardos constructs the witness whenever the LLL certifies.** The certificate is not just an
155    /// existence proof — when it fires, resampling reaches an actual model, and that model is verified to
156    /// satisfy every clause. This makes the LLL the constructive, witness-producing lower bookend.
157    #[test]
158    fn moser_tardos_constructs_a_model_under_the_lll_condition() {
159        let n = 16usize;
160        let mut constructed = 0usize;
161        for k in 3..=5usize {
162            for &num in &[2usize, 3, 4, 6, 8, 12] {
163                for t in 0..10u64 {
164                    let tag = (k as u64) << 40 ^ (num as u64) << 20 ^ 0xA5;
165                    let cnf = crate::families::random_ksat(k, n, num, seed_of(tag, t));
166                    if lll_certifies_sat(&cnf.clauses).is_some() {
167                        let model = moser_tardos_witness(n, &cnf.clauses, seed_of(tag ^ 0xF00D, t), 100 * num.max(1))
168                            .expect("under the LLL condition Moser–Tardos must reach a model");
169                        assert!(
170                            cnf.clauses.iter().all(|c| c.iter().any(|l| model[l.var() as usize] == l.is_positive())),
171                            "the constructed witness must satisfy every clause"
172                        );
173                        constructed += 1;
174                    }
175                }
176            }
177        }
178        assert!(constructed >= 5, "constructed too few witnesses to be meaningful: {constructed}");
179    }
180
181    /// **The two bookends are consistent and complementary.** A sparse instance the LLL certifies SAT sits
182    /// far *below* the first-moment density `α*(k)` (which only forbids satisfiability *above* it) — so the
183    /// SAT certificate and the UNSAT bound never disagree, they bracket the threshold from opposite sides.
184    #[test]
185    fn lll_sat_region_sits_below_the_first_moment_unsat_bound() {
186        let n = 16usize;
187        let k = 4usize;
188        let alpha_star = crate::families::ksat_threshold_first_moment_upper(k as u32);
189        let mut checked = 0usize;
190        for &num in &[3usize, 4, 5] {
191            for t in 0..8u64 {
192                let cnf = crate::families::random_ksat(k, n, num, seed_of(0xBEEF, (num as u64) << 8 ^ t));
193                if lll_certifies_sat(&cnf.clauses).is_some() {
194                    let alpha = num as f64 / n as f64;
195                    assert!(alpha < alpha_star, "an LLL-certified instance must lie below α*({k})={alpha_star:.3}, got α={alpha:.3}");
196                    assert!(brute_sat(n, &cnf.clauses), "and it is genuinely SAT");
197                    checked += 1;
198                }
199            }
200        }
201        assert!(checked >= 1, "expected at least one LLL-certified sparse instance");
202    }
203}