use crate::sat::LearnedStep;
use std::fmt::Write as _;
pub fn emit_lrat(n_orig: usize, trace: &[LearnedStep]) -> String {
let mut out = String::new();
for (k, step) in trace.iter().enumerate() {
let id = n_orig + k + 1;
let _ = write!(out, "{id}");
for lit in &step.clause {
let _ = write!(out, " {lit}");
}
out.push_str(" 0");
for &ante in &step.antecedents {
let _ = write!(out, " {}", ante + 1);
}
out.push_str(" 0\n");
}
out
}
pub fn emit_lrat_trimmed(n_orig: usize, trace: &[LearnedStep]) -> String {
let Some(last) = trace.len().checked_sub(1) else {
return emit_lrat(n_orig, trace);
};
if !trace[last].clause.is_empty() {
return emit_lrat(n_orig, trace);
}
let mut keep = vec![false; trace.len()];
let mut work = vec![last];
keep[last] = true;
while let Some(k) = work.pop() {
for &ante in &trace[k].antecedents {
if ante >= n_orig {
let j = ante - n_orig;
if !keep[j] {
keep[j] = true;
work.push(j);
}
}
}
}
let mut rank = vec![usize::MAX; trace.len()];
let mut next = 0usize;
for (k, &kept) in keep.iter().enumerate() {
if kept {
rank[k] = next;
next += 1;
}
}
let mut out = String::new();
for (k, step) in trace.iter().enumerate() {
if !keep[k] {
continue;
}
let id = n_orig + rank[k] + 1;
let _ = write!(out, "{id}");
for lit in &step.clause {
let _ = write!(out, " {lit}");
}
out.push_str(" 0");
for &ante in &step.antecedents {
let mapped = if ante < n_orig {
ante + 1
} else {
n_orig + rank[ante - n_orig] + 1
};
let _ = write!(out, " {mapped}");
}
out.push_str(" 0\n");
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cnf::CnfFormula;
use crate::sat::{SatResult, SatSolver};
fn unsat_cert(clauses: Vec<Vec<i32>>, num_vars: u32) -> (Vec<Vec<i32>>, String) {
let formula = CnfFormula {
num_vars,
clauses: clauses.clone(),
};
let mut solver = SatSolver::new();
assert_eq!(solver.solve(&formula), SatResult::Unsat, "expected UNSAT");
let cert = emit_lrat(clauses.len(), solver.proof_trace());
(clauses, cert)
}
#[test]
fn two_units_certificate_is_accepted() {
let (clauses, cert) = unsat_cert(vec![vec![1], vec![-1]], 1);
assert!(!cert.is_empty());
ordeal_lrat::check(&clauses, &cert).expect("checker must accept");
}
#[test]
fn four_clause_contradiction_certificate_is_accepted() {
let (clauses, cert) =
unsat_cert(vec![vec![1, 2], vec![-1, 2], vec![1, -2], vec![-1, -2]], 2);
ordeal_lrat::check(&clauses, &cert).expect("checker must accept");
}
#[test]
fn pigeonhole_certificate_is_accepted() {
let v = |i: i32, h: i32| 3 * (i - 1) + h;
let mut clauses: Vec<Vec<i32>> = (1..=4)
.map(|i| (1..=3).map(|h| v(i, h)).collect())
.collect();
for h in 1..=3 {
for i in 1..=4 {
for j in (i + 1)..=4 {
clauses.push(vec![-v(i, h), -v(j, h)]);
}
}
}
let (clauses, cert) = unsat_cert(clauses, 12);
ordeal_lrat::check(&clauses, &cert).expect("checker must accept PHP(4,3)");
}
#[test]
fn random_unsat_certificates_are_accepted() {
let mut s: u64 = 0x1DA7_CE27_0000_0001;
let mut next = move || {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
s
};
let mut unsat_seen = 0;
for _ in 0..60 {
let n_vars = 6 + (next() % 5) as u32; let n_clauses = (n_vars as u64 * 5) as usize;
let clauses: Vec<Vec<i32>> = (0..n_clauses)
.map(|_| {
(0..3)
.map(|_| {
let v = (next() % n_vars as u64) as i32 + 1;
if next() % 2 == 0 { v } else { -v }
})
.collect()
})
.collect();
let formula = CnfFormula {
num_vars: n_vars,
clauses: clauses.clone(),
};
let mut solver = SatSolver::new();
if solver.solve(&formula) == SatResult::Unsat {
unsat_seen += 1;
let cert = emit_lrat(clauses.len(), solver.proof_trace());
ordeal_lrat::check(&clauses, &cert)
.unwrap_or_else(|e| panic!("checker rejected a real certificate: {e:?}"));
}
}
assert!(
unsat_seen >= 10,
"corpus produced too few UNSAT instances: {unsat_seen}"
);
}
#[test]
fn trimmed_certificate_checks_and_shrinks() {
let v = |i: i32, h: i32| 4 * (i - 1) + h;
let mut clauses: Vec<Vec<i32>> = (1..=5)
.map(|i| (1..=4).map(|h| v(i, h)).collect())
.collect();
for h in 1..=4 {
for i in 1..=5 {
for j in (i + 1)..=5 {
clauses.push(vec![-v(i, h), -v(j, h)]);
}
}
}
let formula = CnfFormula {
num_vars: 20,
clauses: clauses.clone(),
};
let mut solver = SatSolver::new();
assert_eq!(solver.solve(&formula), SatResult::Unsat);
let raw = emit_lrat(clauses.len(), solver.proof_trace());
let trimmed = emit_lrat_trimmed(clauses.len(), solver.proof_trace());
ordeal_lrat::check(&clauses, &trimmed).expect("trimmed cert must check");
assert!(
trimmed.len() <= raw.len(),
"trimming must never grow the certificate"
);
assert!(
trimmed.len() < raw.len(),
"PHP(5,4) must trim strictly: raw={} trimmed={}",
raw.len(),
trimmed.len()
);
println!(
"PHP(5,4): raw {} B -> trimmed {} B ({:.0}%)",
raw.len(),
trimmed.len(),
100.0 * trimmed.len() as f64 / raw.len() as f64
);
}
#[test]
fn trimmed_random_corpus_all_check() {
let mut s: u64 = 0x1DA7_CE27_0000_0001;
let mut next = move || {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
s
};
let (mut total_raw, mut total_trimmed, mut n_unsat) = (0usize, 0usize, 0);
for _ in 0..60 {
let n_vars = 6 + (next() % 5) as u32;
let n_clauses = (n_vars as u64 * 5) as usize;
let clauses: Vec<Vec<i32>> = (0..n_clauses)
.map(|_| {
(0..3)
.map(|_| {
let v = (next() % n_vars as u64) as i32 + 1;
if next() % 2 == 0 { v } else { -v }
})
.collect()
})
.collect();
let formula = CnfFormula {
num_vars: n_vars,
clauses: clauses.clone(),
};
let mut solver = SatSolver::new();
if solver.solve(&formula) == SatResult::Unsat {
n_unsat += 1;
let raw = emit_lrat(clauses.len(), solver.proof_trace());
let trimmed = emit_lrat_trimmed(clauses.len(), solver.proof_trace());
ordeal_lrat::check(&clauses, &trimmed).expect("trimmed must check");
assert!(trimmed.len() <= raw.len());
total_raw += raw.len();
total_trimmed += trimmed.len();
}
}
assert!(n_unsat >= 10);
println!(
"random corpus ({n_unsat} UNSAT): raw {total_raw} B -> trimmed {total_trimmed} B ({:.0}%)",
100.0 * total_trimmed as f64 / total_raw as f64
);
}
#[test]
fn mutated_certificates_are_rejected() {
let (clauses, cert) =
unsat_cert(vec![vec![1, 2], vec![-1, 2], vec![1, -2], vec![-1, -2]], 2);
let mut lines: Vec<&str> = cert.lines().collect();
let last = lines.pop().expect("certificate has lines");
let truncated = lines.join("\n");
assert!(ordeal_lrat::check(&clauses, &truncated).is_err());
let mut parts: Vec<&str> = last.split_whitespace().collect();
let second_zero = parts.len() - 1;
let first_zero = parts
.iter()
.position(|&p| p == "0")
.expect("lit terminator");
if second_zero - first_zero > 2 {
parts[first_zero + 1..second_zero].reverse();
let mut mutated_lines = lines.clone();
let mutated_last = parts.join(" ");
mutated_lines.push(&mutated_last);
let mutated = mutated_lines.join("\n");
assert!(
ordeal_lrat::check(&clauses, &mutated).is_err(),
"reordered hints must not verify"
);
}
}
}