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}