use crate::cdcl::Lit;
use crate::dimacs::DimacsCnf;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExpectedVerdict {
Sat,
Unsat,
}
pub fn php(n: usize) -> (DimacsCnf, ExpectedVerdict) {
let holes = n.saturating_sub(1);
let num_vars = n * holes;
let var = |p: usize, h: usize| Lit::pos((p * holes + h) as u32);
let mut clauses: Vec<Vec<Lit>> = Vec::new();
for p in 0..n {
clauses.push((0..holes).map(|h| var(p, h)).collect());
}
for h in 0..holes {
for p in 0..n {
for q in (p + 1)..n {
clauses.push(vec![var(p, h).negated(), var(q, h).negated()]);
}
}
}
(DimacsCnf { num_vars, clauses }, ExpectedVerdict::Unsat)
}
pub fn parity_exactly_one(n: usize) -> (DimacsCnf, ExpectedVerdict) {
assert!(n >= 2, "need at least two selectors");
let x = |i: usize| i as u32; let s = |i: usize| (n + i) as u32; let mut clauses: Vec<Vec<Lit>> = Vec::new();
clauses.push((0..n).map(|i| Lit::pos(x(i))).collect());
for i in 0..n {
for j in (i + 1)..n {
clauses.push(vec![Lit::neg(x(i)), Lit::neg(x(j))]);
}
}
let gadget = |vars: &[u32], out: &mut Vec<Vec<Lit>>| {
let k = vars.len();
for mask in 0u32..(1 << k) {
if mask.count_ones() % 2 == 1 {
out.push((0..k).map(|t| Lit::new(vars[t], (mask >> t) & 1 == 0)).collect());
}
}
};
gadget(&[s(0), x(0)], &mut clauses);
for i in 1..n {
gadget(&[s(i), s(i - 1), x(i)], &mut clauses);
}
clauses.push(vec![Lit::neg(s(n - 1))]);
(DimacsCnf { num_vars: 2 * n, clauses }, ExpectedVerdict::Unsat)
}
pub fn clique_coloring(n: usize, k: usize) -> (DimacsCnf, ExpectedVerdict) {
let num_vars = n * k;
let var = |v: usize, c: usize| Lit::pos((v * k + c) as u32);
let mut clauses: Vec<Vec<Lit>> = Vec::new();
for v in 0..n {
clauses.push((0..k).map(|c| var(v, c)).collect());
}
for u in 0..n {
for w in (u + 1)..n {
for c in 0..k {
clauses.push(vec![var(u, c).negated(), var(w, c).negated()]);
}
}
}
let verdict = if k < n { ExpectedVerdict::Unsat } else { ExpectedVerdict::Sat };
(DimacsCnf { num_vars, clauses }, verdict)
}
pub fn mutilated_chessboard(n: usize) -> (DimacsCnf, ExpectedVerdict) {
assert!(n >= 4 && n % 2 == 0, "the parity argument needs an even board ≥ 4");
let removed = |r: usize, c: usize| (r == 0 && c == 0) || (r == n - 1 && c == n - 1);
let sq = |r: usize, c: usize| r * n + c;
let mut edges: Vec<(usize, usize)> = Vec::new();
for r in 0..n {
for c in 0..n {
if removed(r, c) {
continue;
}
if c + 1 < n && !removed(r, c + 1) {
edges.push((sq(r, c), sq(r, c + 1)));
}
if r + 1 < n && !removed(r + 1, c) {
edges.push((sq(r, c), sq(r + 1, c)));
}
}
}
let num_vars = edges.len();
let mut incident: std::collections::HashMap<usize, Vec<usize>> = std::collections::HashMap::new();
for (e, &(a, b)) in edges.iter().enumerate() {
incident.entry(a).or_default().push(e);
incident.entry(b).or_default().push(e);
}
let mut clauses: Vec<Vec<Lit>> = Vec::new();
for r in 0..n {
for c in 0..n {
if removed(r, c) {
continue;
}
let inc = incident.get(&sq(r, c)).cloned().unwrap_or_default();
clauses.push(inc.iter().map(|&e| Lit::pos(e as u32)).collect()); for i in 0..inc.len() {
for j in (i + 1)..inc.len() {
clauses.push(vec![Lit::pos(inc[i] as u32).negated(), Lit::pos(inc[j] as u32).negated()]); }
}
}
}
(DimacsCnf { num_vars, clauses }, ExpectedVerdict::Unsat)
}
pub fn ordering_principle(n: usize) -> (DimacsCnf, ExpectedVerdict) {
assert!(n >= 2);
let var = |i: usize, j: usize| Lit::pos((i * n + j) as u32);
let num_vars = n * n;
let mut clauses: Vec<Vec<Lit>> = Vec::new();
for i in 0..n {
for j in (i + 1)..n {
clauses.push(vec![var(i, j), var(j, i)]); clauses.push(vec![var(i, j).negated(), var(j, i).negated()]); }
}
for i in 0..n {
for j in 0..n {
for k in 0..n {
if i != j && j != k && i != k {
clauses.push(vec![var(i, j).negated(), var(j, k).negated(), var(i, k)]); }
}
}
}
for i in 0..n {
clauses.push((0..n).filter(|&j| j != i).map(|j| var(i, j)).collect()); }
(DimacsCnf { num_vars, clauses }, ExpectedVerdict::Unsat)
}
pub fn random_3sat(vars: usize, num_clauses: usize, seed: u64) -> DimacsCnf {
let mut state = seed;
let mut next = move || {
state = state.wrapping_add(0x9E3779B97F4A7C15);
let mut z = state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
z ^ (z >> 31)
};
let mut clauses: Vec<Vec<Lit>> = Vec::with_capacity(num_clauses);
while clauses.len() < num_clauses {
let mut vs: Vec<u32> = Vec::with_capacity(3);
while vs.len() < 3 {
let v = (next() as usize % vars) as u32;
if !vs.contains(&v) {
vs.push(v);
}
}
clauses.push(vs.iter().map(|&v| Lit::new(v, next() & 1 == 0)).collect());
}
DimacsCnf { num_vars: vars, clauses }
}
pub fn random_ksat(k: usize, vars: usize, num_clauses: usize, seed: u64) -> DimacsCnf {
let mut state = seed;
let mut next = move || {
state = state.wrapping_add(0x9E3779B97F4A7C15);
let mut z = state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
z ^ (z >> 31)
};
let mut clauses: Vec<Vec<Lit>> = Vec::with_capacity(num_clauses);
while clauses.len() < num_clauses {
let mut vs: Vec<u32> = Vec::with_capacity(k);
while vs.len() < k {
let v = (next() as usize % vars) as u32;
if !vs.contains(&v) {
vs.push(v);
}
}
clauses.push(vs.iter().map(|&v| Lit::new(v, next() & 1 == 0)).collect());
}
DimacsCnf { num_vars: vars, clauses }
}
fn xor_clauses(vs: &[u32], rhs: bool) -> Vec<Vec<Lit>> {
let mut out = Vec::new();
for mask in 0u32..(1 << vs.len()) {
let odd = mask.count_ones() % 2 == 1;
if odd != rhs {
out.push(vs.iter().enumerate().map(|(i, &v)| Lit::new(v, (mask >> i) & 1 == 0)).collect());
}
}
out
}
fn gf2_absorb(basis: &mut std::collections::HashMap<u32, Vec<u32>>, vars: &[u32]) -> bool {
let mut row: std::collections::BTreeSet<u32> = vars.iter().copied().collect();
while let Some(&pivot) = row.iter().next() {
match basis.get(&pivot) {
Some(b) => {
for &v in b {
if !row.remove(&v) {
row.insert(v); }
}
}
None => {
basis.insert(pivot, row.iter().copied().collect());
return true;
}
}
}
false
}
pub fn random_kxor(k: usize, n: usize, m: usize, seed: u64) -> (Vec<crate::xorsat::XorEquation>, DimacsCnf) {
assert!((1..=n).contains(&k) && m >= 1, "need 1 ≤ k ≤ n and m ≥ 1");
let mut state = seed;
let mut next = move || {
state = state.wrapping_add(0x9E3779B97F4A7C15);
let mut z = state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
z ^ (z >> 31)
};
let planted: Vec<bool> = (0..n).map(|_| next() & 1 == 0).collect();
let mut draw = || {
let mut vs: Vec<u32> = Vec::with_capacity(k);
while vs.len() < k {
let v = (next() as usize % n) as u32;
if !vs.contains(&v) {
vs.push(v);
}
}
vs
};
let consistent_rhs = |vs: &[u32]| vs.iter().fold(false, |a, &v| a ^ planted[v as usize]);
let target_rank = if k % 2 == 1 { n } else { n.saturating_sub(1) };
let cap = 8 * n + 64; let mut basis: std::collections::HashMap<u32, Vec<u32>> = std::collections::HashMap::new();
let mut rows: Vec<(Vec<u32>, bool)> = Vec::new();
let mut rank = 0usize;
while (rank < target_rank || rows.len() < m) && rows.len() < cap {
let vs = draw();
if gf2_absorb(&mut basis, &vs) {
rank += 1;
}
let rhs = consistent_rhs(&vs);
rows.push((vs, rhs));
}
let vs = draw();
let rhs = !consistent_rhs(&vs);
rows.push((vs, rhs));
let mut eqs = Vec::new();
let mut clauses = Vec::new();
for (vs, rhs) in &rows {
eqs.push(crate::xorsat::XorEquation::new(vs.iter().map(|&v| v as usize).collect::<Vec<_>>(), *rhs));
clauses.extend(xor_clauses(vs, *rhs));
}
(eqs, DimacsCnf { num_vars: n, clauses })
}
pub fn parity_unsat(n: usize, m: usize, seed: u64) -> (Vec<crate::xorsat::XorEquation>, DimacsCnf) {
random_kxor(3, n, m, seed)
}
fn random_3regular(n: usize, seed: u64) -> Vec<(usize, usize)> {
let mut state = seed ^ 0x9E3779B97F4A7C15;
let mut next = move || {
state = state.wrapping_add(0x9E3779B97F4A7C15);
let mut z = state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
z ^ (z >> 31)
};
for _ in 0..4000 {
let mut stubs: Vec<usize> = (0..n).flat_map(|v| [v, v, v]).collect();
for i in (1..stubs.len()).rev() {
let j = (next() as usize) % (i + 1);
stubs.swap(i, j);
}
let mut edges = Vec::new();
let mut seen = std::collections::HashSet::new();
let mut ok = true;
for c in stubs.chunks(2) {
let (a, b) = (c[0].min(c[1]), c[0].max(c[1]));
if a == b || !seen.insert((a, b)) {
ok = false;
break;
}
edges.push((a, b));
}
if ok {
return edges;
}
}
panic!("could not build a simple 3-regular graph on {n} vertices");
}
pub fn tseitin_expander(n: usize, seed: u64) -> (Vec<crate::xorsat::XorEquation>, DimacsCnf, ExpectedVerdict) {
assert!(n % 2 == 0 && n >= 4, "a 3-regular graph needs an even vertex count ≥ 4");
let edges = random_3regular(n, seed);
let m = edges.len();
let mut incident: Vec<Vec<usize>> = vec![Vec::new(); n];
for (e, &(a, b)) in edges.iter().enumerate() {
incident[a].push(e);
incident[b].push(e);
}
let mut eqs = Vec::new();
let mut clauses = Vec::new();
for v in 0..n {
let inc = &incident[v];
let r = v == 0; eqs.push(crate::xorsat::XorEquation::new(inc.clone(), r));
let d = inc.len();
for mask in 0u32..(1u32 << d) {
if ((mask.count_ones() % 2) == 1) != r {
clauses.push((0..d).map(|i| Lit::new(inc[i] as u32, (mask >> i) & 1 == 0)).collect());
}
}
}
(eqs, DimacsCnf { num_vars: m, clauses }, ExpectedVerdict::Unsat)
}
pub fn grid_tseitin(w: usize, n: usize) -> (Vec<crate::xorsat::XorEquation>, DimacsCnf, ExpectedVerdict) {
assert!(w >= 2 && n >= 2, "a grid needs both dimensions ≥ 2");
let vid = |i: usize, j: usize| i * n + j;
let mut incident: Vec<Vec<usize>> = vec![Vec::new(); w * n];
let mut num_edges = 0usize;
for i in 0..w {
for j in 0..n {
let v = vid(i, j);
if j + 1 < n {
let e = num_edges;
num_edges += 1;
incident[v].push(e);
incident[vid(i, j + 1)].push(e);
}
if i + 1 < w {
let e = num_edges;
num_edges += 1;
incident[v].push(e);
incident[vid(i + 1, j)].push(e);
}
}
}
let mut eqs = Vec::new();
let mut clauses = Vec::new();
for v in 0..(w * n) {
let inc = &incident[v];
let r = v == 0; eqs.push(crate::xorsat::XorEquation::new(inc.clone(), r));
let d = inc.len();
for mask in 0u32..(1u32 << d) {
if ((mask.count_ones() % 2) == 1) != r {
clauses.push((0..d).map(|i| Lit::new(inc[i] as u32, (mask >> i) & 1 == 0)).collect());
}
}
}
(eqs, DimacsCnf { num_vars: num_edges, clauses }, ExpectedVerdict::Unsat)
}
pub fn mod_p_tseitin_expander(
n: usize,
p: u64,
seed: u64,
) -> (Vec<crate::modp::ModpEquation>, DimacsCnf, ExpectedVerdict) {
assert!(p >= 3, "the mod-p obstruction needs an odd prime (p=2 is the parity case, consistent here)");
mod_tseitin_expander_core(n, p, seed)
}
pub fn mod_m_tseitin_expander(
n: usize,
m: u64,
seed: u64,
) -> (Vec<crate::modp::ModpEquation>, DimacsCnf, ExpectedVerdict) {
assert!(m >= 3, "the composite obstruction needs a modulus ≥ 3 (m=2 is the parity case, consistent here)");
mod_tseitin_expander_core(n, m, seed)
}
fn mod_tseitin_expander_core(
n: usize,
modulus: u64,
seed: u64,
) -> (Vec<crate::modp::ModpEquation>, DimacsCnf, ExpectedVerdict) {
use crate::cdcl::Lit;
use crate::modp::ModpEquation;
assert!(n % 2 == 0 && n >= 4, "a 3-regular graph needs an even vertex count ≥ 4");
let edges = random_3regular(n, seed); let ne = edges.len();
let charge = |v: usize| u64::from(v == 0 || v == 1);
let mut eqs: Vec<ModpEquation> = Vec::new();
for v in 0..n {
let mut coeffs: Vec<(usize, u64)> = Vec::new();
for (e, &(a, b)) in edges.iter().enumerate() {
if a == v {
coeffs.push((e, 1));
} else if b == v {
coeffs.push((e, modulus - 1));
}
}
eqs.push(ModpEquation::new(coeffs, charge(v)));
}
let bvar = |e: usize, val: u64| (e * modulus as usize + val as usize) as u32;
let mut clauses: Vec<Vec<Lit>> = Vec::new();
for e in 0..ne {
clauses.push((0..modulus).map(|val| Lit::pos(bvar(e, val))).collect()); for v1 in 0..modulus {
for v2 in (v1 + 1)..modulus {
clauses.push(vec![Lit::neg(bvar(e, v1)), Lit::neg(bvar(e, v2))]); }
}
}
for v in 0..n {
let incident: Vec<(usize, i64)> = edges
.iter()
.enumerate()
.filter_map(|(e, &(a, b))| {
if a == v {
Some((e, 1i64))
} else if b == v {
Some((e, -1i64))
} else {
None
}
})
.collect();
let d = incident.len();
let want = charge(v) as i64;
let pi = modulus as i64;
for idx in 0..modulus.pow(d as u32) {
let mut x = idx;
let mut combo = vec![0u64; d];
for slot in combo.iter_mut() {
*slot = x % modulus;
x /= modulus;
}
let s = incident.iter().zip(&combo).fold(0i64, |acc, (&(_, sign), &val)| acc + sign * val as i64);
if (s.rem_euclid(pi)) != (want.rem_euclid(pi)) {
clauses.push(
incident.iter().zip(&combo).map(|(&(e, _), &val)| Lit::neg(bvar(e, val))).collect(),
);
}
}
}
(eqs, DimacsCnf { num_vars: ne * modulus as usize, clauses }, ExpectedVerdict::Unsat)
}
pub fn mod_p_consistent_onehot(
n: usize,
p: u64,
seed: u64,
) -> (Vec<crate::modp::ModpEquation>, DimacsCnf, ExpectedVerdict) {
use crate::cdcl::Lit;
use crate::modp::ModpEquation;
assert!(n % 2 == 0 && n >= 4, "a 3-regular graph needs an even vertex count ≥ 4");
assert!(p >= 3, "the mod-p one-hot encoding needs an odd prime");
let edges = random_3regular(n, seed);
let ne = edges.len();
let charge = |v: usize| -> u64 {
match v {
0 => 1,
1 => p - 1,
_ => 0,
}
};
let mut eqs: Vec<ModpEquation> = Vec::new();
for v in 0..n {
let mut coeffs: Vec<(usize, u64)> = Vec::new();
for (e, &(a, b)) in edges.iter().enumerate() {
if a == v {
coeffs.push((e, 1));
} else if b == v {
coeffs.push((e, p - 1));
}
}
eqs.push(ModpEquation::new(coeffs, charge(v)));
}
let bvar = |e: usize, val: u64| (e * p as usize + val as usize) as u32;
let mut clauses: Vec<Vec<Lit>> = Vec::new();
for e in 0..ne {
clauses.push((0..p).map(|val| Lit::pos(bvar(e, val))).collect());
for v1 in 0..p {
for v2 in (v1 + 1)..p {
clauses.push(vec![Lit::neg(bvar(e, v1)), Lit::neg(bvar(e, v2))]);
}
}
}
for v in 0..n {
let incident: Vec<(usize, i64)> = edges
.iter()
.enumerate()
.filter_map(|(e, &(a, b))| {
if a == v {
Some((e, 1i64))
} else if b == v {
Some((e, -1i64))
} else {
None
}
})
.collect();
let d = incident.len();
let want = charge(v) as i64;
let pi = p as i64;
for idx in 0..p.pow(d as u32) {
let mut x = idx;
let mut combo = vec![0u64; d];
for slot in combo.iter_mut() {
*slot = x % p;
x /= p;
}
let s = incident.iter().zip(&combo).fold(0i64, |acc, (&(_, sign), &val)| acc + sign * val as i64);
if (s.rem_euclid(pi)) != (want.rem_euclid(pi)) {
clauses.push(
incident.iter().zip(&combo).map(|(&(e, _), &val)| Lit::neg(bvar(e, val))).collect(),
);
}
}
}
(eqs, DimacsCnf { num_vars: ne * p as usize, clauses }, ExpectedVerdict::Sat)
}
pub fn ksat_threshold_first_moment_upper(k: u32) -> f64 {
let pow = (1u64 << k) as f64;
std::f64::consts::LN_2 / (pow / (pow - 1.0)).ln()
}
fn combinations(n: usize, q: usize) -> Vec<Vec<usize>> {
let mut out = Vec::new();
if q == 0 || q > n {
return out;
}
let mut idx: Vec<usize> = (0..q).collect();
loop {
out.push(idx.clone());
let mut i = q;
loop {
if i == 0 {
return out;
}
i -= 1;
if idx[i] != i + n - q {
break;
}
}
idx[i] += 1;
for j in (i + 1)..q {
idx[j] = idx[j - 1] + 1;
}
}
}
pub fn weak_php(pigeons: usize, holes: usize) -> (DimacsCnf, ExpectedVerdict) {
let num_vars = pigeons * holes;
let var = |p: usize, h: usize| Lit::pos((p * holes + h) as u32);
let mut clauses: Vec<Vec<Lit>> = Vec::new();
for p in 0..pigeons {
clauses.push((0..holes).map(|h| var(p, h)).collect()); }
for h in 0..holes {
for p in 0..pigeons {
for q in (p + 1)..pigeons {
clauses.push(vec![var(p, h).negated(), var(q, h).negated()]); }
}
}
let verdict = if pigeons > holes { ExpectedVerdict::Unsat } else { ExpectedVerdict::Sat };
(DimacsCnf { num_vars, clauses }, verdict)
}
pub fn functional_php(n: usize) -> (DimacsCnf, ExpectedVerdict) {
let holes = n.saturating_sub(1);
let (mut cnf, _) = php(n);
let var = |p: usize, h: usize| Lit::pos((p * holes + h) as u32);
for p in 0..n {
for h1 in 0..holes {
for h2 in (h1 + 1)..holes {
cnf.clauses.push(vec![var(p, h1).negated(), var(p, h2).negated()]); }
}
}
(cnf, ExpectedVerdict::Unsat)
}
pub fn onto_php(n: usize) -> (DimacsCnf, ExpectedVerdict) {
let holes = n.saturating_sub(1);
let (mut cnf, _) = functional_php(n);
let var = |p: usize, h: usize| Lit::pos((p * holes + h) as u32);
for h in 0..holes {
cnf.clauses.push((0..n).map(|p| var(p, h)).collect()); }
(cnf, ExpectedVerdict::Unsat)
}
pub fn mod_counting(n: usize, q: usize) -> (DimacsCnf, ExpectedVerdict) {
assert!(q >= 2 && n >= q, "need a block size q ≥ 2 with n ≥ q");
let edges = combinations(n, q);
let num_vars = edges.len();
let mut incident: Vec<Vec<usize>> = vec![Vec::new(); n];
for (e, edge) in edges.iter().enumerate() {
for &v in edge {
incident[v].push(e);
}
}
let mut clauses: Vec<Vec<Lit>> = Vec::new();
for inc in &incident {
clauses.push(inc.iter().map(|&e| Lit::pos(e as u32)).collect()); }
for e in 0..edges.len() {
for f in (e + 1)..edges.len() {
if edges[e].iter().any(|v| edges[f].contains(v)) {
clauses.push(vec![Lit::neg(e as u32), Lit::neg(f as u32)]); }
}
}
let verdict = if n % q == 0 { ExpectedVerdict::Sat } else { ExpectedVerdict::Unsat };
(DimacsCnf { num_vars, clauses }, verdict)
}
pub fn mod_counting_groups(n: usize, q: usize) -> Vec<Vec<u32>> {
let (cnf, _) = mod_counting(n, q);
cnf.clauses
.iter()
.filter(|c| c.iter().all(|l| l.is_positive()))
.map(|c| c.iter().map(|l| l.var()).collect())
.collect()
}
pub fn mod_counting_edges(n: usize, q: usize) -> Vec<Vec<usize>> {
combinations(n, q)
}
pub fn ramsey_number(s: usize, t: usize) -> Option<usize> {
let (a, b) = (s.min(t), s.max(t));
match (a, b) {
(1, _) => Some(1),
(2, b) => Some(b),
(3, 3) => Some(6),
(3, 4) => Some(9),
(3, 5) => Some(14),
(3, 6) => Some(18),
(3, 7) => Some(23),
(3, 8) => Some(28),
(3, 9) => Some(36),
(4, 4) => Some(18),
(4, 5) => Some(25),
_ => None,
}
}
pub fn ramsey(s: usize, t: usize, n: usize) -> (DimacsCnf, ExpectedVerdict) {
assert!(s >= 2 && t >= 2 && n >= 2);
let r = ramsey_number(s, t).expect("Ramsey number R(s,t) must be known to pin the verdict");
let mut edge_id = std::collections::HashMap::new();
for (e, pair) in combinations(n, 2).iter().enumerate() {
edge_id.insert((pair[0], pair[1]), e as u32);
}
let num_vars = edge_id.len();
let mut clauses: Vec<Vec<Lit>> = Vec::new();
let clique_edges = |clique: &[usize]| -> Vec<u32> {
combinations(clique.len(), 2).iter().map(|p| edge_id[&(clique[p[0]], clique[p[1]])]).collect()
};
for clique in combinations(n, s) {
clauses.push(clique_edges(&clique).into_iter().map(Lit::neg).collect()); }
for clique in combinations(n, t) {
clauses.push(clique_edges(&clique).into_iter().map(Lit::pos).collect()); }
let verdict = if n >= r { ExpectedVerdict::Unsat } else { ExpectedVerdict::Sat };
(DimacsCnf { num_vars, clauses }, verdict)
}
pub fn pebbling_pyramid(height: usize) -> (DimacsCnf, ExpectedVerdict) {
let node = |r: usize, i: usize| (r * (r + 1) / 2 + i) as u32; let num_vars = (height + 1) * (height + 2) / 2;
let mut clauses: Vec<Vec<Lit>> = Vec::new();
for i in 0..=height {
clauses.push(vec![Lit::pos(node(height, i))]); }
for r in 0..height {
for i in 0..=r {
clauses.push(vec![
Lit::neg(node(r + 1, i)),
Lit::neg(node(r + 1, i + 1)),
Lit::pos(node(r, i)),
]); }
}
clauses.push(vec![Lit::neg(node(0, 0))]); (DimacsCnf { num_vars, clauses }, ExpectedVerdict::Unsat)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cdcl::SolveResult;
#[test]
fn the_ksat_threshold_sequence_first_moment_upper_bound() {
let ln2 = std::f64::consts::LN_2;
let ub = ksat_threshold_first_moment_upper;
let known = [(2u32, 2.40942), (3, 5.19089), (4, 10.73970), (5, 21.83239)];
for (k, want) in known {
assert!((ub(k) - want).abs() < 5e-3, "α*({k}) = {} want {want}", ub(k));
}
for k in 2..=20 {
assert!(ub(k + 1) > ub(k), "increasing at k={k}");
}
for k in 4..=20 {
let ratio = ub(k + 1) / ub(k);
assert!((1.9..=2.1).contains(&ratio), "ratio ≈ 2 at k={k}: {ratio}");
}
for k in 2..=20 {
let lead = (1u64 << k) as f64 * ln2;
assert!(ub(k) < lead, "below the leading term 2ᵏln2 at k={k}");
assert!((ub(k) - (lead - ln2 / 2.0)).abs() < 0.05, "→ 2ᵏln2 − ½ln2 at k={k}");
}
for k in 2..=20 {
let p = 1.0 - 2f64.powi(-(k as i32));
let base = |a: f64| 2.0 * p.powf(a);
assert!((base(ub(k)) - 1.0).abs() < 1e-9, "α*(k={k}) is exactly base = 1");
assert!(base(ub(k) + 0.5) < 1.0, "above α*(k={k}): E[X] → 0");
assert!(base(ub(k) - 0.5) > 1.0, "below α*(k={k}): E[X] → ∞");
}
for (k, sharp) in [(2u32, 1.0), (3, 4.267), (4, 9.931), (5, 21.117)] {
assert!(ub(k) > sharp, "first-moment bound α*({k})={} exceeds the true threshold {sharp}", ub(k));
}
}
#[test]
fn reciprocal_threshold_sums_telescope_to_euler_and_gf2_constants() {
let ln2 = std::f64::consts::LN_2;
let recip = |k: u32| 1.0 / ksat_threshold_first_moment_upper(k);
let (mut s, mut p, mut alt, mut palt) = (0.0f64, 1.0f64, 0.0f64, 1.0f64);
for k in 1..=50u32 {
let term = 1.0 - 2f64.powi(-(k as i32));
s += recip(k);
p *= term;
assert!((s + p.ln() / ln2).abs() < 1e-9, "Σ1/α* telescopes to −log₂Π(1−2⁻ᵏ) at k={k}");
let sign = if k % 2 == 1 { 1.0 } else { -1.0 };
alt += sign * recip(k);
palt *= term.powf(if k % 2 == 1 { -1.0 } else { 1.0 });
assert!((alt - palt.ln() / ln2).abs() < 1e-9, "alternating sum telescopes to log₂Π(1−2⁻ᵏ)^((−1)ᵏ) at k={k}");
}
let phi_half = 0.288_788_095_1;
assert!((p - phi_half).abs() < 1e-9, "Π(1−2⁻ᵏ) → φ(½) = the GF(2) invertibility constant 0.28879");
assert!((s - 1.791_916_824_7).abs() < 1e-6, "Σ 1/α*_k ≈ 1.79192");
assert!((s + phi_half.ln() / ln2).abs() < 1e-6, "and it equals −log₂ φ(½) exactly");
assert!((alt - 0.715_131_251_2).abs() < 1e-6, "Σ(−1)ᵏ⁺¹/α*_k ≈ 0.71513");
}
#[test]
fn mutilated_chessboard_and_ordering_are_correctly_unsat() {
for n in [4, 6, 8] {
let (cnf, v) = mutilated_chessboard(n);
assert_eq!(v, ExpectedVerdict::Unsat);
assert!(cnf.clauses.iter().all(|c| !c.is_empty()), "chessboard {n} has no empty clause");
let solved = crate::solve::solve_structured(cnf.num_vars, &cnf.clauses);
assert!(matches!(solved.answer, crate::solve::Answer::Unsat), "mutilated chessboard {n} must be UNSAT");
}
for n in [3, 4, 5, 6] {
let (cnf, v) = ordering_principle(n);
assert_eq!(v, ExpectedVerdict::Unsat);
let solved = crate::solve::solve_structured(cnf.num_vars, &cnf.clauses);
assert!(matches!(solved.answer, crate::solve::Answer::Unsat), "ordering principle GT({n}) must be UNSAT");
}
}
#[test]
fn tseitin_expander_is_unsat_and_gaussian_refutes_it() {
for seed in [1u64, 7, 42] {
let (eqs, cnf, verdict) = tseitin_expander(10, seed);
assert_eq!(verdict, ExpectedVerdict::Unsat);
match crate::xorsat::solve(&eqs, cnf.num_vars) {
crate::xorsat::XorOutcome::Unsat(refutation) => {
assert!(
crate::xorsat::is_refutation(&eqs, cnf.num_vars, &refutation),
"the Gaussian refutation must independently check"
);
}
crate::xorsat::XorOutcome::Sat(_) => panic!("expander-Tseitin must be UNSAT"),
}
assert_eq!(cnf.into_solver().solve(), SolveResult::Unsat, "CNF encoding must be UNSAT too");
}
}
#[test]
fn mod_p_tseitin_is_refuted_over_gf_p_with_a_checkable_certificate() {
for &p in &[3u64, 5, 7] {
for seed in [1u64, 7] {
let (eqs, cnf, verdict) = mod_p_tseitin_expander(6, p, seed);
assert_eq!(verdict, ExpectedVerdict::Unsat);
let edges = cnf.num_vars / p as usize; match crate::modp::solve(&eqs, edges, p) {
crate::modp::ModpOutcome::Unsat(combo) => assert!(
crate::modp::is_refutation(&eqs, edges, p, &combo),
"the GF({p}) refutation must independently re-check"
),
crate::modp::ModpOutcome::Sat(_) => panic!("mod-{p} obstruction must be UNSAT over GF({p})"),
}
assert_eq!(cnf.into_solver().solve(), SolveResult::Unsat, "the Boolean CNF must be UNSAT");
}
}
}
#[test]
fn mod_p_obstruction_is_invisible_to_gf2() {
let p = 3u64;
let (eqs, cnf, _) = mod_p_tseitin_expander(6, p, 7);
let edges = cnf.num_vars / p as usize;
let gf2: Vec<crate::xorsat::XorEquation> = eqs
.iter()
.map(|eq| {
let vars: Vec<usize> = eq.coeffs.iter().map(|&(v, _)| v).collect();
crate::xorsat::XorEquation::new(vars, eq.rhs % 2 == 1)
})
.collect();
assert!(
matches!(crate::xorsat::solve(&gf2, edges), crate::xorsat::XorOutcome::Sat(_)),
"over GF(2) the even-charge obstruction is satisfiable — the parity engine is blind to it"
);
}
#[test]
fn mod_m_tseitin_is_refuted_over_the_ring_with_a_checkable_certificate() {
for &m in &[6u64, 15] {
for seed in [1u64, 7] {
let (eqs, cnf, verdict) = mod_m_tseitin_expander(6, m, seed);
assert_eq!(verdict, ExpectedVerdict::Unsat);
let vars = cnf.num_vars / m as usize; match crate::modm::solve(&eqs, vars, m) {
Some(crate::modm::ModmOutcome::Unsat { modulus, combo }) => assert!(
crate::modm::is_refutation(&eqs, vars, modulus, &combo),
"the ℤ/{m} refutation must independently re-check (via its GF({modulus}) factor)"
),
other => panic!("mod-{m} obstruction must be UNSAT over ℤ/{m}, got {other:?}"),
}
assert_eq!(cnf.into_solver().solve(), SolveResult::Unsat, "the Boolean CNF must be UNSAT");
}
}
}
#[test]
#[ignore = "heavy: CDCL on Tseitin is exponential — that's the wall. Charts it vs the Gaussian collapse."]
fn tseitin_parity_wall_collapses_under_gaussian() {
use crate::xorsat::{is_refutation, solve, XorOutcome};
let seed = 42u64;
let mut rows = vec![
" n | edges | CDCL: conflicts / time | Gaussian | certified".to_string(),
"----+-------+--------------------------+--------------+----------".to_string(),
];
for n in [16usize, 24, 32, 40, 48, 56] {
let (eqs, cnf, verdict) = tseitin_expander(n, seed);
assert_eq!(verdict, ExpectedVerdict::Unsat);
let mut solver = cnf.into_solver();
let ct = std::time::Instant::now();
assert_eq!(solver.solve(), SolveResult::Unsat, "Tseitin CNF is UNSAT");
let cdcl_time = ct.elapsed();
let conflicts = solver.conflicts();
let t = std::time::Instant::now();
let out = solve(&eqs, cnf.num_vars);
let gauss = t.elapsed();
let certified = match &out {
XorOutcome::Unsat(r) => is_refutation(&eqs, cnf.num_vars, r),
XorOutcome::Sat(_) => false,
};
assert!(certified, "Gaussian gives a checkable refutation for n={n}");
rows.push(format!("{n:3} | {:5} | {conflicts:10} {cdcl_time:>10?} | {gauss:>11?} | yes", cnf.num_vars));
}
let chart = rows.join("\n");
eprintln!("\nTSEITIN PARITY WALL — resolution (CDCL) exponential vs Gaussian polynomial+certified\n{chart}\n");
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../logs/derived_facts");
if std::fs::create_dir_all(&dir).is_ok() {
let _ = std::fs::write(dir.join("tseitin_parity_wall.txt"), format!("TSEITIN PARITY WALL — CDCL (resolution, 2^Ω(n)) vs Gaussian (poly, certified)\n\n{chart}\n"));
}
}
#[test]
fn the_two_geometries_of_hardness() {
let half_point_satisfies = |cnf: &DimacsCnf| cnf.clauses.iter().all(|c| c.len() >= 2);
let (php_cnf, _) = php(8);
let (eqs, tse_cnf, _) = tseitin_expander(12, 7);
assert!(half_point_satisfies(&php_cnf), "x≡½ ∈ the PHP clause-polytope");
assert!(half_point_satisfies(&tse_cnf), "x≡½ ∈ the Tseitin clause-polytope");
assert!(crate::pigeonhole::certify_pigeonhole_unsat(8, 7).is_some(), "convex/Farkas counting hyperplane refutes PHP(8)");
match crate::xorsat::solve(&eqs, tse_cnf.num_vars) {
crate::xorsat::XorOutcome::Unsat(r) => assert!(crate::xorsat::is_refutation(&eqs, tse_cnf.num_vars, &r), "GF(2) geometry refutes parity, certified"),
crate::xorsat::XorOutcome::Sat(_) => panic!("Tseitin must be UNSAT"),
}
}
#[test]
fn counting_is_provably_blind_two_covers_same_counts_opposite_answers() {
use crate::xorsat::{solve, XorEquation, XorOutcome};
let build = |edges: &[(usize, usize)], n: usize, charges: &[bool]| -> (Vec<XorEquation>, DimacsCnf) {
let mut incident = vec![Vec::new(); n];
for (e, &(a, b)) in edges.iter().enumerate() {
incident[a].push(e);
incident[b].push(e);
}
let (mut eqs, mut clauses) = (Vec::new(), Vec::new());
for v in 0..n {
let (inc, r, d) = (&incident[v], charges[v], incident[v].len());
eqs.push(XorEquation::new(inc.clone(), r));
for mask in 0u32..(1u32 << d) {
if ((mask.count_ones() % 2) == 1) != r {
clauses.push((0..d).map(|i| Lit::new(inc[i] as u32, (mask >> i) & 1 == 0)).collect());
}
}
}
(eqs, DimacsCnf { num_vars: edges.len(), clauses })
};
let edges = super::random_3regular(12, 7);
let (eqs_sat, cnf_sat) = build(&edges, 12, &vec![false; 12]); let mut odd = vec![false; 12];
odd[0] = true; let (eqs_uns, cnf_uns) = build(&edges, 12, &odd);
let profile = |cnf: &DimacsCnf| {
let mut lens: Vec<usize> = cnf.clauses.iter().map(|c| c.len()).collect();
lens.sort_unstable();
let mut occ = vec![0usize; cnf.num_vars];
for c in &cnf.clauses {
for l in c {
occ[l.var() as usize] += 1;
}
}
occ.sort_unstable();
(cnf.clauses.len(), lens, occ)
};
assert_eq!(profile(&cnf_sat), profile(&cnf_uns), "IDENTICAL counting profile — counting cannot tell them apart");
assert!(matches!(solve(&eqs_sat, cnf_sat.num_vars), XorOutcome::Sat(_)), "even total parity ⇒ SAT");
match solve(&eqs_uns, cnf_uns.num_vars) {
XorOutcome::Unsat(r) => assert!(crate::xorsat::is_refutation(&eqs_uns, cnf_uns.num_vars, &r), "odd total parity ⇒ UNSAT, certified by GF(2)"),
XorOutcome::Sat(_) => panic!("odd-parity Tseitin must be UNSAT"),
}
}
#[test]
fn php_has_the_expected_shape() {
let (cnf, verdict) = php(4);
assert_eq!(verdict, ExpectedVerdict::Unsat);
assert_eq!(cnf.num_vars, 4 * 3);
assert_eq!(cnf.clauses.len(), 4 + 3 * 6);
}
#[test]
fn php_is_unsatisfiable_for_small_n() {
for n in 1..=5 {
let (cnf, _) = php(n);
assert_eq!(
cnf.into_solver().solve(),
SolveResult::Unsat,
"PHP({n}) must be unsatisfiable"
);
}
}
#[test]
#[ignore = "core benchmark on hard random 3-SAT — the general-instance (non-symmetric) baseline"]
fn bench_core_on_random_3sat() {
use std::time::Instant;
for &(vars, ratio) in &[(50usize, 4.26), (75, 4.26), (100, 4.26), (125, 4.26)] {
let nc = (vars as f64 * ratio) as usize;
let cnf = random_3sat(vars, nc, 0xC0FFEE_1234);
let mut s = cnf.into_solver();
let t = Instant::now();
let res = s.solve();
let ms = t.elapsed().as_secs_f64() * 1e3;
println!(
"rand3sat(v={vars}, c={nc}): {res:?} in {ms:.1}ms — {} conflicts, {} learned clauses (unbounded!)",
s.conflicts(),
s.learned().len()
);
}
}
#[test]
#[ignore = "A/B benchmark: LBD clause deletion ON vs OFF on hard random 3-SAT"]
fn bench_lbd_reduction_ab() {
use std::time::Instant;
for &v in &[140usize, 160, 180, 200] {
let nc = (v as f64 * 4.26) as usize;
let cnf = random_3sat(v, nc, 0xBADC0DE_99);
for on in [false, true] {
let mut s = cnf.into_solver();
s.set_reduce(on);
let t = Instant::now();
let res = s.solve();
let ms = t.elapsed().as_secs_f64() * 1e3;
println!(
"v={v} reduce={on:5}: {} in {ms:7.0}ms — {} conflicts, {} LIVE learned clauses",
if matches!(res, SolveResult::Sat(_)) { "SAT " } else { "UNSAT" },
s.conflicts(),
s.live_learned()
);
}
}
}
#[test]
#[ignore = "parity grave-dance: dump expander-XOR CNF + time Gaussian (xorsat), pairs with Kissat loop"]
fn dump_parity_and_time_xorsat() {
use std::time::Instant;
for n in [40usize, 60, 80, 100, 120] {
let m = (n as f64 * 1.1) as usize;
let (eqs, cnf) = parity_unsat(n, m, 0x9A2173_5C);
std::fs::write(format!("/tmp/parity_{n}.cnf"), crate::dimacs::print(&cnf)).unwrap();
let t = Instant::now();
let outcome = crate::xorsat::solve(&eqs, n);
let us = t.elapsed().as_secs_f64() * 1e6;
assert!(matches!(outcome, crate::xorsat::XorOutcome::Unsat(_)), "parity(n={n}) must be UNSAT");
println!(
"XORSAT parity(n={n}, m={m}): UNSAT in {us:.1}µs via Gaussian elimination | {} CNF clauses dumped for Kissat",
cnf.clauses.len()
);
}
}
#[test]
fn parity_unsat_is_genuinely_unsat() {
let (eqs, cnf) = parity_unsat(12, 14, 0x9A2173_5C);
assert!(matches!(crate::xorsat::solve(&eqs, 12), crate::xorsat::XorOutcome::Unsat(_)));
assert_eq!(cnf.into_solver().solve(), SolveResult::Unsat, "CNF encoding is UNSAT too");
}
#[test]
fn clique_coloring_verdict_tracks_colors_vs_clique() {
for n in 2..=4 {
let (unsat, v_unsat) = clique_coloring(n, n - 1);
assert_eq!(v_unsat, ExpectedVerdict::Unsat);
assert_eq!(unsat.into_solver().solve(), SolveResult::Unsat, "K_{n} with {} colors", n - 1);
let (sat, v_sat) = clique_coloring(n, n);
assert_eq!(v_sat, ExpectedVerdict::Sat);
assert!(matches!(sat.into_solver().solve(), SolveResult::Sat(_)), "K_{n} with {n} colors");
}
}
#[test]
fn clique_coloring_exposes_color_permutation_symmetry() {
let (cnf, _) = clique_coloring(3, 2);
let gens = crate::symmetry_detect::find_generators(cnf.num_vars, &cnf.clauses);
assert!(!gens.iter().all(|g| g.is_identity()), "color symmetry must be detected");
for g in &gens {
assert!(crate::symmetry_detect::perm_is_automorphism(&cnf.clauses, g));
}
}
#[test]
fn clique_coloring_is_refuted_with_certified_symmetry_breaking() {
for (n, k) in [(3usize, 2usize), (4, 3)] {
let (cnf, _) = clique_coloring(n, k);
let r = crate::sym_certify::certified_unsat_auto(cnf.num_vars, &cnf.clauses);
assert!(r.refuted, "K_{n} / {k} colors refuted");
assert!(r.sbp_clauses >= 1, "color symmetry certified-broken");
assert!(crate::pr::check_pr_refutation(cnf.num_vars, &cnf.clauses, &r.steps));
}
}
fn decides(cnf: &DimacsCnf, verdict: ExpectedVerdict) {
let solved = crate::solve::solve_structured(cnf.num_vars, &cnf.clauses);
match verdict {
ExpectedVerdict::Unsat => {
assert!(matches!(solved.answer, crate::solve::Answer::Unsat), "expected UNSAT, solver said SAT");
}
ExpectedVerdict::Sat => match &solved.answer {
crate::solve::Answer::Sat(model) => assert!(
cnf.clauses.iter().all(|c| c.iter().any(|l| model[l.var() as usize] == l.is_positive())),
"the returned SAT model must satisfy every clause"
),
crate::solve::Answer::Unsat => panic!("expected SAT, solver said UNSAT"),
},
}
}
#[test]
fn weak_php_generalizes_php_and_tracks_the_pigeon_hole_ratio() {
for n in 1..=5 {
let (tight, tv) = weak_php(n, n.saturating_sub(1));
let (base, bv) = php(n);
assert_eq!(tight.num_vars, base.num_vars, "weak_php(n,n-1) shares PHP's variable count");
assert_eq!(tight.clauses, base.clauses, "weak_php(n,n-1) is byte-identical to php(n)");
assert_eq!(tv, bv);
}
for &(p, h) in &[(3usize, 2usize), (5, 4), (4, 7), (2, 3), (3, 3), (6, 2)] {
let (cnf, v) = weak_php(p, h);
assert_eq!(v, if p > h { ExpectedVerdict::Unsat } else { ExpectedVerdict::Sat }, "PHP^{h}_{p} verdict");
decides(&cnf, v);
}
}
#[test]
fn functional_and_onto_php_are_unsat_strengthenings_of_php() {
for n in 2..=5 {
let (base, _) = php(n);
let (func, fv) = functional_php(n);
let (onto, ov) = onto_php(n);
assert_eq!(fv, ExpectedVerdict::Unsat);
assert_eq!(ov, ExpectedVerdict::Unsat);
assert!(func.clauses.starts_with(&base.clauses), "FPHP({n}) ⊇ PHP({n})");
assert!(onto.clauses.starts_with(&func.clauses), "onto-FPHP({n}) ⊇ FPHP({n})");
decides(&func, ExpectedVerdict::Unsat);
decides(&onto, ExpectedVerdict::Unsat);
}
for n in 3..=5 {
let (base, _) = php(n);
let (func, _) = functional_php(n);
let (onto, _) = onto_php(n);
assert!(func.clauses.len() > base.clauses.len(), "FPHP adds functional clauses at n={n}");
assert!(onto.clauses.len() > func.clauses.len(), "onto-FPHP adds surjectivity clauses at n={n}");
}
}
#[test]
fn mod_counting_is_unsat_exactly_when_q_does_not_divide_n() {
for &(n, q) in &[(3usize, 2usize), (4, 2), (5, 2), (6, 2), (4, 3), (6, 3), (7, 3), (8, 4)] {
let (cnf, v) = mod_counting(n, q);
assert_eq!(v, if n % q == 0 { ExpectedVerdict::Sat } else { ExpectedVerdict::Unsat }, "Count_{q}({n})");
assert!(cnf.clauses.iter().all(|c| !c.is_empty()), "Count_{q}({n}) has no empty clause");
decides(&cnf, v);
}
let (c52, _) = mod_counting(5, 2);
assert_eq!(c52.num_vars, 10, "C(5,2) = 10 edges of K_5");
}
#[test]
fn ramsey_tracks_the_known_ramsey_numbers() {
for &(s, t, n) in &[(3usize, 3usize, 5usize), (3, 3, 6), (3, 4, 8), (3, 4, 9)] {
let (cnf, v) = ramsey(s, t, n);
let r = ramsey_number(s, t).unwrap();
assert_eq!(v, if n >= r { ExpectedVerdict::Unsat } else { ExpectedVerdict::Sat }, "Ramsey({s},{t};{n}) vs R={r}");
decides(&cnf, v);
}
let (cnf, _) = ramsey(3, 3, 6);
let key = |c: &Vec<Lit>, flip: bool| {
let mut k: Vec<(u32, bool)> = c.iter().map(|l| (l.var(), l.is_positive() ^ flip)).collect();
k.sort_unstable();
k
};
let set: std::collections::HashSet<Vec<(u32, bool)>> = cnf.clauses.iter().map(|c| key(c, false)).collect();
for c in &cnf.clauses {
assert!(set.contains(&key(c, true)), "global colour-flip is an automorphism of diagonal Ramsey");
}
}
#[test]
fn random_kxor_is_guaranteed_unsat_for_every_seed_arity_and_size() {
use crate::cdcl::SolveResult;
for k in [2usize, 3, 4, 5] {
for n in [8usize, 12, 16] {
for seed in 0..24u64 {
let (eqs, cnf) = random_kxor(k, n, n, seed.wrapping_mul(0x1000193) ^ k as u64);
assert!(
matches!(crate::xorsat::solve(&eqs, cnf.num_vars), crate::xorsat::XorOutcome::Unsat(_)),
"k={k} n={n} seed={seed}: maximal-rank k-XOR must be UNSAT over GF(2)"
);
assert_eq!(cnf.into_solver().solve(), SolveResult::Unsat, "k={k} n={n} seed={seed}: CNF must be UNSAT");
}
}
}
}
#[test]
fn random_kxor_generalizes_parity_and_gaussian_refutes_every_arity() {
for seed in [1u64, 7, 0x9A2173_5C] {
let (_, a) = parity_unsat(12, 14, seed);
let (_, b) = random_kxor(3, 12, 14, seed);
assert_eq!(a.clauses, b.clauses, "parity_unsat is exactly random_kxor(k=3)");
}
for k in [2usize, 4, 5] {
let (eqs, cnf) = random_kxor(k, 14, 16, 0xC0FFEE ^ k as u64);
match crate::xorsat::solve(&eqs, cnf.num_vars) {
crate::xorsat::XorOutcome::Unsat(r) => assert!(
crate::xorsat::is_refutation(&eqs, cnf.num_vars, &r),
"{k}-XOR Gaussian refutation must re-check"
),
crate::xorsat::XorOutcome::Sat(_) => panic!("planted-then-flipped {k}-XOR must be UNSAT"),
}
assert_eq!(cnf.into_solver().solve(), SolveResult::Unsat, "{k}-XOR CNF encoding is UNSAT");
}
}
#[test]
fn pebbling_pyramid_is_unsat_with_the_expected_triangular_shape() {
for h in 1..=5 {
let (cnf, v) = pebbling_pyramid(h);
assert_eq!(v, ExpectedVerdict::Unsat);
let nodes = (h + 1) * (h + 2) / 2;
assert_eq!(cnf.num_vars, nodes, "pyramid of height {h} has T(h+1) nodes");
assert_eq!(cnf.clauses.len(), nodes + 1, "pebbling({h}) clause count = nodes + 1");
assert!(cnf.clauses.iter().all(|c| !c.is_empty()), "no empty clause");
decides(&cnf, ExpectedVerdict::Unsat);
}
}
}