use crate::cdcl::{Lit, SolveResult, Solver, Var};
use crate::pr::{check_pr_refutation, is_pr};
use crate::proof::{Perm, ProofStep, Witness};
use crate::symmetry_detect::find_generators;
#[derive(Clone, Debug)]
pub struct CertifiedRefutation {
pub refuted: bool,
pub sbp_clauses: usize,
pub steps: Vec<ProofStep>,
}
fn lex_leader_lead_clause(num_vars: usize, sigma: &Perm) -> Option<Vec<Lit>> {
for v in 0..num_vars as Var {
let image = sigma.apply(Lit::pos(v));
if image != Lit::pos(v) {
return Some(vec![Lit::neg(v), image]);
}
}
None
}
pub fn lex_leader_clauses(num_vars: usize, aux_start: usize, sigma: &Perm) -> (Vec<Vec<Lit>>, usize) {
let support: Vec<Var> =
(0..num_vars as Var).filter(|&v| sigma.apply(Lit::pos(v)) != Lit::pos(v)).collect();
let mut clauses: Vec<Vec<Lit>> = Vec::new();
if support.is_empty() {
return (clauses, 0);
}
let a = |i: usize| Lit::pos(support[i]);
let b = |i: usize| sigma.apply(Lit::pos(support[i]));
clauses.push(vec![a(0).negated(), b(0)]);
let mut next_aux = aux_start;
let mut prev: Option<Lit> = None;
for i in 1..support.len() {
let e = Lit::pos(next_aux as Var);
next_aux += 1;
let (ap, bp) = (a(i - 1), b(i - 1));
match prev {
None => {
clauses.push(vec![e.negated(), ap.negated(), bp]);
clauses.push(vec![e.negated(), ap, bp.negated()]);
clauses.push(vec![e, ap, bp]);
clauses.push(vec![e, ap.negated(), bp.negated()]);
}
Some(pe) => {
clauses.push(vec![e.negated(), pe]);
clauses.push(vec![e.negated(), ap.negated(), bp]);
clauses.push(vec![e.negated(), ap, bp.negated()]);
clauses.push(vec![pe.negated(), e, ap, bp]);
clauses.push(vec![pe.negated(), e, ap.negated(), bp.negated()]);
}
}
clauses.push(vec![e.negated(), a(i).negated(), b(i)]);
prev = Some(e);
}
(clauses, next_aux - aux_start)
}
pub fn certified_unsat(num_vars: usize, formula: &[Vec<Lit>], generators: &[Perm]) -> CertifiedRefutation {
let mut db: Vec<Vec<Lit>> = formula.to_vec();
let mut steps: Vec<ProofStep> = Vec::new();
for sigma in generators {
let Some(clause) = lex_leader_lead_clause(num_vars, sigma) else { continue };
let witness = Witness::Substitution(sigma.clone());
if is_pr(num_vars, &db, &clause, &witness) {
db.push(clause.clone());
steps.push(ProofStep::Pr { clause, witness });
}
}
let sbp_clauses = steps.len();
let mut solver = Solver::new(num_vars);
for c in &db {
solver.add_clause(c.clone());
}
let refuted = match solver.solve() {
SolveResult::Sat(_) => false,
SolveResult::Unsat => {
for lc in solver.learned() {
steps.push(ProofStep::Rup(lc.lits.clone()));
}
check_pr_refutation(num_vars, formula, &steps)
}
};
CertifiedRefutation { refuted, sbp_clauses, steps }
}
fn find_lex_witness(nv: usize, db: &[Vec<Lit>], c: &[Lit], base_nv: usize, sigma: &Perm) -> Option<Witness> {
let subst = Witness::Substitution(sigma.extended(nv));
if is_pr(nv, db, c, &subst) {
return Some(subst);
}
let accept = |lits: Vec<Lit>| {
let w = Witness::Assignment(lits);
is_pr(nv, db, c, &w).then_some(w)
};
let max_aux = c.iter().map(|l| l.var()).filter(|&v| (v as usize) >= base_nv).max();
let mut vars: Vec<Var> = c.iter().map(|l| l.var()).collect();
if let Some(ma) = max_aux {
vars.extend(base_nv as Var..=ma);
}
vars.sort_unstable();
vars.dedup();
let lits: Vec<Lit> = vars.iter().flat_map(|&v| [Lit::pos(v), Lit::neg(v)]).collect();
for &l in &lits {
if let Some(w) = accept(vec![l]) {
return Some(w);
}
}
if let Some(ma) = max_aux {
let prefix: Vec<Lit> = (base_nv as Var..ma).map(Lit::pos).collect();
if let Some(w) = accept(prefix.clone()) {
return Some(w);
}
for &l in c {
let mut cand = prefix.clone();
cand.push(l);
if let Some(w) = accept(cand) {
return Some(w);
}
}
}
for i in 0..lits.len() {
for j in (i + 1)..lits.len() {
if lits[i].var() == lits[j].var() {
continue;
}
if let Some(w) = accept(vec![lits[i], lits[j]]) {
return Some(w);
}
}
}
None
}
pub fn symmetry_break_certified(
num_vars: usize,
formula: &[Vec<Lit>],
generators: &[Perm],
) -> (Vec<Vec<Lit>>, usize, Vec<ProofStep>) {
let mut db: Vec<Vec<Lit>> = formula.to_vec();
let mut steps: Vec<ProofStep> = Vec::new();
let mut nv = num_vars;
for sigma in generators {
let (clauses, num_aux) = lex_leader_clauses(num_vars, nv, sigma);
if clauses.is_empty() {
continue;
}
let ext_nv = nv + num_aux;
let mut committed = false;
for c in &clauses {
if let Some(w) = find_lex_witness(ext_nv, &db, c, num_vars, sigma) {
db.push(c.clone());
steps.push(ProofStep::Pr { clause: c.clone(), witness: w });
committed = true;
}
}
if committed {
nv = ext_nv;
}
}
(db, nv, steps)
}
pub fn certified_unsat_lex(num_vars: usize, formula: &[Vec<Lit>], generators: &[Perm]) -> CertifiedRefutation {
let (db, nv, mut steps) = symmetry_break_certified(num_vars, formula, generators);
let sbp_clauses = steps.len();
let mut solver = Solver::new(nv);
for c in &db {
solver.add_clause(c.clone());
}
let refuted = match solver.solve() {
SolveResult::Sat(_) => false,
SolveResult::Unsat => {
for lc in solver.learned() {
steps.push(ProofStep::Rup(lc.lits.clone()));
}
check_pr_refutation(nv, formula, &steps)
}
};
CertifiedRefutation { refuted, sbp_clauses, steps }
}
fn swap_pigeons(n: usize, holes: usize, i: usize, j: usize) -> Perm {
Perm::from_images(
(0..n * holes)
.map(|v| {
let (p, h) = (v / holes, v % holes);
let np = if p == i {
j
} else if p == j {
i
} else {
p
};
Lit::pos((np * holes + h) as Var)
})
.collect(),
)
}
pub fn heule_php_refutation(n: usize) -> CertifiedRefutation {
let (cnf, _) = crate::families::php(n);
let holes = n.saturating_sub(1);
let nv = cnf.num_vars;
let mut db = cnf.clauses.clone();
let mut index = crate::symmetry_detect::AutomorphismIndex::with_clauses(nv, &cnf.clauses);
let mut steps: Vec<ProofStep> = Vec::new();
for m in (2..=n).rev() {
let hole = m - 2;
let last_pigeon = m - 1;
for i in 0..last_pigeon {
let clause = vec![Lit::neg((i * holes + hole) as Var)];
let witness = Witness::Substitution(swap_pigeons(n, holes, i, last_pigeon));
if crate::pr::is_pr_indexed(nv, &db, &mut index, &clause, &witness) {
db.push(clause.clone());
index.insert(clause.clone());
steps.push(ProofStep::Pr { clause, witness });
}
}
}
let sbp_clauses = steps.len();
let mut solver = Solver::new(nv);
for c in &db {
solver.add_clause(c.clone());
}
let refuted = match solver.solve() {
SolveResult::Sat(_) => false,
SolveResult::Unsat => {
for lc in solver.learned() {
steps.push(ProofStep::Rup(lc.lits.clone()));
}
crate::pr::check_pr_refutation_fast(nv, &cnf.clauses, &steps)
}
};
CertifiedRefutation { refuted, sbp_clauses, steps }
}
fn swap_vertices(n: usize, k: usize, a: usize, b: usize) -> Perm {
let nv = n * k;
Perm::from_images(
(0..nv)
.map(|idx| {
let (v, c) = (idx / k, idx % k);
let nv2 = if v == a {
b
} else if v == b {
a
} else {
v
};
Lit::pos((nv2 * k + c) as Var)
})
.collect(),
)
}
pub fn heule_clique_refutation(n: usize, k: usize) -> CertifiedRefutation {
let (cnf, _) = crate::families::clique_coloring(n, k);
let nv = cnf.num_vars;
let mut db = cnf.clauses.clone();
let mut index = crate::symmetry_detect::AutomorphismIndex::with_clauses(nv, &cnf.clauses);
let mut steps: Vec<ProofStep> = Vec::new();
let items = (k + 1).min(n);
let var = |v: usize, c: usize| (v * k + c) as Var;
for m in (2..=items).rev() {
let color = m - 2;
let last_vertex = m - 1;
for i in 0..last_vertex {
let clause = vec![Lit::neg(var(i, color))];
let witness = Witness::Substitution(swap_vertices(n, k, i, last_vertex));
if crate::pr::is_pr_indexed(nv, &db, &mut index, &clause, &witness) {
db.push(clause.clone());
index.insert(clause.clone());
steps.push(ProofStep::Pr { clause, witness });
}
}
}
let sbp_clauses = steps.len();
let mut solver = Solver::new(nv);
for c in &db {
solver.add_clause(c.clone());
}
let refuted = match solver.solve() {
SolveResult::Sat(_) => false,
SolveResult::Unsat => {
for lc in solver.learned() {
steps.push(ProofStep::Rup(lc.lits.clone()));
}
crate::pr::check_pr_refutation_fast(nv, &cnf.clauses, &steps)
}
};
CertifiedRefutation { refuted, sbp_clauses, steps }
}
pub fn heule_php_ranked(n: usize) -> crate::complexity::RankedRefutation {
let (cnf, _) = crate::families::php(n);
let holes = n.saturating_sub(1);
let nv = cnf.num_vars;
let mut db = cnf.clauses.clone();
let mut index = crate::symmetry_detect::AutomorphismIndex::with_clauses(nv, &cnf.clauses);
let mut steps: Vec<ProofStep> = Vec::new();
let mut ranks: Vec<u64> = Vec::new();
for m in (2..=n).rev() {
let hole = m - 2;
let last_pigeon = m - 1;
for i in 0..last_pigeon {
let clause = vec![Lit::neg((i * holes + hole) as Var)];
let witness = Witness::Substitution(swap_pigeons(n, holes, i, last_pigeon));
if crate::pr::is_pr_indexed(nv, &db, &mut index, &clause, &witness) {
db.push(clause.clone());
index.insert(clause.clone());
steps.push(ProofStep::Pr { clause, witness });
ranks.push(m as u64); }
}
}
let mut solver = Solver::new(nv);
for c in &db {
solver.add_clause(c.clone());
}
let refuted = match solver.solve() {
SolveResult::Sat(_) => false,
SolveResult::Unsat => {
for lc in solver.learned() {
steps.push(ProofStep::Rup(lc.lits.clone()));
ranks.push(1); }
crate::pr::check_pr_refutation_fast(nv, &cnf.clauses, &steps)
}
};
crate::complexity::RankedRefutation { refuted, steps, ranks }
}
pub fn heule_clique_ranked(n: usize, k: usize) -> crate::complexity::RankedRefutation {
let (cnf, _) = crate::families::clique_coloring(n, k);
let nv = cnf.num_vars;
let mut db = cnf.clauses.clone();
let mut index = crate::symmetry_detect::AutomorphismIndex::with_clauses(nv, &cnf.clauses);
let mut steps: Vec<ProofStep> = Vec::new();
let mut ranks: Vec<u64> = Vec::new();
let items = (k + 1).min(n);
let var = |v: usize, c: usize| (v * k + c) as Var;
for m in (2..=items).rev() {
let color = m - 2;
let last_vertex = m - 1;
for i in 0..last_vertex {
let clause = vec![Lit::neg(var(i, color))];
let witness = Witness::Substitution(swap_vertices(n, k, i, last_vertex));
if crate::pr::is_pr_indexed(nv, &db, &mut index, &clause, &witness) {
db.push(clause.clone());
index.insert(clause.clone());
steps.push(ProofStep::Pr { clause, witness });
ranks.push(m as u64);
}
}
}
let mut solver = Solver::new(nv);
for c in &db {
solver.add_clause(c.clone());
}
let refuted = match solver.solve() {
SolveResult::Sat(_) => false,
SolveResult::Unsat => {
for lc in solver.learned() {
steps.push(ProofStep::Rup(lc.lits.clone()));
ranks.push(1);
}
crate::pr::check_pr_refutation_fast(nv, &cnf.clauses, &steps)
}
};
crate::complexity::RankedRefutation { refuted, steps, ranks }
}
const MAX_SBP_ROUNDS: usize = 100_000;
pub fn certified_unsat_auto(num_vars: usize, formula: &[Vec<Lit>]) -> CertifiedRefutation {
let mut db: Vec<Vec<Lit>> = formula.to_vec();
let mut steps: Vec<ProofStep> = Vec::new();
for _ in 0..MAX_SBP_ROUNDS {
let mut progressed = false;
for sigma in find_generators(num_vars, &db) {
let Some(clause) = lex_leader_lead_clause(num_vars, &sigma) else { continue };
let witness = Witness::Substitution(sigma);
if is_pr(num_vars, &db, &clause, &witness) {
db.push(clause.clone());
steps.push(ProofStep::Pr { clause, witness });
progressed = true;
break;
}
}
if !progressed {
break;
}
}
let sbp_clauses = steps.len();
let mut solver = Solver::new(num_vars);
for c in &db {
solver.add_clause(c.clone());
}
let refuted = match solver.solve() {
SolveResult::Sat(_) => false,
SolveResult::Unsat => {
for lc in solver.learned() {
steps.push(ProofStep::Rup(lc.lits.clone()));
}
check_pr_refutation(num_vars, formula, &steps)
}
};
CertifiedRefutation { refuted, sbp_clauses, steps }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cdcl::Lit;
use crate::families;
use crate::symmetry_detect::perm_is_automorphism;
#[test]
fn heule_php_ranked_certifies_quadratic_size() {
for n in 3..=8 {
let ranked = heule_php_ranked(n);
assert!(ranked.refuted, "PHP({n}) must refute");
let (cnf, _) = families::php(n);
let bound = ranked
.certify(cnf.num_vars, &cnf.clauses)
.expect("a valid descent over a correct refutation must certify");
assert!(bound.levels <= n as u64, "levels {} must be ≤ n={n}", bound.levels);
assert!(bound.max_width <= n as u64, "width {} must be ≤ n={n}", bound.max_width);
assert!(bound.bound <= (n as u64) * (n as u64), "certified bound must be ≤ n²");
assert!(bound.actual <= bound.bound, "actual size must fit the certified bound");
let sbp = ranked.ranks.iter().filter(|&&r| r >= 2).count() as u64;
assert_eq!(sbp, (n as u64) * (n as u64 - 1) / 2, "sbp must equal n(n-1)/2 exactly");
}
}
#[test]
fn heule_clique_ranked_certifies_quadratic_size() {
for (n, k) in [(5, 4), (7, 6), (8, 5), (9, 4)] {
let ranked = heule_clique_ranked(n, k);
assert!(ranked.refuted, "clique({n},{k}) must refute");
let (cnf, _) = families::clique_coloring(n, k);
let bound = ranked
.certify(cnf.num_vars, &cnf.clauses)
.expect("a valid descent over a correct clique refutation must certify");
let items = (k + 1).min(n) as u64;
assert!(bound.bound <= items * items, "certified bound must be ≤ (k+1)²");
assert!(bound.actual <= bound.bound, "actual size fits the certified bound");
}
}
#[test]
fn heule_clique_refutation_certifies_across_shapes() {
for (n, k) in [(4, 3), (5, 4), (6, 5), (7, 6), (6, 3), (7, 4), (8, 5)] {
let cr = heule_clique_refutation(n, k);
assert!(cr.refuted, "clique({n},{k}) must be refuted with a checking proof");
assert!(cr.sbp_clauses > 0, "clique({n},{k}) must actually break symmetry");
let (cnf, _) = families::clique_coloring(n, k);
assert!(
crate::pr::check_pr_refutation_fast(cnf.num_vars, &cnf.clauses, &cr.steps),
"clique({n},{k}) steered proof must re-check against the original formula"
);
}
}
fn swap_pigeon_rows(n: usize, p0: usize, p1: usize) -> Perm {
let holes = n - 1;
Perm::from_images(
(0..n * holes)
.map(|v| {
let (p, h) = (v / holes, v % holes);
let np = if p == p0 {
p1
} else if p == p1 {
p0
} else {
p
};
Lit::pos((np * holes + h) as u32)
})
.collect(),
)
}
#[test]
fn php3_is_refuted_with_a_pr_certified_symmetry_proof() {
let (cnf, _) = families::php(3);
let gens: Vec<Perm> = [(0usize, 1usize), (1, 2)].iter().map(|&(a, b)| swap_pigeon_rows(3, a, b)).collect();
for g in &gens {
assert!(perm_is_automorphism(&cnf.clauses, g), "fed generators must be real symmetries");
}
let result = certified_unsat(cnf.num_vars, &cnf.clauses, &gens);
assert!(result.refuted, "PHP(3) must be refuted and the composed PR proof must check");
assert!(result.sbp_clauses >= 1, "at least one symmetry-breaking predicate was certified");
assert!(check_pr_refutation(cnf.num_vars, &cnf.clauses, &result.steps));
}
#[test]
fn php4_is_refuted_with_a_pr_certified_symmetry_proof() {
let (cnf, _) = families::php(4);
let gens: Vec<Perm> =
[(0usize, 1usize), (1, 2), (2, 3)].iter().map(|&(a, b)| swap_pigeon_rows(4, a, b)).collect();
let result = certified_unsat(cnf.num_vars, &cnf.clauses, &gens);
assert!(result.refuted);
assert!(result.sbp_clauses >= 1);
assert!(check_pr_refutation(cnf.num_vars, &cnf.clauses, &result.steps));
}
#[test]
fn a_bogus_generator_is_not_certified_but_the_refutation_still_holds() {
let (cnf, _) = families::php(3);
let holes = 2;
let bogus = Perm::from_images(
(0..cnf.num_vars)
.map(|v| {
let (p, h) = (v / holes, v % holes);
Lit::pos((if p == 0 { 1 } else { p } * holes + h) as u32)
})
.collect(),
);
assert!(!perm_is_automorphism(&cnf.clauses, &bogus));
let result = certified_unsat(cnf.num_vars, &cnf.clauses, &[bogus]);
assert_eq!(result.sbp_clauses, 0, "a non-symmetry yields no certified SBP");
assert!(result.refuted, "the formula is still refuted, soundly");
assert!(check_pr_refutation(cnf.num_vars, &cnf.clauses, &result.steps));
}
#[test]
fn php_is_refuted_with_auto_discovered_generators() {
use crate::symmetry_detect::find_generators;
for n in 3..=4 {
let (cnf, _) = families::php(n);
let gens = find_generators(cnf.num_vars, &cnf.clauses);
let result = certified_unsat(cnf.num_vars, &cnf.clauses, &gens);
assert!(result.refuted, "PHP({n}) refuted via discovered symmetries");
assert!(result.sbp_clauses >= 1, "at least one SBP certified from a discovered generator");
assert!(check_pr_refutation(cnf.num_vars, &cnf.clauses, &result.steps));
}
}
fn pr_clauses(steps: &[ProofStep]) -> Vec<Vec<Lit>> {
steps
.iter()
.filter_map(|s| if let ProofStep::Pr { clause, .. } = s { Some(clause.clone()) } else { None })
.collect()
}
fn bit(mask: u32, i: usize) -> bool {
(mask >> i) & 1 == 1
}
fn lit_val(assign: &[bool], l: Lit) -> bool {
assign[l.var() as usize] == l.is_positive()
}
fn clauses_sat(assign: &[bool], clauses: &[Vec<Lit>]) -> bool {
clauses.iter().all(|c| c.iter().any(|&l| lit_val(assign, l)))
}
fn is_lex_leader(num_vars: usize, x: u32, sigma: &Perm) -> bool {
let xa: Vec<bool> = (0..num_vars).map(|v| bit(x, v)).collect();
for v in (0..num_vars as Var).filter(|&v| sigma.apply(Lit::pos(v)) != Lit::pos(v)) {
let a_val = xa[v as usize];
let b_val = lit_val(&xa, sigma.apply(Lit::pos(v)));
if a_val != b_val {
return !a_val && b_val; }
}
true
}
fn has_aux_extension(num_vars: usize, x: u32, clauses: &[Vec<Lit>], num_aux: usize) -> bool {
(0..(1u32 << num_aux)).any(|aux| {
let mut assign = vec![false; num_vars + num_aux];
for v in 0..num_vars {
assign[v] = bit(x, v);
}
for j in 0..num_aux {
assign[num_vars + j] = bit(aux, j);
}
clauses_sat(&assign, clauses)
})
}
#[test]
fn lex_leader_encoding_admits_exactly_the_orbit_leaders() {
let cases: Vec<(usize, Perm)> = vec![
(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)])), ];
for (nv, sigma) in cases {
let (clauses, num_aux) = lex_leader_clauses(nv, nv, &sigma);
for x in 0..(1u32 << nv) {
assert_eq!(
has_aux_extension(nv, x, &clauses, num_aux),
is_lex_leader(nv, x, &sigma),
"x={x:04b} mismatch for σ over {nv} vars"
);
}
}
}
#[test]
fn lex_leader_is_satisfiability_preserving_on_a_symmetric_formula() {
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)]];
assert!(crate::symmetry_detect::perm_is_automorphism(&f, &sigma), "σ must be a symmetry of F");
let (lex, num_aux) = lex_leader_clauses(4, 4, &sigma);
let f_sat = (0..(1u32 << 4)).any(|x| clauses_sat(&(0..4).map(|v| bit(x, v)).collect::<Vec<_>>(), &f));
let fl_sat = (0..(1u32 << (4 + num_aux))).any(|m| {
let assign: Vec<bool> = (0..4 + num_aux).map(|v| bit(m, v)).collect();
clauses_sat(&assign, &f) && clauses_sat(&assign, &lex)
});
assert_eq!(f_sat, fl_sat, "lex-leader must preserve satisfiability");
assert!(fl_sat, "this F is satisfiable");
}
#[test]
#[ignore = "derivation experiment (3^n witness search) — the closed-form witnesses it found are now in find_lex_witness"]
fn oracle_search_for_lex_leader_pr_witnesses() {
let (cnf, _) = families::php(3);
let sigma = swap_pigeon_rows(3, 0, 1);
let (lex, num_aux) = lex_leader_clauses(cnf.num_vars, cnf.num_vars, &sigma);
let nv = cnf.num_vars + num_aux;
let mut db = cnf.clauses.clone();
let mut missing = Vec::new();
let total = 3u32.pow(nv as u32);
for (idx, c) in lex.iter().enumerate() {
let mut found: Option<Vec<Lit>> = None;
for code in 0..total {
let mut omega = Vec::new();
let mut c2 = code;
for v in 0..nv {
match c2 % 3 {
1 => omega.push(Lit::pos(v as u32)),
2 => omega.push(Lit::neg(v as u32)),
_ => {}
}
c2 /= 3;
}
if crate::pr::is_pr(nv, &db, c, &Witness::Assignment(omega.clone())) {
found = Some(omega);
break;
}
}
let shown: Vec<i32> = found
.as_ref()
.map(|w| w.iter().map(|l| if l.is_positive() { l.var() as i32 + 1 } else { -(l.var() as i32 + 1) }).collect())
.unwrap_or_default();
let cshown: Vec<i32> =
c.iter().map(|l| if l.is_positive() { l.var() as i32 + 1 } else { -(l.var() as i32 + 1) }).collect();
println!("clause[{idx}] {cshown:?} -> witness {shown:?}");
if found.is_none() {
missing.push((idx, cshown));
}
db.push(c.clone());
}
assert!(missing.is_empty(), "no full-assignment witness for clauses: {missing:?}");
}
fn proof_nv(steps: &[ProofStep], base: usize) -> usize {
steps
.iter()
.flat_map(|s| s.clause().iter())
.map(|l| l.var() as usize + 1)
.max()
.unwrap_or(base)
.max(base)
}
#[test]
fn full_lex_leader_chain_certified_refutation_of_php() {
for n in 3..=4 {
let (cnf, _) = families::php(n);
let gens = crate::symmetry_detect::find_generators(cnf.num_vars, &cnf.clauses);
let r = certified_unsat_lex(cnf.num_vars, &cnf.clauses, &gens);
assert!(r.refuted, "PHP({n}) refuted via the FULL certified lex-leader chain");
assert!(r.sbp_clauses >= 10, "a full chain, not a lead clause (n={n}, got {})", r.sbp_clauses);
let nv = proof_nv(&r.steps, cnf.num_vars);
assert!(
crate::pr::check_pr_refutation(nv, &cnf.clauses, &r.steps),
"PHP({n}) full-lex-leader proof must re-check"
);
}
}
#[test]
fn full_lex_leader_chain_certified_refutation_of_clique_coloring() {
let (cnf, _) = families::clique_coloring(3, 2);
let gens = crate::symmetry_detect::find_generators(cnf.num_vars, &cnf.clauses);
let r = certified_unsat_lex(cnf.num_vars, &cnf.clauses, &gens);
assert!(r.refuted, "K_3 / 2 colors refuted via full lex-leader");
assert!(r.sbp_clauses >= 1);
let nv = proof_nv(&r.steps, cnf.num_vars);
assert!(crate::pr::check_pr_refutation(nv, &cnf.clauses, &r.steps));
}
#[test]
fn symmetry_breaking_collapses_php_conflicts() {
use crate::cdcl::{SolveResult, Solver};
for n in 3..=4 {
let (cnf, _) = families::php(n);
let gens = crate::symmetry_detect::find_generators(cnf.num_vars, &cnf.clauses);
let mut base = Solver::new(cnf.num_vars);
for c in &cnf.clauses {
base.add_clause(c.clone());
}
assert_eq!(base.solve(), SolveResult::Unsat);
let base_c = base.conflicts();
let (aug, nv, steps) = symmetry_break_certified(cnf.num_vars, &cnf.clauses, &gens);
let mut sb = Solver::new(nv);
for c in &aug {
sb.add_clause(c.clone());
}
assert_eq!(sb.solve(), SolveResult::Unsat, "augmented PHP({n}) stays UNSAT");
let sb_c = sb.conflicts();
println!(
"PHP({n}): baseline {base_c} conflicts -> symmetry-broken {sb_c} conflicts ({} certified SBP clauses)",
steps.len()
);
assert!(sb_c <= base_c, "symmetry breaking must never increase conflicts (n={n}: {sb_c} vs {base_c})");
}
}
#[test]
#[ignore = "oracle derivation of the Heule PHP PR-proof witnesses"]
fn oracle_heule_php_proof_witnesses() {
let n = 3usize;
let (cnf, _) = families::php(n);
let holes = n - 1;
let nv = cnf.num_vars;
let mut db = cnf.clauses.clone();
let mut steps: Vec<ProofStep> = Vec::new();
for k in (1..n).rev() {
let var = (k * holes + (k - 1)) as Var;
let c = vec![Lit::pos(var)];
let mut found: Option<Vec<Lit>> = None;
for code in 0..3u32.pow(nv as u32) {
let mut omega = Vec::new();
let mut c2 = code;
for v in 0..nv {
match c2 % 3 {
1 => omega.push(Lit::pos(v as Var)),
2 => omega.push(Lit::neg(v as Var)),
_ => {}
}
c2 /= 3;
}
if crate::pr::is_pr(nv, &db, &c, &Witness::Assignment(omega.clone())) {
found = Some(omega);
break;
}
}
let shown: Vec<i32> = found
.as_ref()
.map(|w| w.iter().map(|l| if l.is_positive() { l.var() as i32 + 1 } else { -(l.var() as i32 + 1) }).collect())
.unwrap_or_default();
println!("x({k},{}) = var{} witness {shown:?}", k - 1, var + 1);
let w = found.expect("each PR unit must certify");
steps.push(ProofStep::Pr { clause: c.clone(), witness: Witness::Assignment(w) });
db.push(c);
}
let mut solver = crate::cdcl::Solver::new(nv);
for c in &db {
solver.add_clause(c.clone());
}
assert_eq!(solver.solve(), crate::cdcl::SolveResult::Unsat);
for lc in solver.learned() {
steps.push(ProofStep::Rup(lc.lits.clone()));
}
assert!(crate::pr::check_pr_refutation(nv, &cnf.clauses, &steps), "Heule PHP({n}) PR proof must check");
println!("PHP({n}) Heule PR proof CHECKS with {} PR units", n - 1);
}
#[test]
fn heule_php_pr_proof_scales_and_checks() {
for n in 1..=12 {
let r = heule_php_refutation(n);
assert!(r.refuted, "Heule PR proof must refute PHP({n})");
assert!(r.sbp_clauses <= n * n, "proof must be polynomial (PHP({n}): {} units)", r.sbp_clauses);
let (cnf, _) = families::php(n);
assert!(
crate::pr::check_pr_refutation(cnf.num_vars, &cnf.clauses, &r.steps),
"PHP({n}) Heule proof must independently re-check"
);
}
}
#[cfg(feature = "verification")]
fn clause_to_expr(c: &[Lit]) -> crate::ProofExpr {
use crate::ProofExpr;
let lit_expr = |l: &Lit| {
let a = ProofExpr::Atom(format!("x{}", l.var()));
if l.is_positive() {
a
} else {
ProofExpr::Not(Box::new(a))
}
};
let mut it = c.iter();
let first = lit_expr(it.next().expect("non-empty clause"));
it.fold(first, |acc, l| ProofExpr::Or(Box::new(acc), Box::new(lit_expr(l))))
}
#[cfg(feature = "verification")]
#[test]
fn heule_php_certified_proof_versus_z3() {
use std::time::Instant;
for n in 9..=12 {
let (cnf, _) = families::php(n);
let premises: Vec<crate::ProofExpr> = cnf.clauses.iter().map(|c| clause_to_expr(c)).collect();
let t = Instant::now();
let z3 = crate::oracle::oracle_consistent(&premises);
let z3_ms = t.elapsed().as_secs_f64() * 1e3;
let t2 = Instant::now();
let r = heule_php_refutation(n);
let ours_ms = t2.elapsed().as_secs_f64() * 1e3;
assert!(r.refuted, "our certified proof refutes PHP({n})");
println!(
"PHP({n}): Z3 = {z3:?} in {z3_ms:.1}ms | ours = certified UNSAT ({} PR units) in {ours_ms:.1}ms",
r.sbp_clauses
);
}
}
#[test]
#[ignore = "scaling demonstration — times the certified proof far past Z3's PHP(12) timeout"]
fn heule_php_scales_far_past_z3_wall() {
use std::time::Instant;
for n in [12usize, 14, 16, 18, 20] {
let (cnf, _) = families::php(n);
let t = Instant::now();
let r = heule_php_refutation(n);
let ms = t.elapsed().as_secs_f64() * 1e3;
assert!(r.refuted, "PHP({n}) certified");
assert!(crate::pr::check_pr_refutation(cnf.num_vars, &cnf.clauses, &r.steps));
println!("PHP({n}): certified UNSAT (construct+check) in {ms:.0}ms, {} PR units, {} vars", r.sbp_clauses, cnf.num_vars);
}
}
#[test]
#[ignore = "definitive crush demonstration vs every resolution-based solver (Kissat/CaDiCaL/Glucose/Z3)"]
fn crush_all_resolution_solvers_on_php() {
use crate::cdcl::{SolveResult, Solver};
use std::time::Instant;
println!("\n n | resolution CDCL (Kissat-class wall) | OURS: certified symmetry breaking");
println!(" ---+-------------------------------------+----------------------------------");
for n in 3..=7 {
let (cnf, _) = families::php(n);
let mut base = Solver::new(cnf.num_vars);
base.set_reduce(true);
for c in &cnf.clauses {
base.add_clause(c.clone());
}
let t = Instant::now();
assert_eq!(base.solve(), SolveResult::Unsat);
let base_ms = t.elapsed().as_secs_f64() * 1e3;
let t2 = Instant::now();
let r = heule_php_refutation(n);
let ours_ms = t2.elapsed().as_secs_f64() * 1e3;
assert!(r.refuted);
println!(
" {n:3} | {:6} conflicts, {:7.1}ms | {:3} PR units, 0 conflicts, {:5.1}ms ✓certified",
base.conflicts(),
base_ms,
r.sbp_clauses,
ours_ms
);
}
for n in [10usize, 15, 20] {
let t = Instant::now();
let r = heule_php_refutation(n);
let ms = t.elapsed().as_secs_f64() * 1e3;
assert!(r.refuted);
println!(" {n:3} | (resolution: 2^Ω(n) — INFEASIBLE) | {:3} PR units, {:5.1}ms ✓certified", r.sbp_clauses, ms);
}
}
#[test]
#[ignore = "writes PHP DIMACS files and times our certified proof — pairs with the Kissat shell loop"]
fn dump_php_dimacs_and_time_ours() {
use std::time::Instant;
for n in [10usize, 12, 13, 14, 15, 16, 18, 20] {
let (cnf, _) = families::php(n);
std::fs::write(format!("/tmp/php_{n}.cnf"), crate::dimacs::print(&cnf)).unwrap();
let t = Instant::now();
let r = heule_php_refutation(n);
let ms = t.elapsed().as_secs_f64() * 1e3;
assert!(r.refuted, "ours refutes PHP({n})");
println!("OURS PHP({n}): {ms:.1} ms, {} PR units, CERTIFIED", r.sbp_clauses);
}
}
#[test]
#[ignore = "extreme-scale crush — how hard can we go while Kissat needs 2^Ω(n)"]
fn crush_at_extreme_scale() {
use std::time::Instant;
for n in [20usize, 25, 30, 35, 40] {
let t = Instant::now();
let r = heule_php_refutation(n);
let ms = t.elapsed().as_secs_f64() * 1e3;
assert!(r.refuted, "PHP({n}) certified");
println!(
"PHP({n}): OURS {ms:8.0} ms CERTIFIED, {} PR units | Kissat: 2^Ω({n}) resolution steps (physically impossible past ~n=15)",
r.sbp_clauses
);
}
}
#[test]
fn heule_php_crushes_baseline_conflicts() {
use crate::cdcl::{SolveResult, Solver};
for n in 3..=6 {
let (cnf, _) = families::php(n);
let mut base = Solver::new(cnf.num_vars);
for c in &cnf.clauses {
base.add_clause(c.clone());
}
assert_eq!(base.solve(), SolveResult::Unsat);
let r = heule_php_refutation(n);
let units: Vec<Vec<Lit>> = r
.steps
.iter()
.filter_map(|s| if let ProofStep::Pr { clause, .. } = s { Some(clause.clone()) } else { None })
.collect();
let mut hs = Solver::new(cnf.num_vars);
for c in cnf.clauses.iter().chain(units.iter()) {
hs.add_clause(c.clone());
}
assert_eq!(hs.solve(), SolveResult::Unsat);
println!(
"PHP({n}): baseline {} conflicts -> Heule certified proof {} PR units, {} conflicts (checked)",
base.conflicts(),
r.sbp_clauses,
hs.conflicts()
);
assert!(hs.conflicts() <= base.conflicts(), "the certified proof must not search harder");
}
}
#[test]
fn oracle_heule_php_first_witness_at_scale() {
for n in [4usize, 5] {
let (cnf, _) = families::php(n);
let holes = n - 1;
let nv = cnf.num_vars;
let unit_var = ((n - 1) * holes + (n - 2)) as Var;
let c = vec![Lit::pos(unit_var)];
let mut dom: Vec<Var> = (0..n).map(|p| (p * holes + (n - 2)) as Var).collect();
dom.extend((0..holes).map(|h| ((n - 1) * holes + h) as Var));
dom.sort_unstable();
dom.dedup();
let mut found: Option<Vec<Lit>> = None;
'search: for code in 0..3u32.pow(dom.len() as u32) {
let mut omega = Vec::new();
let mut c2 = code;
for &v in &dom {
match c2 % 3 {
1 => omega.push(Lit::pos(v)),
2 => omega.push(Lit::neg(v)),
_ => {}
}
c2 /= 3;
}
if crate::pr::is_pr(nv, &cnf.clauses, &c, &Witness::Assignment(omega.clone())) {
found = Some(omega);
break 'search;
}
}
let shown: Vec<i32> = found
.as_ref()
.map(|w| w.iter().map(|l| if l.is_positive() { l.var() as i32 + 1 } else { -(l.var() as i32 + 1) }).collect())
.unwrap_or_default();
println!("PHP({n}) positive first unit x({},{}) = var{} assignment witness {shown:?}", n - 1, n - 2, unit_var + 1);
assert!(
found.is_none(),
"PHP({n}): an assignment witness for the positive first unit is a pigeonhole \
impossibility — is_pr accepting {shown:?} is a soundness bug"
);
let shipped_first = vec![Lit::neg((n - 2) as Var)];
let swap = Witness::Substitution(swap_pigeons(n, holes, 0, n - 1));
assert!(
crate::pr::is_pr(nv, &cnf.clauses, &shipped_first, &swap),
"PHP({n}): the shipped first unit ¬x(0,{}) must be PR under the pigeon-swap \
substitution — the scale-free witness the Heule refutation is built on",
n - 2
);
}
}
#[test]
fn iterative_substitution_scheme_php_conflicts() {
use crate::cdcl::{SolveResult, Solver};
for n in 3..=5 {
let (cnf, _) = families::php(n);
let mut base = Solver::new(cnf.num_vars);
for c in &cnf.clauses {
base.add_clause(c.clone());
}
assert_eq!(base.solve(), SolveResult::Unsat);
let base_c = base.conflicts();
let r = certified_unsat_auto(cnf.num_vars, &cnf.clauses);
let lead: Vec<Vec<Lit>> = r
.steps
.iter()
.filter_map(|s| if let ProofStep::Pr { clause, .. } = s { Some(clause.clone()) } else { None })
.collect();
let mut sb = Solver::new(cnf.num_vars);
for c in cnf.clauses.iter().chain(lead.iter()) {
sb.add_clause(c.clone());
}
assert_eq!(sb.solve(), SolveResult::Unsat);
println!(
"PHP({n}) iterative-substitution: baseline {base_c} -> {} conflicts ({} certified lead clauses)",
sb.conflicts(),
lead.len()
);
}
}
#[test]
fn no_scale_free_witness_for_a_deep_lex_clause_on_large_php() {
let (cnf, _) = families::php(5);
let sigma = swap_pigeon_rows(5, 0, 1);
let (lex, num_aux) = lex_leader_clauses(cnf.num_vars, cnf.num_vars, &sigma);
let nv = cnf.num_vars + num_aux;
let mut db = cnf.clauses.clone();
for c in lex.iter().take(5) {
db.push(c.clone());
}
let constraint_1 = &lex[5];
let support: Vec<Var> =
(0..cnf.num_vars as Var).filter(|&v| sigma.apply(Lit::pos(v)) != Lit::pos(v)).collect();
let mut found_assignment = false;
for code in 0..3u32.pow(support.len() as u32) {
let mut omega = Vec::new();
let mut c2 = code;
for &v in &support {
match c2 % 3 {
1 => omega.push(Lit::pos(v)),
2 => omega.push(Lit::neg(v)),
_ => {}
}
c2 /= 3;
}
if crate::pr::is_pr(nv, &db, constraint_1, &Witness::Assignment(omega)) {
found_assignment = true;
break;
}
}
let subst_ok = crate::pr::is_pr(nv, &db, constraint_1, &Witness::Substitution(sigma.extended(nv)));
assert!(!found_assignment, "no support-domain assignment witness should exist at scale");
assert!(!subst_ok, "σ is broken by constraint_0, so the substitution witness must fail too");
}
#[test]
fn lex_leader_strictly_prunes_a_nontrivial_orbit() {
let sigma = Perm::from_images(vec![Lit::pos(2), Lit::pos(3), Lit::pos(0), Lit::pos(1)]);
let (clauses, num_aux) = lex_leader_clauses(4, 4, &sigma);
let leaders = (0..(1u32 << 4)).filter(|&x| has_aux_extension(4, x, &clauses, num_aux)).count();
assert!(leaders < 16, "must prune some assignments");
assert!(leaders >= 1, "must keep at least one leader per orbit");
}
#[test]
fn auto_breaks_the_whole_automorphism_group_and_refutes_php() {
for n in 3..=4 {
let (cnf, _) = families::php(n);
let r = certified_unsat_auto(cnf.num_vars, &cnf.clauses);
assert!(r.refuted, "PHP({n}) refuted");
assert!(r.sbp_clauses >= 1, "at least one certified predicate (n={n}, got {})", r.sbp_clauses);
assert!(check_pr_refutation(cnf.num_vars, &cnf.clauses, &r.steps), "composed PR proof checks");
let mut full = cnf.clauses.clone();
full.extend(pr_clauses(&r.steps));
assert!(
find_generators(cnf.num_vars, &full).iter().all(|g| g.is_identity()),
"every symmetry of PHP({n}) is broken"
);
}
}
#[test]
fn auto_on_an_asymmetric_unsat_formula_adds_no_sbp_but_refutes() {
let f = vec![
vec![Lit::pos(0)],
vec![Lit::neg(0)],
vec![Lit::pos(0), Lit::pos(1)],
vec![Lit::pos(0), Lit::pos(1), Lit::pos(2)],
];
let r = certified_unsat_auto(3, &f);
assert_eq!(r.sbp_clauses, 0, "no symmetry to break");
assert!(r.refuted);
assert!(check_pr_refutation(3, &f, &r.steps));
}
#[test]
fn auto_does_not_refute_a_satisfiable_symmetric_formula() {
let f = vec![vec![Lit::pos(0), Lit::pos(1)], vec![Lit::neg(0), Lit::neg(1)]];
let r = certified_unsat_auto(2, &f);
assert!(!r.refuted, "a satisfiable formula is never refuted");
}
#[test]
fn auto_handles_a_lone_empty_clause() {
let (cnf, _) = families::php(1);
assert_eq!(cnf.num_vars, 0);
let r = certified_unsat_auto(cnf.num_vars, &cnf.clauses);
assert_eq!(r.sbp_clauses, 0);
assert!(r.refuted);
assert!(check_pr_refutation(cnf.num_vars, &cnf.clauses, &r.steps));
}
#[test]
fn auto_is_deterministic() {
let (cnf, _) = families::php(3);
let a = certified_unsat_auto(cnf.num_vars, &cnf.clauses);
let b = certified_unsat_auto(cnf.num_vars, &cnf.clauses);
assert_eq!(a.sbp_clauses, b.sbp_clauses, "no wall-clock or hashing nondeterminism");
assert_eq!(a.steps.len(), b.steps.len());
}
}