logicaffeine_proof/
sym_dynamic.rs1use std::collections::HashSet;
22
23use crate::cdcl::{BudgetedResult, Lit, Solver};
24use crate::proof::ProofStep;
25use crate::symmetry_detect::find_generators;
26
27#[derive(Clone, Debug)]
29pub enum SelOutcome {
30 Unsat { steps: Vec<ProofStep>, conflicts: u64, amplified: usize },
33 Sat(Vec<bool>),
35 Unknown { conflicts: u64 },
38}
39
40fn canon(c: &[Lit]) -> Vec<u32> {
42 let mut k: Vec<u32> = c.iter().map(|l| l.var() * 2 + u32::from(!l.is_positive())).collect();
43 k.sort_unstable();
44 k.dedup();
45 k
46}
47
48fn try_add(
50 num_vars: usize,
51 db: &mut Vec<Vec<Lit>>,
52 steps: &mut Vec<ProofStep>,
53 seen: &mut HashSet<Vec<u32>>,
54 x: Vec<Lit>,
55) -> bool {
56 let key = canon(&x);
57 if seen.contains(&key) {
58 return false;
59 }
60 if crate::rup::is_rup(num_vars, db, &x) {
61 seen.insert(key);
62 db.push(x.clone());
63 steps.push(ProofStep::Rup(x));
64 true
65 } else {
66 false
67 }
68}
69
70pub fn sel_refute(num_vars: usize, clauses: &[Vec<Lit>]) -> SelOutcome {
74 let gens = find_generators(num_vars, clauses);
75 let mut db: Vec<Vec<Lit>> = clauses.to_vec();
76 let mut steps: Vec<ProofStep> = Vec::new();
77 let mut seen: HashSet<Vec<u32>> = db.iter().map(|c| canon(c)).collect();
78 let mut total_conflicts = 0u64;
79 let mut amplified = 0usize;
80 let mut budget = 64u64;
81 const MAX_ROUNDS: usize = 4000;
82
83 for _round in 0..MAX_ROUNDS {
84 if crate::rup::is_rup(num_vars, &db, &[]) {
85 break;
86 }
87 let mut solver = Solver::new(num_vars);
88 solver.set_reduce(false);
91 for c in &db {
92 solver.add_clause(c.clone());
93 }
94 let res = solver.solve_budgeted(budget);
95 total_conflicts += solver.conflicts();
96
97 match res {
98 BudgetedResult::Sat(model) => return SelOutcome::Sat(model),
99 BudgetedResult::Unsat => {
100 let learned: Vec<Vec<Lit>> = solver.learned().iter().map(|l| l.lits.clone()).collect();
102 for c in learned {
103 try_add(num_vars, &mut db, &mut steps, &mut seen, c);
104 }
105 break;
106 }
107 BudgetedResult::Budget => {
108 let learned: Vec<Vec<Lit>> = solver.learned().iter().map(|l| l.lits.clone()).collect();
109 let mut progress = false;
110 for c in learned {
111 if try_add(num_vars, &mut db, &mut steps, &mut seen, c.clone()) {
112 progress = true;
113 }
114 for g in &gens {
117 let image = g.apply_clause(&c);
118 if try_add(num_vars, &mut db, &mut steps, &mut seen, image) {
119 amplified += 1;
120 progress = true;
121 }
122 }
123 }
124 if !progress {
125 budget = budget.saturating_mul(2);
127 if budget > 1_000_000 {
128 break;
129 }
130 }
131 }
132 }
133 }
134
135 if crate::pr::check_pr_refutation_fast(num_vars, clauses, &steps) {
136 SelOutcome::Unsat { steps, conflicts: total_conflicts, amplified }
137 } else {
138 SelOutcome::Unknown { conflicts: total_conflicts }
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::cdcl::{SolveResult, Solver};
146
147 fn sat_brute(num_vars: usize, clauses: &[Vec<Lit>]) -> bool {
149 for mask in 0u32..(1u32 << num_vars) {
150 let model: Vec<bool> = (0..num_vars).map(|v| (mask >> v) & 1 == 1).collect();
151 if clauses.iter().all(|c| c.iter().any(|l| model[l.var() as usize] == l.is_positive())) {
152 return true;
153 }
154 }
155 false
156 }
157
158 #[test]
159 fn sel_never_reports_pigeonhole_satisfiable() {
160 for n in 7..=7 {
164 let (cnf, _) = crate::families::php(n);
165 match sel_refute(cnf.num_vars, &cnf.clauses) {
166 SelOutcome::Sat(_) => panic!("PHP({n}) reported SATISFIABLE — soundness violation"),
167 SelOutcome::Unsat { steps, .. } => {
168 assert!(crate::pr::check_pr_refutation_fast(cnf.num_vars, &cnf.clauses, &steps));
169 }
170 SelOutcome::Unknown { .. } => {}
171 }
172 }
173 }
174
175 #[test]
176 fn sel_certifies_pigeonhole() {
177 for n in 3..=6 {
179 let (cnf, _) = crate::families::php(n);
180 match sel_refute(cnf.num_vars, &cnf.clauses) {
181 SelOutcome::Unsat { steps, .. } => {
182 assert!(
183 crate::pr::check_pr_refutation_fast(cnf.num_vars, &cnf.clauses, &steps),
184 "PHP({n}) SEL proof must check"
185 );
186 }
187 other => panic!("PHP({n}) must be refuted, got {other:?}"),
188 }
189 }
190 }
191
192 #[test]
193 fn sel_amplification_cuts_conflicts_on_pigeonhole() {
194 let (cnf, _) = crate::families::php(6);
197 let mut plain = Solver::new(cnf.num_vars);
198 for c in &cnf.clauses {
199 plain.add_clause(c.clone());
200 }
201 assert_eq!(plain.solve(), SolveResult::Unsat);
202 let plain_conflicts = plain.conflicts();
203
204 match sel_refute(cnf.num_vars, &cnf.clauses) {
205 SelOutcome::Unsat { conflicts, amplified, .. } => {
206 assert!(amplified > 0, "symmetry amplification must actually fire on PHP");
207 eprintln!(
208 "PHP(6): plain CDCL = {plain_conflicts} conflicts, SEL = {conflicts} conflicts ({amplified} symmetric clauses), {:.1}x fewer",
209 plain_conflicts as f64 / conflicts.max(1) as f64
210 );
211 assert!(
212 conflicts < plain_conflicts,
213 "SEL conflicts ({conflicts}) must beat plain CDCL ({plain_conflicts})"
214 );
215 }
216 other => panic!("expected refutation, got {other:?}"),
217 }
218 }
219
220 #[test]
221 fn sel_never_returns_a_wrong_verdict_random() {
222 let mut state = 0xC0FFEE123456789Au64;
226 let mut next = || {
227 state = state.wrapping_add(0x9E3779B97F4A7C15);
228 let mut z = state;
229 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
230 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
231 z ^ (z >> 31)
232 };
233 let num_vars = 5usize;
234 for _ in 0..1500 {
235 let nclauses = next() as usize % 12;
236 let clauses: Vec<Vec<Lit>> = (0..nclauses)
237 .map(|_| {
238 let len = 1 + (next() as usize % 3);
239 let mut c = Vec::new();
240 for _ in 0..len {
241 let v = (next() as u32) % num_vars as u32;
242 let lit = Lit::new(v, next() & 1 == 0);
243 if !c.contains(&lit) && !c.contains(&lit.negated()) {
244 c.push(lit);
245 }
246 }
247 c
248 })
249 .filter(|c| !c.is_empty())
250 .collect();
251 let truth = sat_brute(num_vars, &clauses);
252 match sel_refute(num_vars, &clauses) {
253 SelOutcome::Unsat { steps, .. } => {
254 assert!(!truth, "SEL refuted a satisfiable formula: {clauses:?}");
255 assert!(
256 crate::pr::check_pr_refutation_fast(num_vars, &clauses, &steps),
257 "SEL Unsat proof must check: {clauses:?}"
258 );
259 }
260 SelOutcome::Sat(model) => {
261 assert!(truth, "SEL claimed SAT on an unsatisfiable formula: {clauses:?}");
262 assert!(
263 clauses.iter().all(|c| c.iter().any(|l| model[l.var() as usize] == l.is_positive())),
264 "SEL returned an invalid model"
265 );
266 }
267 SelOutcome::Unknown { .. } => {}
268 }
269 }
270 }
271}