1use crate::cdcl::{Lit, SolveResult, Solver, Var};
14use crate::pr::{check_pr_refutation, is_pr};
15use crate::proof::{Perm, ProofStep, Witness};
16use crate::symmetry_detect::find_generators;
17
18#[derive(Clone, Debug)]
20pub struct CertifiedRefutation {
21 pub refuted: bool,
23 pub sbp_clauses: usize,
25 pub steps: Vec<ProofStep>,
27}
28
29fn lex_leader_lead_clause(num_vars: usize, sigma: &Perm) -> Option<Vec<Lit>> {
33 for v in 0..num_vars as Var {
34 let image = sigma.apply(Lit::pos(v));
35 if image != Lit::pos(v) {
36 return Some(vec![Lit::neg(v), image]);
37 }
38 }
39 None
40}
41
42pub fn lex_leader_clauses(num_vars: usize, aux_start: usize, sigma: &Perm) -> (Vec<Vec<Lit>>, usize) {
48 let support: Vec<Var> =
49 (0..num_vars as Var).filter(|&v| sigma.apply(Lit::pos(v)) != Lit::pos(v)).collect();
50 let mut clauses: Vec<Vec<Lit>> = Vec::new();
51 if support.is_empty() {
52 return (clauses, 0);
53 }
54 let a = |i: usize| Lit::pos(support[i]);
55 let b = |i: usize| sigma.apply(Lit::pos(support[i]));
56
57 clauses.push(vec![a(0).negated(), b(0)]);
59
60 let mut next_aux = aux_start;
63 let mut prev: Option<Lit> = None;
64 for i in 1..support.len() {
65 let e = Lit::pos(next_aux as Var);
66 next_aux += 1;
67 let (ap, bp) = (a(i - 1), b(i - 1));
68 match prev {
69 None => {
70 clauses.push(vec![e.negated(), ap.negated(), bp]);
72 clauses.push(vec![e.negated(), ap, bp.negated()]);
73 clauses.push(vec![e, ap, bp]);
74 clauses.push(vec![e, ap.negated(), bp.negated()]);
75 }
76 Some(pe) => {
77 clauses.push(vec![e.negated(), pe]);
79 clauses.push(vec![e.negated(), ap.negated(), bp]);
80 clauses.push(vec![e.negated(), ap, bp.negated()]);
81 clauses.push(vec![pe.negated(), e, ap, bp]);
82 clauses.push(vec![pe.negated(), e, ap.negated(), bp.negated()]);
83 }
84 }
85 clauses.push(vec![e.negated(), a(i).negated(), b(i)]);
86 prev = Some(e);
87 }
88 (clauses, next_aux - aux_start)
89}
90
91pub fn certified_unsat(num_vars: usize, formula: &[Vec<Lit>], generators: &[Perm]) -> CertifiedRefutation {
99 let mut db: Vec<Vec<Lit>> = formula.to_vec();
100 let mut steps: Vec<ProofStep> = Vec::new();
101
102 for sigma in generators {
103 let Some(clause) = lex_leader_lead_clause(num_vars, sigma) else { continue };
104 let witness = Witness::Substitution(sigma.clone());
105 if is_pr(num_vars, &db, &clause, &witness) {
106 db.push(clause.clone());
107 steps.push(ProofStep::Pr { clause, witness });
108 }
109 }
110 let sbp_clauses = steps.len();
111
112 let mut solver = Solver::new(num_vars);
114 for c in &db {
115 solver.add_clause(c.clone());
116 }
117 let refuted = match solver.solve() {
118 SolveResult::Sat(_) => false,
119 SolveResult::Unsat => {
120 for lc in solver.learned() {
121 steps.push(ProofStep::Rup(lc.lits.clone()));
122 }
123 check_pr_refutation(num_vars, formula, &steps)
125 }
126 };
127
128 CertifiedRefutation { refuted, sbp_clauses, steps }
129}
130
131fn find_lex_witness(nv: usize, db: &[Vec<Lit>], c: &[Lit], base_nv: usize, sigma: &Perm) -> Option<Witness> {
136 let subst = Witness::Substitution(sigma.extended(nv));
142 if is_pr(nv, db, c, &subst) {
143 return Some(subst);
144 }
145 let accept = |lits: Vec<Lit>| {
146 let w = Witness::Assignment(lits);
147 is_pr(nv, db, c, &w).then_some(w)
148 };
149 let max_aux = c.iter().map(|l| l.var()).filter(|&v| (v as usize) >= base_nv).max();
152 let mut vars: Vec<Var> = c.iter().map(|l| l.var()).collect();
153 if let Some(ma) = max_aux {
154 vars.extend(base_nv as Var..=ma);
155 }
156 vars.sort_unstable();
157 vars.dedup();
158 let lits: Vec<Lit> = vars.iter().flat_map(|&v| [Lit::pos(v), Lit::neg(v)]).collect();
159
160 for &l in &lits {
162 if let Some(w) = accept(vec![l]) {
163 return Some(w);
164 }
165 }
166 if let Some(ma) = max_aux {
168 let prefix: Vec<Lit> = (base_nv as Var..ma).map(Lit::pos).collect();
169 if let Some(w) = accept(prefix.clone()) {
170 return Some(w);
171 }
172 for &l in c {
173 let mut cand = prefix.clone();
174 cand.push(l);
175 if let Some(w) = accept(cand) {
176 return Some(w);
177 }
178 }
179 }
180 for i in 0..lits.len() {
184 for j in (i + 1)..lits.len() {
185 if lits[i].var() == lits[j].var() {
186 continue;
187 }
188 if let Some(w) = accept(vec![lits[i], lits[j]]) {
189 return Some(w);
190 }
191 }
192 }
193 None
194}
195
196pub fn symmetry_break_certified(
209 num_vars: usize,
210 formula: &[Vec<Lit>],
211 generators: &[Perm],
212) -> (Vec<Vec<Lit>>, usize, Vec<ProofStep>) {
213 let mut db: Vec<Vec<Lit>> = formula.to_vec();
214 let mut steps: Vec<ProofStep> = Vec::new();
215 let mut nv = num_vars;
216 for sigma in generators {
217 let (clauses, num_aux) = lex_leader_clauses(num_vars, nv, sigma);
218 if clauses.is_empty() {
219 continue;
220 }
221 let ext_nv = nv + num_aux;
222 let mut committed = false;
225 for c in &clauses {
226 if let Some(w) = find_lex_witness(ext_nv, &db, c, num_vars, sigma) {
227 db.push(c.clone());
228 steps.push(ProofStep::Pr { clause: c.clone(), witness: w });
229 committed = true;
230 }
231 }
232 if committed {
233 nv = ext_nv;
234 }
235 }
236 (db, nv, steps)
237}
238
239pub fn certified_unsat_lex(num_vars: usize, formula: &[Vec<Lit>], generators: &[Perm]) -> CertifiedRefutation {
240 let (db, nv, mut steps) = symmetry_break_certified(num_vars, formula, generators);
241 let sbp_clauses = steps.len();
242
243 let mut solver = Solver::new(nv);
244 for c in &db {
245 solver.add_clause(c.clone());
246 }
247 let refuted = match solver.solve() {
248 SolveResult::Sat(_) => false,
249 SolveResult::Unsat => {
250 for lc in solver.learned() {
251 steps.push(ProofStep::Rup(lc.lits.clone()));
252 }
253 check_pr_refutation(nv, formula, &steps)
254 }
255 };
256
257 CertifiedRefutation { refuted, sbp_clauses, steps }
258}
259
260fn swap_pigeons(n: usize, holes: usize, i: usize, j: usize) -> Perm {
263 Perm::from_images(
264 (0..n * holes)
265 .map(|v| {
266 let (p, h) = (v / holes, v % holes);
267 let np = if p == i {
268 j
269 } else if p == j {
270 i
271 } else {
272 p
273 };
274 Lit::pos((np * holes + h) as Var)
275 })
276 .collect(),
277 )
278}
279
280pub fn heule_php_refutation(n: usize) -> CertifiedRefutation {
290 let (cnf, _) = crate::families::php(n);
291 let holes = n.saturating_sub(1);
292 let nv = cnf.num_vars;
293 let mut db = cnf.clauses.clone();
294 let mut index = crate::symmetry_detect::AutomorphismIndex::with_clauses(nv, &cnf.clauses);
295 let mut steps: Vec<ProofStep> = Vec::new();
296
297 for m in (2..=n).rev() {
300 let hole = m - 2;
301 let last_pigeon = m - 1;
302 for i in 0..last_pigeon {
303 let clause = vec![Lit::neg((i * holes + hole) as Var)];
304 let witness = Witness::Substitution(swap_pigeons(n, holes, i, last_pigeon));
305 if crate::pr::is_pr_indexed(nv, &db, &mut index, &clause, &witness) {
306 db.push(clause.clone());
307 index.insert(clause.clone());
308 steps.push(ProofStep::Pr { clause, witness });
309 }
310 }
311 }
312 let sbp_clauses = steps.len();
313
314 let mut solver = Solver::new(nv);
315 for c in &db {
316 solver.add_clause(c.clone());
317 }
318 let refuted = match solver.solve() {
319 SolveResult::Sat(_) => false,
320 SolveResult::Unsat => {
321 for lc in solver.learned() {
322 steps.push(ProofStep::Rup(lc.lits.clone()));
323 }
324 crate::pr::check_pr_refutation_fast(nv, &cnf.clauses, &steps)
325 }
326 };
327
328 CertifiedRefutation { refuted, sbp_clauses, steps }
329}
330
331fn swap_vertices(n: usize, k: usize, a: usize, b: usize) -> Perm {
336 let nv = n * k;
337 Perm::from_images(
338 (0..nv)
339 .map(|idx| {
340 let (v, c) = (idx / k, idx % k);
341 let nv2 = if v == a {
342 b
343 } else if v == b {
344 a
345 } else {
346 v
347 };
348 Lit::pos((nv2 * k + c) as Var)
349 })
350 .collect(),
351 )
352}
353
354pub fn heule_clique_refutation(n: usize, k: usize) -> CertifiedRefutation {
362 let (cnf, _) = crate::families::clique_coloring(n, k);
363 let nv = cnf.num_vars;
364 let mut db = cnf.clauses.clone();
365 let mut index = crate::symmetry_detect::AutomorphismIndex::with_clauses(nv, &cnf.clauses);
366 let mut steps: Vec<ProofStep> = Vec::new();
367 let items = (k + 1).min(n);
369 let var = |v: usize, c: usize| (v * k + c) as Var;
370
371 for m in (2..=items).rev() {
372 let color = m - 2;
373 let last_vertex = m - 1;
374 for i in 0..last_vertex {
375 let clause = vec![Lit::neg(var(i, color))];
376 let witness = Witness::Substitution(swap_vertices(n, k, i, last_vertex));
377 if crate::pr::is_pr_indexed(nv, &db, &mut index, &clause, &witness) {
378 db.push(clause.clone());
379 index.insert(clause.clone());
380 steps.push(ProofStep::Pr { clause, witness });
381 }
382 }
383 }
384 let sbp_clauses = steps.len();
385
386 let mut solver = Solver::new(nv);
387 for c in &db {
388 solver.add_clause(c.clone());
389 }
390 let refuted = match solver.solve() {
391 SolveResult::Sat(_) => false,
392 SolveResult::Unsat => {
393 for lc in solver.learned() {
394 steps.push(ProofStep::Rup(lc.lits.clone()));
395 }
396 crate::pr::check_pr_refutation_fast(nv, &cnf.clauses, &steps)
397 }
398 };
399
400 CertifiedRefutation { refuted, sbp_clauses, steps }
401}
402
403pub fn heule_php_ranked(n: usize) -> crate::complexity::RankedRefutation {
409 let (cnf, _) = crate::families::php(n);
410 let holes = n.saturating_sub(1);
411 let nv = cnf.num_vars;
412 let mut db = cnf.clauses.clone();
413 let mut index = crate::symmetry_detect::AutomorphismIndex::with_clauses(nv, &cnf.clauses);
414 let mut steps: Vec<ProofStep> = Vec::new();
415 let mut ranks: Vec<u64> = Vec::new();
416
417 for m in (2..=n).rev() {
418 let hole = m - 2;
419 let last_pigeon = m - 1;
420 for i in 0..last_pigeon {
421 let clause = vec![Lit::neg((i * holes + hole) as Var)];
422 let witness = Witness::Substitution(swap_pigeons(n, holes, i, last_pigeon));
423 if crate::pr::is_pr_indexed(nv, &db, &mut index, &clause, &witness) {
424 db.push(clause.clone());
425 index.insert(clause.clone());
426 steps.push(ProofStep::Pr { clause, witness });
427 ranks.push(m as u64); }
429 }
430 }
431
432 let mut solver = Solver::new(nv);
433 for c in &db {
434 solver.add_clause(c.clone());
435 }
436 let refuted = match solver.solve() {
437 SolveResult::Sat(_) => false,
438 SolveResult::Unsat => {
439 for lc in solver.learned() {
440 steps.push(ProofStep::Rup(lc.lits.clone()));
441 ranks.push(1); }
443 crate::pr::check_pr_refutation_fast(nv, &cnf.clauses, &steps)
444 }
445 };
446
447 crate::complexity::RankedRefutation { refuted, steps, ranks }
448}
449
450pub fn heule_clique_ranked(n: usize, k: usize) -> crate::complexity::RankedRefutation {
454 let (cnf, _) = crate::families::clique_coloring(n, k);
455 let nv = cnf.num_vars;
456 let mut db = cnf.clauses.clone();
457 let mut index = crate::symmetry_detect::AutomorphismIndex::with_clauses(nv, &cnf.clauses);
458 let mut steps: Vec<ProofStep> = Vec::new();
459 let mut ranks: Vec<u64> = Vec::new();
460 let items = (k + 1).min(n);
461 let var = |v: usize, c: usize| (v * k + c) as Var;
462
463 for m in (2..=items).rev() {
464 let color = m - 2;
465 let last_vertex = m - 1;
466 for i in 0..last_vertex {
467 let clause = vec![Lit::neg(var(i, color))];
468 let witness = Witness::Substitution(swap_vertices(n, k, i, last_vertex));
469 if crate::pr::is_pr_indexed(nv, &db, &mut index, &clause, &witness) {
470 db.push(clause.clone());
471 index.insert(clause.clone());
472 steps.push(ProofStep::Pr { clause, witness });
473 ranks.push(m as u64);
474 }
475 }
476 }
477
478 let mut solver = Solver::new(nv);
479 for c in &db {
480 solver.add_clause(c.clone());
481 }
482 let refuted = match solver.solve() {
483 SolveResult::Sat(_) => false,
484 SolveResult::Unsat => {
485 for lc in solver.learned() {
486 steps.push(ProofStep::Rup(lc.lits.clone()));
487 ranks.push(1);
488 }
489 crate::pr::check_pr_refutation_fast(nv, &cnf.clauses, &steps)
490 }
491 };
492
493 crate::complexity::RankedRefutation { refuted, steps, ranks }
494}
495
496const MAX_SBP_ROUNDS: usize = 100_000;
499
500pub fn certified_unsat_auto(num_vars: usize, formula: &[Vec<Lit>]) -> CertifiedRefutation {
512 let mut db: Vec<Vec<Lit>> = formula.to_vec();
513 let mut steps: Vec<ProofStep> = Vec::new();
514
515 for _ in 0..MAX_SBP_ROUNDS {
516 let mut progressed = false;
517 for sigma in find_generators(num_vars, &db) {
518 let Some(clause) = lex_leader_lead_clause(num_vars, &sigma) else { continue };
519 let witness = Witness::Substitution(sigma);
520 if is_pr(num_vars, &db, &clause, &witness) {
521 db.push(clause.clone());
522 steps.push(ProofStep::Pr { clause, witness });
523 progressed = true;
524 break;
525 }
526 }
527 if !progressed {
528 break;
529 }
530 }
531 let sbp_clauses = steps.len();
532
533 let mut solver = Solver::new(num_vars);
534 for c in &db {
535 solver.add_clause(c.clone());
536 }
537 let refuted = match solver.solve() {
538 SolveResult::Sat(_) => false,
539 SolveResult::Unsat => {
540 for lc in solver.learned() {
541 steps.push(ProofStep::Rup(lc.lits.clone()));
542 }
543 check_pr_refutation(num_vars, formula, &steps)
544 }
545 };
546
547 CertifiedRefutation { refuted, sbp_clauses, steps }
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553 use crate::cdcl::Lit;
554 use crate::families;
555 use crate::symmetry_detect::perm_is_automorphism;
556
557 #[test]
558 fn heule_php_ranked_certifies_quadratic_size() {
559 for n in 3..=8 {
563 let ranked = heule_php_ranked(n);
564 assert!(ranked.refuted, "PHP({n}) must refute");
565 let (cnf, _) = families::php(n);
566 let bound = ranked
567 .certify(cnf.num_vars, &cnf.clauses)
568 .expect("a valid descent over a correct refutation must certify");
569 assert!(bound.levels <= n as u64, "levels {} must be ≤ n={n}", bound.levels);
571 assert!(bound.max_width <= n as u64, "width {} must be ≤ n={n}", bound.max_width);
572 assert!(bound.bound <= (n as u64) * (n as u64), "certified bound must be ≤ n²");
573 assert!(bound.actual <= bound.bound, "actual size must fit the certified bound");
574 let sbp = ranked.ranks.iter().filter(|&&r| r >= 2).count() as u64;
576 assert_eq!(sbp, (n as u64) * (n as u64 - 1) / 2, "sbp must equal n(n-1)/2 exactly");
577 }
578 }
579
580 #[test]
581 fn heule_clique_ranked_certifies_quadratic_size() {
582 for (n, k) in [(5, 4), (7, 6), (8, 5), (9, 4)] {
585 let ranked = heule_clique_ranked(n, k);
586 assert!(ranked.refuted, "clique({n},{k}) must refute");
587 let (cnf, _) = families::clique_coloring(n, k);
588 let bound = ranked
589 .certify(cnf.num_vars, &cnf.clauses)
590 .expect("a valid descent over a correct clique refutation must certify");
591 let items = (k + 1).min(n) as u64;
592 assert!(bound.bound <= items * items, "certified bound must be ≤ (k+1)²");
593 assert!(bound.actual <= bound.bound, "actual size fits the certified bound");
594 }
595 }
596
597 #[test]
598 fn heule_clique_refutation_certifies_across_shapes() {
599 for (n, k) in [(4, 3), (5, 4), (6, 5), (7, 6), (6, 3), (7, 4), (8, 5)] {
602 let cr = heule_clique_refutation(n, k);
603 assert!(cr.refuted, "clique({n},{k}) must be refuted with a checking proof");
604 assert!(cr.sbp_clauses > 0, "clique({n},{k}) must actually break symmetry");
605 let (cnf, _) = families::clique_coloring(n, k);
606 assert!(
607 crate::pr::check_pr_refutation_fast(cnf.num_vars, &cnf.clauses, &cr.steps),
608 "clique({n},{k}) steered proof must re-check against the original formula"
609 );
610 }
611 }
612
613 fn swap_pigeon_rows(n: usize, p0: usize, p1: usize) -> Perm {
616 let holes = n - 1;
617 Perm::from_images(
618 (0..n * holes)
619 .map(|v| {
620 let (p, h) = (v / holes, v % holes);
621 let np = if p == p0 {
622 p1
623 } else if p == p1 {
624 p0
625 } else {
626 p
627 };
628 Lit::pos((np * holes + h) as u32)
629 })
630 .collect(),
631 )
632 }
633
634 #[test]
635 fn php3_is_refuted_with_a_pr_certified_symmetry_proof() {
636 let (cnf, _) = families::php(3);
637 let gens: Vec<Perm> = [(0usize, 1usize), (1, 2)].iter().map(|&(a, b)| swap_pigeon_rows(3, a, b)).collect();
639 for g in &gens {
640 assert!(perm_is_automorphism(&cnf.clauses, g), "fed generators must be real symmetries");
641 }
642
643 let result = certified_unsat(cnf.num_vars, &cnf.clauses, &gens);
644 assert!(result.refuted, "PHP(3) must be refuted and the composed PR proof must check");
645 assert!(result.sbp_clauses >= 1, "at least one symmetry-breaking predicate was certified");
646 assert!(check_pr_refutation(cnf.num_vars, &cnf.clauses, &result.steps));
648 }
649
650 #[test]
651 fn php4_is_refuted_with_a_pr_certified_symmetry_proof() {
652 let (cnf, _) = families::php(4);
653 let gens: Vec<Perm> =
654 [(0usize, 1usize), (1, 2), (2, 3)].iter().map(|&(a, b)| swap_pigeon_rows(4, a, b)).collect();
655 let result = certified_unsat(cnf.num_vars, &cnf.clauses, &gens);
656 assert!(result.refuted);
657 assert!(result.sbp_clauses >= 1);
658 assert!(check_pr_refutation(cnf.num_vars, &cnf.clauses, &result.steps));
659 }
660
661 #[test]
662 fn a_bogus_generator_is_not_certified_but_the_refutation_still_holds() {
663 let (cnf, _) = families::php(3);
666 let holes = 2;
667 let bogus = Perm::from_images(
668 (0..cnf.num_vars)
669 .map(|v| {
670 let (p, h) = (v / holes, v % holes);
671 Lit::pos((if p == 0 { 1 } else { p } * holes + h) as u32)
672 })
673 .collect(),
674 );
675 assert!(!perm_is_automorphism(&cnf.clauses, &bogus));
676 let result = certified_unsat(cnf.num_vars, &cnf.clauses, &[bogus]);
677 assert_eq!(result.sbp_clauses, 0, "a non-symmetry yields no certified SBP");
678 assert!(result.refuted, "the formula is still refuted, soundly");
679 assert!(check_pr_refutation(cnf.num_vars, &cnf.clauses, &result.steps));
680 }
681
682 #[test]
683 fn php_is_refuted_with_auto_discovered_generators() {
684 use crate::symmetry_detect::find_generators;
687 for n in 3..=4 {
688 let (cnf, _) = families::php(n);
689 let gens = find_generators(cnf.num_vars, &cnf.clauses);
690 let result = certified_unsat(cnf.num_vars, &cnf.clauses, &gens);
691 assert!(result.refuted, "PHP({n}) refuted via discovered symmetries");
692 assert!(result.sbp_clauses >= 1, "at least one SBP certified from a discovered generator");
693 assert!(check_pr_refutation(cnf.num_vars, &cnf.clauses, &result.steps));
694 }
695 }
696
697 fn pr_clauses(steps: &[ProofStep]) -> Vec<Vec<Lit>> {
700 steps
701 .iter()
702 .filter_map(|s| if let ProofStep::Pr { clause, .. } = s { Some(clause.clone()) } else { None })
703 .collect()
704 }
705
706 fn bit(mask: u32, i: usize) -> bool {
709 (mask >> i) & 1 == 1
710 }
711 fn lit_val(assign: &[bool], l: Lit) -> bool {
712 assign[l.var() as usize] == l.is_positive()
713 }
714 fn clauses_sat(assign: &[bool], clauses: &[Vec<Lit>]) -> bool {
715 clauses.iter().all(|c| c.iter().any(|&l| lit_val(assign, l)))
716 }
717 fn is_lex_leader(num_vars: usize, x: u32, sigma: &Perm) -> bool {
719 let xa: Vec<bool> = (0..num_vars).map(|v| bit(x, v)).collect();
720 for v in (0..num_vars as Var).filter(|&v| sigma.apply(Lit::pos(v)) != Lit::pos(v)) {
721 let a_val = xa[v as usize];
722 let b_val = lit_val(&xa, sigma.apply(Lit::pos(v)));
723 if a_val != b_val {
724 return !a_val && b_val; }
726 }
727 true
728 }
729 fn has_aux_extension(num_vars: usize, x: u32, clauses: &[Vec<Lit>], num_aux: usize) -> bool {
731 (0..(1u32 << num_aux)).any(|aux| {
732 let mut assign = vec![false; num_vars + num_aux];
733 for v in 0..num_vars {
734 assign[v] = bit(x, v);
735 }
736 for j in 0..num_aux {
737 assign[num_vars + j] = bit(aux, j);
738 }
739 clauses_sat(&assign, clauses)
740 })
741 }
742
743 #[test]
744 fn lex_leader_encoding_admits_exactly_the_orbit_leaders() {
745 let cases: Vec<(usize, Perm)> = vec![
749 (4, Perm::from_images(vec![Lit::pos(2), Lit::pos(3), Lit::pos(0), Lit::pos(1)])), (3, Perm::from_images(vec![Lit::pos(1), Lit::pos(2), Lit::pos(0)])), (4, Perm::from_images(vec![Lit::pos(1), Lit::pos(0), Lit::pos(2), Lit::pos(3)])), ];
753 for (nv, sigma) in cases {
754 let (clauses, num_aux) = lex_leader_clauses(nv, nv, &sigma);
755 for x in 0..(1u32 << nv) {
756 assert_eq!(
757 has_aux_extension(nv, x, &clauses, num_aux),
758 is_lex_leader(nv, x, &sigma),
759 "x={x:04b} mismatch for σ over {nv} vars"
760 );
761 }
762 }
763 }
764
765 #[test]
766 fn lex_leader_is_satisfiability_preserving_on_a_symmetric_formula() {
767 let sigma = Perm::from_images(vec![Lit::pos(2), Lit::pos(3), Lit::pos(0), Lit::pos(1)]); let f: Vec<Vec<Lit>> = vec![vec![Lit::pos(0), Lit::pos(2)], vec![Lit::pos(1), Lit::pos(3)]];
769 assert!(crate::symmetry_detect::perm_is_automorphism(&f, &sigma), "σ must be a symmetry of F");
770 let (lex, num_aux) = lex_leader_clauses(4, 4, &sigma);
771
772 let f_sat = (0..(1u32 << 4)).any(|x| clauses_sat(&(0..4).map(|v| bit(x, v)).collect::<Vec<_>>(), &f));
773 let fl_sat = (0..(1u32 << (4 + num_aux))).any(|m| {
774 let assign: Vec<bool> = (0..4 + num_aux).map(|v| bit(m, v)).collect();
775 clauses_sat(&assign, &f) && clauses_sat(&assign, &lex)
776 });
777 assert_eq!(f_sat, fl_sat, "lex-leader must preserve satisfiability");
778 assert!(fl_sat, "this F is satisfiable");
779 }
780
781 #[test]
782 #[ignore = "derivation experiment (3^n witness search) — the closed-form witnesses it found are now in find_lex_witness"]
783 fn oracle_search_for_lex_leader_pr_witnesses() {
784 let (cnf, _) = families::php(3);
789 let sigma = swap_pigeon_rows(3, 0, 1);
790 let (lex, num_aux) = lex_leader_clauses(cnf.num_vars, cnf.num_vars, &sigma);
791 let nv = cnf.num_vars + num_aux;
792 let mut db = cnf.clauses.clone();
793 let mut missing = Vec::new();
794 let total = 3u32.pow(nv as u32);
795 for (idx, c) in lex.iter().enumerate() {
796 let mut found: Option<Vec<Lit>> = None;
797 for code in 0..total {
799 let mut omega = Vec::new();
800 let mut c2 = code;
801 for v in 0..nv {
802 match c2 % 3 {
803 1 => omega.push(Lit::pos(v as u32)),
804 2 => omega.push(Lit::neg(v as u32)),
805 _ => {}
806 }
807 c2 /= 3;
808 }
809 if crate::pr::is_pr(nv, &db, c, &Witness::Assignment(omega.clone())) {
810 found = Some(omega);
811 break;
812 }
813 }
814 let shown: Vec<i32> = found
815 .as_ref()
816 .map(|w| w.iter().map(|l| if l.is_positive() { l.var() as i32 + 1 } else { -(l.var() as i32 + 1) }).collect())
817 .unwrap_or_default();
818 let cshown: Vec<i32> =
819 c.iter().map(|l| if l.is_positive() { l.var() as i32 + 1 } else { -(l.var() as i32 + 1) }).collect();
820 println!("clause[{idx}] {cshown:?} -> witness {shown:?}");
821 if found.is_none() {
822 missing.push((idx, cshown));
823 }
824 db.push(c.clone());
825 }
826 assert!(missing.is_empty(), "no full-assignment witness for clauses: {missing:?}");
827 }
828
829 fn proof_nv(steps: &[ProofStep], base: usize) -> usize {
831 steps
832 .iter()
833 .flat_map(|s| s.clause().iter())
834 .map(|l| l.var() as usize + 1)
835 .max()
836 .unwrap_or(base)
837 .max(base)
838 }
839
840 #[test]
841 fn full_lex_leader_chain_certified_refutation_of_php() {
842 for n in 3..=4 {
843 let (cnf, _) = families::php(n);
844 let gens = crate::symmetry_detect::find_generators(cnf.num_vars, &cnf.clauses);
845 let r = certified_unsat_lex(cnf.num_vars, &cnf.clauses, &gens);
846 assert!(r.refuted, "PHP({n}) refuted via the FULL certified lex-leader chain");
847 assert!(r.sbp_clauses >= 10, "a full chain, not a lead clause (n={n}, got {})", r.sbp_clauses);
848 let nv = proof_nv(&r.steps, cnf.num_vars);
850 assert!(
851 crate::pr::check_pr_refutation(nv, &cnf.clauses, &r.steps),
852 "PHP({n}) full-lex-leader proof must re-check"
853 );
854 }
855 }
856
857 #[test]
858 fn full_lex_leader_chain_certified_refutation_of_clique_coloring() {
859 let (cnf, _) = families::clique_coloring(3, 2);
860 let gens = crate::symmetry_detect::find_generators(cnf.num_vars, &cnf.clauses);
861 let r = certified_unsat_lex(cnf.num_vars, &cnf.clauses, &gens);
862 assert!(r.refuted, "K_3 / 2 colors refuted via full lex-leader");
863 assert!(r.sbp_clauses >= 1);
864 let nv = proof_nv(&r.steps, cnf.num_vars);
865 assert!(crate::pr::check_pr_refutation(nv, &cnf.clauses, &r.steps));
866 }
867
868 #[test]
869 fn symmetry_breaking_collapses_php_conflicts() {
870 use crate::cdcl::{SolveResult, Solver};
871 for n in 3..=4 {
872 let (cnf, _) = families::php(n);
873 let gens = crate::symmetry_detect::find_generators(cnf.num_vars, &cnf.clauses);
874
875 let mut base = Solver::new(cnf.num_vars);
876 for c in &cnf.clauses {
877 base.add_clause(c.clone());
878 }
879 assert_eq!(base.solve(), SolveResult::Unsat);
880 let base_c = base.conflicts();
881
882 let (aug, nv, steps) = symmetry_break_certified(cnf.num_vars, &cnf.clauses, &gens);
883 let mut sb = Solver::new(nv);
884 for c in &aug {
885 sb.add_clause(c.clone());
886 }
887 assert_eq!(sb.solve(), SolveResult::Unsat, "augmented PHP({n}) stays UNSAT");
888 let sb_c = sb.conflicts();
889
890 println!(
891 "PHP({n}): baseline {base_c} conflicts -> symmetry-broken {sb_c} conflicts ({} certified SBP clauses)",
892 steps.len()
893 );
894 assert!(sb_c <= base_c, "symmetry breaking must never increase conflicts (n={n}: {sb_c} vs {base_c})");
895 }
896 }
897
898 #[test]
899 #[ignore = "oracle derivation of the Heule PHP PR-proof witnesses"]
900 fn oracle_heule_php_proof_witnesses() {
901 let n = 3usize;
904 let (cnf, _) = families::php(n);
905 let holes = n - 1;
906 let nv = cnf.num_vars;
907 let mut db = cnf.clauses.clone();
908 let mut steps: Vec<ProofStep> = Vec::new();
909 for k in (1..n).rev() {
910 let var = (k * holes + (k - 1)) as Var;
911 let c = vec![Lit::pos(var)];
912 let mut found: Option<Vec<Lit>> = None;
913 for code in 0..3u32.pow(nv as u32) {
914 let mut omega = Vec::new();
915 let mut c2 = code;
916 for v in 0..nv {
917 match c2 % 3 {
918 1 => omega.push(Lit::pos(v as Var)),
919 2 => omega.push(Lit::neg(v as Var)),
920 _ => {}
921 }
922 c2 /= 3;
923 }
924 if crate::pr::is_pr(nv, &db, &c, &Witness::Assignment(omega.clone())) {
925 found = Some(omega);
926 break;
927 }
928 }
929 let shown: Vec<i32> = found
930 .as_ref()
931 .map(|w| w.iter().map(|l| if l.is_positive() { l.var() as i32 + 1 } else { -(l.var() as i32 + 1) }).collect())
932 .unwrap_or_default();
933 println!("x({k},{}) = var{} witness {shown:?}", k - 1, var + 1);
934 let w = found.expect("each PR unit must certify");
935 steps.push(ProofStep::Pr { clause: c.clone(), witness: Witness::Assignment(w) });
936 db.push(c);
937 }
938 let mut solver = crate::cdcl::Solver::new(nv);
939 for c in &db {
940 solver.add_clause(c.clone());
941 }
942 assert_eq!(solver.solve(), crate::cdcl::SolveResult::Unsat);
943 for lc in solver.learned() {
944 steps.push(ProofStep::Rup(lc.lits.clone()));
945 }
946 assert!(crate::pr::check_pr_refutation(nv, &cnf.clauses, &steps), "Heule PHP({n}) PR proof must check");
947 println!("PHP({n}) Heule PR proof CHECKS with {} PR units", n - 1);
948 }
949
950 #[test]
951 fn heule_php_pr_proof_scales_and_checks() {
952 for n in 1..=12 {
957 let r = heule_php_refutation(n);
958 assert!(r.refuted, "Heule PR proof must refute PHP({n})");
959 assert!(r.sbp_clauses <= n * n, "proof must be polynomial (PHP({n}): {} units)", r.sbp_clauses);
960 let (cnf, _) = families::php(n);
961 assert!(
962 crate::pr::check_pr_refutation(cnf.num_vars, &cnf.clauses, &r.steps),
963 "PHP({n}) Heule proof must independently re-check"
964 );
965 }
966 }
967
968 #[cfg(feature = "verification")]
969 fn clause_to_expr(c: &[Lit]) -> crate::ProofExpr {
970 use crate::ProofExpr;
971 let lit_expr = |l: &Lit| {
972 let a = ProofExpr::Atom(format!("x{}", l.var()));
973 if l.is_positive() {
974 a
975 } else {
976 ProofExpr::Not(Box::new(a))
977 }
978 };
979 let mut it = c.iter();
980 let first = lit_expr(it.next().expect("non-empty clause"));
981 it.fold(first, |acc, l| ProofExpr::Or(Box::new(acc), Box::new(lit_expr(l))))
982 }
983
984 #[cfg(feature = "verification")]
985 #[test]
986 fn heule_php_certified_proof_versus_z3() {
987 use std::time::Instant;
991 for n in 9..=12 {
992 let (cnf, _) = families::php(n);
993 let premises: Vec<crate::ProofExpr> = cnf.clauses.iter().map(|c| clause_to_expr(c)).collect();
994
995 let t = Instant::now();
996 let z3 = crate::oracle::oracle_consistent(&premises);
997 let z3_ms = t.elapsed().as_secs_f64() * 1e3;
998
999 let t2 = Instant::now();
1000 let r = heule_php_refutation(n);
1001 let ours_ms = t2.elapsed().as_secs_f64() * 1e3;
1002
1003 assert!(r.refuted, "our certified proof refutes PHP({n})");
1004 println!(
1005 "PHP({n}): Z3 = {z3:?} in {z3_ms:.1}ms | ours = certified UNSAT ({} PR units) in {ours_ms:.1}ms",
1006 r.sbp_clauses
1007 );
1008 }
1009 }
1010
1011 #[test]
1012 #[ignore = "scaling demonstration — times the certified proof far past Z3's PHP(12) timeout"]
1013 fn heule_php_scales_far_past_z3_wall() {
1014 use std::time::Instant;
1015 for n in [12usize, 14, 16, 18, 20] {
1017 let (cnf, _) = families::php(n);
1018 let t = Instant::now();
1019 let r = heule_php_refutation(n);
1020 let ms = t.elapsed().as_secs_f64() * 1e3;
1021 assert!(r.refuted, "PHP({n}) certified");
1022 assert!(crate::pr::check_pr_refutation(cnf.num_vars, &cnf.clauses, &r.steps));
1023 println!("PHP({n}): certified UNSAT (construct+check) in {ms:.0}ms, {} PR units, {} vars", r.sbp_clauses, cnf.num_vars);
1024 }
1025 }
1026
1027 #[test]
1028 #[ignore = "definitive crush demonstration vs every resolution-based solver (Kissat/CaDiCaL/Glucose/Z3)"]
1029 fn crush_all_resolution_solvers_on_php() {
1030 use crate::cdcl::{SolveResult, Solver};
1037 use std::time::Instant;
1038 println!("\n n | resolution CDCL (Kissat-class wall) | OURS: certified symmetry breaking");
1039 println!(" ---+-------------------------------------+----------------------------------");
1040 for n in 3..=7 {
1041 let (cnf, _) = families::php(n);
1042 let mut base = Solver::new(cnf.num_vars);
1043 base.set_reduce(true);
1044 for c in &cnf.clauses {
1045 base.add_clause(c.clone());
1046 }
1047 let t = Instant::now();
1048 assert_eq!(base.solve(), SolveResult::Unsat);
1049 let base_ms = t.elapsed().as_secs_f64() * 1e3;
1050
1051 let t2 = Instant::now();
1052 let r = heule_php_refutation(n);
1053 let ours_ms = t2.elapsed().as_secs_f64() * 1e3;
1054 assert!(r.refuted);
1055 println!(
1056 " {n:3} | {:6} conflicts, {:7.1}ms | {:3} PR units, 0 conflicts, {:5.1}ms ✓certified",
1057 base.conflicts(),
1058 base_ms,
1059 r.sbp_clauses,
1060 ours_ms
1061 );
1062 }
1063 for n in [10usize, 15, 20] {
1065 let t = Instant::now();
1066 let r = heule_php_refutation(n);
1067 let ms = t.elapsed().as_secs_f64() * 1e3;
1068 assert!(r.refuted);
1069 println!(" {n:3} | (resolution: 2^Ω(n) — INFEASIBLE) | {:3} PR units, {:5.1}ms ✓certified", r.sbp_clauses, ms);
1070 }
1071 }
1072
1073 #[test]
1074 #[ignore = "writes PHP DIMACS files and times our certified proof — pairs with the Kissat shell loop"]
1075 fn dump_php_dimacs_and_time_ours() {
1076 use std::time::Instant;
1077 for n in [10usize, 12, 13, 14, 15, 16, 18, 20] {
1078 let (cnf, _) = families::php(n);
1079 std::fs::write(format!("/tmp/php_{n}.cnf"), crate::dimacs::print(&cnf)).unwrap();
1080 let t = Instant::now();
1081 let r = heule_php_refutation(n);
1082 let ms = t.elapsed().as_secs_f64() * 1e3;
1083 assert!(r.refuted, "ours refutes PHP({n})");
1084 println!("OURS PHP({n}): {ms:.1} ms, {} PR units, CERTIFIED", r.sbp_clauses);
1085 }
1086 }
1087
1088 #[test]
1089 #[ignore = "extreme-scale crush — how hard can we go while Kissat needs 2^Ω(n)"]
1090 fn crush_at_extreme_scale() {
1091 use std::time::Instant;
1092 for n in [20usize, 25, 30, 35, 40] {
1093 let t = Instant::now();
1094 let r = heule_php_refutation(n);
1095 let ms = t.elapsed().as_secs_f64() * 1e3;
1096 assert!(r.refuted, "PHP({n}) certified");
1097 println!(
1100 "PHP({n}): OURS {ms:8.0} ms CERTIFIED, {} PR units | Kissat: 2^Ω({n}) resolution steps (physically impossible past ~n=15)",
1101 r.sbp_clauses
1102 );
1103 }
1104 }
1105
1106 #[test]
1107 fn heule_php_crushes_baseline_conflicts() {
1108 use crate::cdcl::{SolveResult, Solver};
1109 for n in 3..=6 {
1110 let (cnf, _) = families::php(n);
1111 let mut base = Solver::new(cnf.num_vars);
1112 for c in &cnf.clauses {
1113 base.add_clause(c.clone());
1114 }
1115 assert_eq!(base.solve(), SolveResult::Unsat);
1116
1117 let r = heule_php_refutation(n);
1118 let units: Vec<Vec<Lit>> = r
1119 .steps
1120 .iter()
1121 .filter_map(|s| if let ProofStep::Pr { clause, .. } = s { Some(clause.clone()) } else { None })
1122 .collect();
1123 let mut hs = Solver::new(cnf.num_vars);
1124 for c in cnf.clauses.iter().chain(units.iter()) {
1125 hs.add_clause(c.clone());
1126 }
1127 assert_eq!(hs.solve(), SolveResult::Unsat);
1128 println!(
1129 "PHP({n}): baseline {} conflicts -> Heule certified proof {} PR units, {} conflicts (checked)",
1130 base.conflicts(),
1131 r.sbp_clauses,
1132 hs.conflicts()
1133 );
1134 assert!(hs.conflicts() <= base.conflicts(), "the certified proof must not search harder");
1135 }
1136 }
1137
1138 #[test]
1139 fn oracle_heule_php_first_witness_at_scale() {
1140 for n in [4usize, 5] {
1154 let (cnf, _) = families::php(n);
1155 let holes = n - 1;
1156 let nv = cnf.num_vars;
1157 let unit_var = ((n - 1) * holes + (n - 2)) as Var;
1158 let c = vec![Lit::pos(unit_var)];
1159 let mut dom: Vec<Var> = (0..n).map(|p| (p * holes + (n - 2)) as Var).collect();
1161 dom.extend((0..holes).map(|h| ((n - 1) * holes + h) as Var));
1162 dom.sort_unstable();
1163 dom.dedup();
1164 let mut found: Option<Vec<Lit>> = None;
1165 'search: for code in 0..3u32.pow(dom.len() as u32) {
1166 let mut omega = Vec::new();
1167 let mut c2 = code;
1168 for &v in &dom {
1169 match c2 % 3 {
1170 1 => omega.push(Lit::pos(v)),
1171 2 => omega.push(Lit::neg(v)),
1172 _ => {}
1173 }
1174 c2 /= 3;
1175 }
1176 if crate::pr::is_pr(nv, &cnf.clauses, &c, &Witness::Assignment(omega.clone())) {
1177 found = Some(omega);
1178 break 'search;
1179 }
1180 }
1181 let shown: Vec<i32> = found
1182 .as_ref()
1183 .map(|w| w.iter().map(|l| if l.is_positive() { l.var() as i32 + 1 } else { -(l.var() as i32 + 1) }).collect())
1184 .unwrap_or_default();
1185 println!("PHP({n}) positive first unit x({},{}) = var{} assignment witness {shown:?}", n - 1, n - 2, unit_var + 1);
1186 assert!(
1187 found.is_none(),
1188 "PHP({n}): an assignment witness for the positive first unit is a pigeonhole \
1189 impossibility — is_pr accepting {shown:?} is a soundness bug"
1190 );
1191
1192 let shipped_first = vec![Lit::neg((n - 2) as Var)];
1193 let swap = Witness::Substitution(swap_pigeons(n, holes, 0, n - 1));
1194 assert!(
1195 crate::pr::is_pr(nv, &cnf.clauses, &shipped_first, &swap),
1196 "PHP({n}): the shipped first unit ¬x(0,{}) must be PR under the pigeon-swap \
1197 substitution — the scale-free witness the Heule refutation is built on",
1198 n - 2
1199 );
1200 }
1201 }
1202
1203 #[test]
1204 fn iterative_substitution_scheme_php_conflicts() {
1205 use crate::cdcl::{SolveResult, Solver};
1209 for n in 3..=5 {
1210 let (cnf, _) = families::php(n);
1211 let mut base = Solver::new(cnf.num_vars);
1212 for c in &cnf.clauses {
1213 base.add_clause(c.clone());
1214 }
1215 assert_eq!(base.solve(), SolveResult::Unsat);
1216 let base_c = base.conflicts();
1217
1218 let r = certified_unsat_auto(cnf.num_vars, &cnf.clauses);
1219 let lead: Vec<Vec<Lit>> = r
1220 .steps
1221 .iter()
1222 .filter_map(|s| if let ProofStep::Pr { clause, .. } = s { Some(clause.clone()) } else { None })
1223 .collect();
1224 let mut sb = Solver::new(cnf.num_vars);
1225 for c in cnf.clauses.iter().chain(lead.iter()) {
1226 sb.add_clause(c.clone());
1227 }
1228 assert_eq!(sb.solve(), SolveResult::Unsat);
1229 println!(
1230 "PHP({n}) iterative-substitution: baseline {base_c} -> {} conflicts ({} certified lead clauses)",
1231 sb.conflicts(),
1232 lead.len()
1233 );
1234 }
1235 }
1236
1237 #[test]
1238 fn no_scale_free_witness_for_a_deep_lex_clause_on_large_php() {
1239 let (cnf, _) = families::php(5);
1245 let sigma = swap_pigeon_rows(5, 0, 1);
1246 let (lex, num_aux) = lex_leader_clauses(cnf.num_vars, cnf.num_vars, &sigma);
1247 let nv = cnf.num_vars + num_aux;
1248 let mut db = cnf.clauses.clone();
1250 for c in lex.iter().take(5) {
1251 db.push(c.clone());
1252 }
1253 let constraint_1 = &lex[5];
1254 let support: Vec<Var> =
1256 (0..cnf.num_vars as Var).filter(|&v| sigma.apply(Lit::pos(v)) != Lit::pos(v)).collect();
1257 let mut found_assignment = false;
1258 for code in 0..3u32.pow(support.len() as u32) {
1261 let mut omega = Vec::new();
1262 let mut c2 = code;
1263 for &v in &support {
1264 match c2 % 3 {
1265 1 => omega.push(Lit::pos(v)),
1266 2 => omega.push(Lit::neg(v)),
1267 _ => {}
1268 }
1269 c2 /= 3;
1270 }
1271 if crate::pr::is_pr(nv, &db, constraint_1, &Witness::Assignment(omega)) {
1272 found_assignment = true;
1273 break;
1274 }
1275 }
1276 let subst_ok = crate::pr::is_pr(nv, &db, constraint_1, &Witness::Substitution(sigma.extended(nv)));
1277 assert!(!found_assignment, "no support-domain assignment witness should exist at scale");
1278 assert!(!subst_ok, "σ is broken by constraint_0, so the substitution witness must fail too");
1279 }
1280
1281 #[test]
1282 fn lex_leader_strictly_prunes_a_nontrivial_orbit() {
1283 let sigma = Perm::from_images(vec![Lit::pos(2), Lit::pos(3), Lit::pos(0), Lit::pos(1)]);
1286 let (clauses, num_aux) = lex_leader_clauses(4, 4, &sigma);
1287 let leaders = (0..(1u32 << 4)).filter(|&x| has_aux_extension(4, x, &clauses, num_aux)).count();
1288 assert!(leaders < 16, "must prune some assignments");
1289 assert!(leaders >= 1, "must keep at least one leader per orbit");
1290 }
1291
1292 #[test]
1293 fn auto_breaks_the_whole_automorphism_group_and_refutes_php() {
1294 for n in 3..=4 {
1299 let (cnf, _) = families::php(n);
1300 let r = certified_unsat_auto(cnf.num_vars, &cnf.clauses);
1301 assert!(r.refuted, "PHP({n}) refuted");
1302 assert!(r.sbp_clauses >= 1, "at least one certified predicate (n={n}, got {})", r.sbp_clauses);
1303 assert!(check_pr_refutation(cnf.num_vars, &cnf.clauses, &r.steps), "composed PR proof checks");
1304 let mut full = cnf.clauses.clone();
1306 full.extend(pr_clauses(&r.steps));
1307 assert!(
1308 find_generators(cnf.num_vars, &full).iter().all(|g| g.is_identity()),
1309 "every symmetry of PHP({n}) is broken"
1310 );
1311 }
1312 }
1313
1314 #[test]
1315 fn auto_on_an_asymmetric_unsat_formula_adds_no_sbp_but_refutes() {
1316 let f = vec![
1320 vec![Lit::pos(0)],
1321 vec![Lit::neg(0)],
1322 vec![Lit::pos(0), Lit::pos(1)],
1323 vec![Lit::pos(0), Lit::pos(1), Lit::pos(2)],
1324 ];
1325 let r = certified_unsat_auto(3, &f);
1326 assert_eq!(r.sbp_clauses, 0, "no symmetry to break");
1327 assert!(r.refuted);
1328 assert!(check_pr_refutation(3, &f, &r.steps));
1329 }
1330
1331 #[test]
1332 fn auto_does_not_refute_a_satisfiable_symmetric_formula() {
1333 let f = vec![vec![Lit::pos(0), Lit::pos(1)], vec![Lit::neg(0), Lit::neg(1)]];
1336 let r = certified_unsat_auto(2, &f);
1337 assert!(!r.refuted, "a satisfiable formula is never refuted");
1338 }
1339
1340 #[test]
1341 fn auto_handles_a_lone_empty_clause() {
1342 let (cnf, _) = families::php(1);
1345 assert_eq!(cnf.num_vars, 0);
1346 let r = certified_unsat_auto(cnf.num_vars, &cnf.clauses);
1347 assert_eq!(r.sbp_clauses, 0);
1348 assert!(r.refuted);
1349 assert!(check_pr_refutation(cnf.num_vars, &cnf.clauses, &r.steps));
1350 }
1351
1352 #[test]
1353 fn auto_is_deterministic() {
1354 let (cnf, _) = families::php(3);
1355 let a = certified_unsat_auto(cnf.num_vars, &cnf.clauses);
1356 let b = certified_unsat_auto(cnf.num_vars, &cnf.clauses);
1357 assert_eq!(a.sbp_clauses, b.sbp_clauses, "no wall-clock or hashing nondeterminism");
1358 assert_eq!(a.steps.len(), b.steps.len());
1359 }
1360}