1use crate::cdcl::Lit;
31use crate::proof::{ProofStep, Witness};
32use crate::rup;
33
34pub fn check_pr_refutation(num_vars: usize, original: &[Vec<Lit>], steps: &[ProofStep]) -> bool {
38 let mut db: Vec<Vec<Lit>> = original.to_vec();
39 for step in steps {
40 match step {
41 ProofStep::Rup(c) => {
42 if !rup::is_rup(num_vars, &db, c) {
43 return false;
44 }
45 db.push(c.clone());
46 }
47 ProofStep::Pr { clause, witness } => {
48 if !is_pr(num_vars, &db, clause, witness) {
49 return false;
50 }
51 db.push(clause.clone());
52 }
53 ProofStep::Delete(clause) => {
54 let key = canon_clause(clause);
56 if let Some(pos) = db.iter().position(|d| canon_clause(d) == key) {
57 db.swap_remove(pos);
58 }
59 }
60 }
61 }
62 rup::is_rup(num_vars, &db, &[])
63}
64
65fn canon_clause(c: &[Lit]) -> Vec<u32> {
67 let mut k: Vec<u32> = c.iter().map(|l| l.var() * 2 + u32::from(!l.is_positive())).collect();
68 k.sort_unstable();
69 k.dedup();
70 k
71}
72
73pub fn is_pr(num_vars: usize, db: &[Vec<Lit>], clause: &[Lit], witness: &Witness) -> bool {
75 match witness {
76 Witness::Assignment(omega) => assignment_pr(num_vars, db, clause, omega),
77 Witness::Substitution(sigma) => substitution_sr(num_vars, db, clause, sigma),
78 }
79}
80
81pub fn is_pr_indexed(
87 num_vars: usize,
88 db: &[Vec<Lit>],
89 index: &mut crate::symmetry_detect::AutomorphismIndex,
90 clause: &[Lit],
91 witness: &Witness,
92) -> bool {
93 match witness {
94 Witness::Assignment(omega) => assignment_pr(num_vars, db, clause, omega),
95 Witness::Substitution(sigma) => {
96 if !index.is_automorphism(sigma) {
97 return false;
98 }
99 let sigma_c = sigma.apply_clause(clause);
100 let mut assume: Vec<Lit> = clause.iter().map(|l| l.negated()).collect();
101 assume.extend(sigma_c.iter().map(|l| l.negated()));
102 let _ = db;
104 index.propagate_to_conflict(num_vars, &assume)
105 }
106 }
107}
108
109pub fn check_pr_refutation_fast(num_vars: usize, original: &[Vec<Lit>], steps: &[ProofStep]) -> bool {
114 if steps.iter().any(|s| matches!(s, ProofStep::Delete(_))) {
115 return check_pr_refutation(num_vars, original, steps);
116 }
117 let mut db: Vec<Vec<Lit>> = original.to_vec();
118 let mut index = crate::symmetry_detect::AutomorphismIndex::with_clauses(num_vars, original);
119 for step in steps {
120 match step {
121 ProofStep::Rup(c) => {
122 if !rup::is_rup(num_vars, &db, c) {
123 return false;
124 }
125 db.push(c.clone());
126 index.insert(c.clone());
127 }
128 ProofStep::Pr { clause, witness } => {
129 if !is_pr_indexed(num_vars, &db, &mut index, clause, witness) {
130 return false;
131 }
132 db.push(clause.clone());
133 index.insert(clause.clone());
134 }
135 ProofStep::Delete(_) => unreachable!("deletes handled by the fallback above"),
136 }
137 }
138 rup::is_rup(num_vars, &db, &[])
139}
140
141fn substitution_sr(num_vars: usize, db: &[Vec<Lit>], clause: &[Lit], sigma: &crate::proof::Perm) -> bool {
147 if !crate::symmetry_detect::perm_is_automorphism(db, sigma) {
148 return false;
149 }
150 let sigma_c = sigma.apply_clause(clause);
152 let mut assume: Vec<Lit> = clause.iter().map(|l| l.negated()).collect();
153 assume.extend(sigma_c.iter().map(|l| l.negated()));
154 up_conflict(num_vars, db, &assume)
155}
156
157fn assignment_pr(num_vars: usize, db: &[Vec<Lit>], clause: &[Lit], omega_true: &[Lit]) -> bool {
161 let mut omega: Vec<Option<bool>> = vec![None; num_vars];
163 for &l in omega_true {
164 if !rup::set_true(&mut omega, l) {
165 return false;
166 }
167 }
168 if !clause.iter().any(|&l| rup::lit_val(&omega, l) == Some(true)) {
170 return false;
171 }
172 let alpha: Vec<Lit> = clause.iter().map(|&l| l.negated()).collect();
174
175 for d in db {
176 if d.iter().any(|&m| rup::lit_val(&omega, m) == Some(true)) {
178 continue;
179 }
180 let mut assume = alpha.clone();
184 for &m in d {
185 if rup::lit_val(&omega, m).is_none() {
186 assume.push(m.negated());
187 }
188 }
189 if !up_conflict(num_vars, db, &assume) {
190 return false;
191 }
192 }
193 true
194}
195
196fn up_conflict(num_vars: usize, db: &[Vec<Lit>], assume: &[Lit]) -> bool {
199 let mut assign: Vec<Option<bool>> = vec![None; num_vars];
200 for &l in assume {
201 if !rup::set_true(&mut assign, l) {
202 return true;
203 }
204 }
205 rup::propagate(db, &mut assign)
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use crate::cdcl::Lit;
212 use crate::proof::Perm;
213
214 fn p(v: u32) -> Lit {
215 Lit::pos(v)
216 }
217 fn n(v: u32) -> Lit {
218 Lit::neg(v)
219 }
220
221 fn sat_brute(num_vars: usize, clauses: &[Vec<Lit>]) -> bool {
223 for mask in 0u32..(1u32 << num_vars) {
224 let model: Vec<bool> = (0..num_vars).map(|v| (mask >> v) & 1 == 1).collect();
225 if clauses.iter().all(|c| c.iter().any(|l| model[l.var() as usize] == l.is_positive())) {
226 return true;
227 }
228 }
229 false
230 }
231
232 #[test]
233 fn pr_accepts_a_model_removing_clause_that_rup_rejects() {
234 let (a, b) = (0u32, 1u32);
238 let f = vec![vec![p(a), p(b)]];
239 let c = vec![n(a), p(b)];
240
241 assert!(!rup::is_rup(2, &f, &c), "a model-removing clause is not RUP");
242 let omega = Witness::Assignment(vec![n(a), p(b)]);
243 assert!(is_pr(2, &f, &c, &omega), "but it IS propagation-redundant");
244 let mut fc = f.clone();
246 fc.push(c.clone());
247 assert_eq!(sat_brute(2, &f), sat_brute(2, &fc));
248 }
249
250 #[test]
251 fn pr_accepts_via_a_substitution_witness() {
252 let (a, b) = (0u32, 1u32);
254 let f = vec![vec![p(a), p(b)]];
255 let c = vec![n(a), p(b)];
256 let sigma = Perm::from_images(vec![p(b), p(a)]); assert!(is_pr(2, &f, &c, &Witness::Substitution(sigma)));
258 }
259
260 #[test]
261 fn pr_rejects_a_witness_that_does_not_satisfy_the_clause() {
262 let (a, b) = (0u32, 1u32);
264 let f = vec![vec![p(a), p(b)]];
265 let c = vec![n(a), p(b)];
266 assert!(!is_pr(2, &f, &c, &Witness::Assignment(vec![p(a), n(b)])));
267 }
268
269 #[test]
270 fn pr_rejects_a_bogus_substitution_on_an_asymmetric_formula() {
271 let (a, b) = (0u32, 1u32);
275 let f = vec![vec![p(a), p(b)], vec![n(b)]];
276 let c = vec![n(a), p(b)];
277 let sigma = Perm::from_images(vec![p(b), p(a)]);
278 assert!(!is_pr(2, &f, &c, &Witness::Substitution(sigma)));
279 let mut fc = f.clone();
281 fc.push(c.clone());
282 assert!(sat_brute(2, &f) && !sat_brute(2, &fc), "C would flip SAT→UNSAT");
283 }
284
285 #[test]
286 fn check_pr_refutation_agrees_with_rup_on_pure_rup_proofs() {
287 let (a, b) = (0u32, 1u32);
289 let f = vec![
290 vec![p(a), p(b)],
291 vec![p(a), n(b)],
292 vec![n(a), p(b)],
293 vec![n(a), n(b)],
294 ];
295 let steps = vec![ProofStep::Rup(vec![p(a)]), ProofStep::Rup(vec![n(a)])];
296 assert!(check_pr_refutation(2, &f, &steps));
297 assert!(!check_pr_refutation(2, &f, &[]));
299 }
300
301 #[test]
302 fn fast_checker_agrees_with_the_trusted_checker_on_php() {
303 for n in 3..=6 {
306 let cr = crate::sym_certify::heule_php_refutation(n);
307 let (cnf, _) = crate::families::php(n);
308 assert_eq!(
309 check_pr_refutation_fast(cnf.num_vars, &cnf.clauses, &cr.steps),
310 check_pr_refutation(cnf.num_vars, &cnf.clauses, &cr.steps),
311 "fast vs trusted disagree on PHP({n})"
312 );
313 assert!(check_pr_refutation_fast(cnf.num_vars, &cnf.clauses, &cr.steps), "PHP({n}) refutes");
314
315 let mut bad = cr.steps.clone();
317 if let Some(ProofStep::Pr { witness, .. }) = bad.iter_mut().find(|s| matches!(s, ProofStep::Pr { .. })) {
318 *witness = Witness::Substitution(Perm::identity(cnf.num_vars));
319 }
320 assert_eq!(
321 check_pr_refutation_fast(cnf.num_vars, &cnf.clauses, &bad),
322 check_pr_refutation(cnf.num_vars, &cnf.clauses, &bad),
323 "fast vs trusted disagree on corrupted PHP({n})"
324 );
325 }
326 }
327
328 #[test]
329 fn pr_never_blesses_an_unsound_addition_random() {
330 let mut state = 0x9E3779B97F4A7C15u64;
334 let mut next = || {
335 state = state.wrapping_add(0x9E3779B97F4A7C15);
337 let mut z = state;
338 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
339 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
340 z ^ (z >> 31)
341 };
342 let num_vars = 4usize;
343 let rand_clause = |next: &mut dyn FnMut() -> u64| -> Vec<Lit> {
344 let len = 1 + (next() as usize % 3);
345 let mut c = Vec::new();
346 for _ in 0..len {
347 let v = (next() as u32) % num_vars as u32;
348 let lit = Lit::new(v, next() & 1 == 0);
349 if !c.contains(&lit) && !c.contains(&lit.negated()) {
350 c.push(lit);
351 }
352 }
353 c
354 };
355
356 let mut accepted = 0;
357 for _ in 0..20_000 {
358 let nclauses = next() as usize % 5;
360 let f: Vec<Vec<Lit>> = (0..nclauses).map(|_| rand_clause(&mut next)).filter(|c| !c.is_empty()).collect();
361 let c = rand_clause(&mut next);
362 if c.is_empty() {
363 continue;
364 }
365 let mut omega = Vec::new();
367 for v in 0..num_vars as u32 {
368 if next() & 1 == 0 {
369 omega.push(Lit::new(v, next() & 1 == 0));
370 }
371 }
372 if is_pr(num_vars, &f, &c, &Witness::Assignment(omega)) {
373 accepted += 1;
374 let before = sat_brute(num_vars, &f);
375 let mut fc = f.clone();
376 fc.push(c.clone());
377 let after = sat_brute(num_vars, &fc);
378 assert_eq!(before, after, "PR accepted a clause that changed satisfiability: F={f:?} C={c:?}");
379 }
380 }
381 assert!(accepted > 0, "the test must actually exercise acceptances");
382 }
383
384 #[test]
385 fn sr_never_blesses_an_unsound_addition_random() {
386 let mut state = 0xD1B54A32D192ED03u64;
390 let mut next = || {
391 state = state.wrapping_add(0x9E3779B97F4A7C15);
392 let mut z = state;
393 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
394 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
395 z ^ (z >> 31)
396 };
397 let num_vars = 4usize;
398 let rand_clause = |next: &mut dyn FnMut() -> u64| -> Vec<Lit> {
399 let len = 1 + (next() as usize % 3);
400 let mut c = Vec::new();
401 for _ in 0..len {
402 let v = (next() as u32) % num_vars as u32;
403 let lit = Lit::new(v, next() & 1 == 0);
404 if !c.contains(&lit) && !c.contains(&lit.negated()) {
405 c.push(lit);
406 }
407 }
408 c
409 };
410 let rand_perm = |next: &mut dyn FnMut() -> u64| -> Perm {
413 if next() % 4 == 0 {
414 return Perm::identity(num_vars);
415 }
416 let mut order: Vec<u32> = (0..num_vars as u32).collect();
417 for i in (1..num_vars).rev() {
418 let j = next() as usize % (i + 1);
419 order.swap(i, j);
420 }
421 Perm::from_images((0..num_vars).map(|v| Lit::new(order[v], next() & 1 == 0)).collect())
422 };
423
424 let mut accepted = 0;
425 for _ in 0..20_000 {
426 let nclauses = next() as usize % 5;
427 let f: Vec<Vec<Lit>> =
428 (0..nclauses).map(|_| rand_clause(&mut next)).filter(|c| !c.is_empty()).collect();
429 let c = rand_clause(&mut next);
430 if c.is_empty() {
431 continue;
432 }
433 let sigma = rand_perm(&mut next);
434 if is_pr(num_vars, &f, &c, &Witness::Substitution(sigma)) {
435 accepted += 1;
436 let before = sat_brute(num_vars, &f);
437 let mut fc = f.clone();
438 fc.push(c.clone());
439 let after = sat_brute(num_vars, &fc);
440 assert_eq!(before, after, "SR accepted a clause that changed satisfiability: F={f:?} C={c:?}");
441 }
442 }
443 assert!(accepted > 0, "the substitution path must actually be exercised");
444 }
445}