1use crate::cdcl::{Lit, SolveResult, Solver, Var};
14use crate::pr::{check_pr_refutation, is_pr};
15use crate::proof::{Perm, ProofStep, Witness};
16use crate::sym_certify::CertifiedRefutation;
17use crate::symmetry_detect::find_generators;
18use std::collections::HashSet;
19
20pub fn find_pr_clause(num_vars: usize, clauses: &[Vec<Lit>], alpha: &[Lit]) -> Option<(Vec<Lit>, Witness)> {
25 let c: Vec<Lit> = alpha.iter().map(|l| l.negated()).collect();
27 if c.is_empty() {
28 return None;
29 }
30 let mut relevant: HashSet<Var> = c.iter().map(|l| l.var()).collect();
34 let mut reduct: Vec<Vec<Lit>> = vec![c.clone()];
35 for d in clauses {
36 if d.iter().any(|l| alpha.contains(l)) {
37 for l in d {
38 relevant.insert(l.var());
39 }
40 reduct.push(d.clone());
41 }
42 }
43 let mut solver = Solver::new(num_vars);
44 for cl in &reduct {
45 solver.add_clause(cl.clone());
46 }
47 if let SolveResult::Sat(model) = solver.solve() {
48 let omega: Vec<Lit> = relevant.iter().map(|&v| Lit::new(v, model[v as usize])).collect();
49 let witness = Witness::Assignment(omega);
50 if is_pr(num_vars, clauses, &c, &witness) {
52 return Some((c, witness));
53 }
54 }
55 None
56}
57
58fn sdcl_discover(num_vars: usize, clauses: &[Vec<Lit>]) -> (Vec<Vec<Lit>>, Vec<ProofStep>) {
69 let mut db = clauses.to_vec();
70 let mut steps: Vec<ProofStep> = Vec::new();
71 let cap = num_vars * num_vars + 8;
72 while steps.len() < cap {
73 let gens = find_generators(num_vars, &db);
74 let mut progressed = false;
75 'probe: for v in 0..num_vars as Var {
76 for &val in &[true, false] {
77 let alpha = [Lit::new(v, val)];
78 if let Some((c, w)) = find_pr_clause_unified(num_vars, &db, &alpha, &gens) {
79 if !db.iter().any(|d| d == &c) {
80 db.push(c.clone());
81 steps.push(ProofStep::Pr { clause: c, witness: w });
82 progressed = true;
83 break 'probe;
84 }
85 }
86 }
87 }
88 if !progressed {
89 break;
90 }
91 }
92 (db, steps)
93}
94
95pub fn sdcl_refute(num_vars: usize, clauses: &[Vec<Lit>]) -> CertifiedRefutation {
96 let (db, mut steps) = sdcl_discover(num_vars, clauses);
97 let sbp_clauses = steps.len();
98 let mut solver = Solver::new(num_vars);
99 for c in &db {
100 solver.add_clause(c.clone());
101 }
102 match solver.solve() {
103 SolveResult::Sat(_) => CertifiedRefutation { refuted: false, sbp_clauses, steps },
104 SolveResult::Unsat => {
105 for lc in solver.learned() {
106 steps.push(ProofStep::Rup(lc.lits.clone()));
107 }
108 if check_pr_refutation(num_vars, clauses, &steps) {
110 CertifiedRefutation { refuted: true, sbp_clauses, steps }
111 } else {
112 CertifiedRefutation { refuted: true, sbp_clauses: 0, steps: plain_cdcl_refutation(num_vars, clauses) }
113 }
114 }
115 }
116}
117
118pub enum CertifiedOutcome {
120 Unsat { steps: Vec<ProofStep>, discovered: usize },
123 Sat(Vec<bool>),
125}
126
127pub fn solve_certified(num_vars: usize, clauses: &[Vec<Lit>]) -> CertifiedOutcome {
134 let (db, mut steps) = sdcl_discover(num_vars, clauses);
135 let discovered = steps.len();
136 let mut solver = Solver::new(num_vars);
137 for c in &db {
138 solver.add_clause(c.clone());
139 }
140 match solver.solve() {
141 SolveResult::Sat(model) => CertifiedOutcome::Sat(model),
142 SolveResult::Unsat => {
143 for lc in solver.learned() {
144 steps.push(ProofStep::Rup(lc.lits.clone()));
145 }
146 if check_pr_refutation(num_vars, clauses, &steps) {
151 CertifiedOutcome::Unsat { steps, discovered }
152 } else {
153 CertifiedOutcome::Unsat { steps: plain_cdcl_refutation(num_vars, clauses), discovered: 0 }
154 }
155 }
156 }
157}
158
159pub fn plain_cdcl_refutation(num_vars: usize, clauses: &[Vec<Lit>]) -> Vec<ProofStep> {
163 let mut solver = Solver::new(num_vars);
164 for c in clauses {
165 solver.add_clause(c.clone());
166 }
167 let _ = solver.solve();
168 solver.learned().iter().map(|lc| ProofStep::Rup(lc.lits.clone())).collect()
169}
170
171pub fn find_pr_clause_unified(
177 num_vars: usize,
178 clauses: &[Vec<Lit>],
179 alpha: &[Lit],
180 generators: &[Perm],
181) -> Option<(Vec<Lit>, Witness)> {
182 if let Some(found) = find_pr_clause(num_vars, clauses, alpha) {
183 return Some(found);
184 }
185 let c: Vec<Lit> = alpha.iter().map(|l| l.negated()).collect();
186 for sigma in generators {
187 let witness = Witness::Substitution(sigma.extended(num_vars));
188 if is_pr(num_vars, clauses, &c, &witness) {
189 return Some((c, witness));
190 }
191 }
192 None
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use crate::families;
199
200 fn sat_brute(num_vars: usize, clauses: &[Vec<Lit>]) -> bool {
201 (0u32..(1u32 << num_vars)).any(|mask| {
202 let model: Vec<bool> = (0..num_vars).map(|v| (mask >> v) & 1 == 1).collect();
203 clauses.iter().all(|c| c.iter().any(|l| model[l.var() as usize] == l.is_positive()))
204 })
205 }
206
207 #[test]
208 fn sdcl_rediscovers_a_pigeonhole_pr_clause_with_no_symmetry_input() {
209 for n in 3..=4 {
213 let (cnf, _) = families::php(n);
214 let last_hole = n - 2;
215 let stuck = vec![Lit::pos(last_hole as Var)]; let (c, w) = find_pr_clause(cnf.num_vars, &cnf.clauses, &stuck)
217 .expect("SDCL must discover a PR clause");
218 assert_eq!(c, vec![Lit::neg(last_hole as Var)], "rediscovered ¬x(0, last_hole)");
219 assert!(is_pr(cnf.num_vars, &cnf.clauses, &c, &w), "and it is genuinely PR");
220 }
221 }
222
223 #[test]
224 fn sdcl_refutes_pigeonhole_fully_automatically() {
225 for n in 2..=4 {
229 let (cnf, _) = families::php(n);
230 let r = sdcl_refute(cnf.num_vars, &cnf.clauses);
231 assert!(r.refuted, "SDCL must auto-refute PHP({n})");
232 assert!(
233 check_pr_refutation(cnf.num_vars, &cnf.clauses, &r.steps),
234 "the self-discovered proof must independently re-check"
235 );
236 if n >= 3 {
238 assert!(r.sbp_clauses >= 1, "PHP({n}) needs self-discovered PR clauses");
239 }
240 }
241 }
242
243 #[test]
244 #[ignore = "scaling SDCL — auto-refutes larger pigeonhole via discovered substitution witnesses"]
245 fn sdcl_refute_scales_with_symmetry_fallback() {
246 use crate::cdcl::{SolveResult, Solver};
247 for n in 5..=7 {
248 let (cnf, _) = families::php(n);
249 let r = sdcl_refute(cnf.num_vars, &cnf.clauses);
250 assert!(r.refuted, "scaling SDCL must auto-refute PHP({n})");
251 assert!(r.sbp_clauses >= 1, "discovered PR clauses at scale");
252 assert!(check_pr_refutation(cnf.num_vars, &cnf.clauses, &r.steps));
253 let sbp: Vec<Vec<Lit>> = r
255 .steps
256 .iter()
257 .filter_map(|s| if let ProofStep::Pr { clause, .. } = s { Some(clause.clone()) } else { None })
258 .collect();
259 let mut solver = Solver::new(cnf.num_vars);
260 for c in cnf.clauses.iter().chain(sbp.iter()) {
261 solver.add_clause(c.clone());
262 }
263 assert_eq!(solver.solve(), SolveResult::Unsat);
264 println!(
265 "SDCL PHP({n}): {} self-discovered PR clauses (no symmetry input) → {} conflicts left, CERTIFIED",
266 r.sbp_clauses,
267 solver.conflicts()
268 );
269 }
270 }
271
272 #[test]
273 fn solve_certified_is_a_complete_certified_solver() {
274 use crate::pr::check_pr_refutation;
275 let (php, _) = families::php(4);
277 match solve_certified(php.num_vars, &php.clauses) {
278 CertifiedOutcome::Unsat { steps, discovered } => {
279 assert!(discovered >= 1, "SDCL discovered breaking clauses");
280 assert!(check_pr_refutation(php.num_vars, &php.clauses, &steps), "PHP(4) refutation re-checks");
281 }
282 CertifiedOutcome::Sat(_) => panic!("PHP(4) is UNSAT"),
283 }
284 let sat = vec![vec![Lit::pos(0), Lit::pos(1)], vec![Lit::neg(0), Lit::pos(1)]];
286 match solve_certified(2, &sat) {
287 CertifiedOutcome::Sat(m) => {
288 assert!(sat.iter().all(|c| c.iter().any(|l| m[l.var() as usize] == l.is_positive())), "valid model");
289 }
290 CertifiedOutcome::Unsat { .. } => panic!("formula is satisfiable"),
291 }
292 }
293
294 #[test]
295 fn solve_certified_matches_brute_force_with_checkable_proofs() {
296 use crate::pr::check_pr_refutation;
297 let mut state = 0x501_E_CE271F1Eu64;
301 let mut next = || {
302 state ^= state << 13;
303 state ^= state >> 7;
304 state ^= state << 17;
305 state
306 };
307 for _ in 0..600 {
308 let nv = 3 + (next() % 4) as usize; let nc = (next() % 12) as usize;
310 let clauses: Vec<Vec<Lit>> = (0..nc)
311 .map(|_| {
312 (0..2 + (next() % 2) as usize)
313 .map(|_| Lit::new((next() % nv as u64) as Var, next() & 1 == 0))
314 .collect::<Vec<_>>()
315 })
316 .filter(|c: &Vec<Lit>| !c.is_empty())
317 .collect();
318 let expected = sat_brute(nv, &clauses);
319 match solve_certified(nv, &clauses) {
320 CertifiedOutcome::Sat(m) => {
321 assert!(expected, "solver said SAT but brute force says UNSAT: {clauses:?}");
322 assert!(clauses.iter().all(|c| c.iter().any(|l| m[l.var() as usize] == l.is_positive())), "model invalid");
323 }
324 CertifiedOutcome::Unsat { steps, .. } => {
325 assert!(!expected, "solver said UNSAT but a model exists: {clauses:?}");
326 assert!(check_pr_refutation(nv, &clauses, &steps), "UNSAT proof must re-check: {clauses:?}");
327 }
328 }
329 }
330 }
331
332 #[test]
333 fn sdcl_discovered_clauses_preserve_satisfiability() {
334 let mut state = 0x5DC1_9999u64;
338 let mut next = || {
339 state ^= state << 13;
340 state ^= state >> 7;
341 state ^= state << 17;
342 state
343 };
344 let mut found = 0;
345 for _ in 0..4000 {
346 let nv = 3 + (next() % 4) as usize; let nc = (next() % 12) as usize;
348 let clauses: Vec<Vec<Lit>> = (0..nc)
349 .map(|_| {
350 (0..2 + (next() % 2) as usize)
351 .map(|_| Lit::new((next() % nv as u64) as Var, next() & 1 == 0))
352 .collect::<Vec<_>>()
353 })
354 .filter(|c: &Vec<Lit>| !c.is_empty())
355 .collect();
356 let mut alpha: Vec<Lit> = Vec::new();
358 for v in 0..nv as Var {
359 if next() & 1 == 0 {
360 alpha.push(Lit::new(v, next() & 1 == 0));
361 }
362 }
363 if alpha.is_empty() {
364 continue;
365 }
366 if let Some((c, _)) = find_pr_clause(nv, &clauses, &alpha) {
367 found += 1;
368 let before = sat_brute(nv, &clauses);
369 let mut with = clauses.clone();
370 with.push(c.clone());
371 assert_eq!(before, sat_brute(nv, &with), "SDCL clause changed satisfiability: {clauses:?} C={c:?}");
372 }
373 }
374 assert!(found > 0, "the discovery path must actually be exercised");
375 }
376}