use crate::cdcl::Lit;
use crate::dimacs::DimacsCnf;
use crate::proof::Perm;
use std::collections::{BTreeSet, HashMap};
pub type Corner = u64;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Subcube {
pub n: usize,
pub care: u64,
pub value: u64,
}
impl Subcube {
pub fn blocker(clause: &[Lit], n: usize) -> Subcube {
let mut care = 0u64;
let mut value = 0u64;
for &lit in clause {
let v = lit.var() as u64;
care |= 1u64 << v;
if !lit.is_positive() {
value |= 1u64 << v;
}
}
Subcube { n, care, value }
}
#[inline]
pub fn covers(&self, corner: Corner) -> bool {
(corner & self.care) == self.value
}
pub fn dimension(&self) -> usize {
self.n - self.care.count_ones() as usize
}
pub fn footprint_card(&self) -> u64 {
1u64 << self.dimension()
}
pub fn clause_literals(&self) -> Vec<(usize, bool)> {
(0..self.n)
.filter(|&v| self.care & (1u64 << v) != 0)
.map(|v| (v, self.value & (1u64 << v) == 0))
.collect()
}
pub fn clause_lp_value(&self, point: &[f64]) -> f64 {
self.clause_literals()
.iter()
.map(|&(v, positive)| if positive { point[v] } else { 1.0 - point[v] })
.sum()
}
pub fn resolve(&self, other: &Subcube) -> Option<(usize, Subcube)> {
let shared = self.care & other.care;
let disagree = shared & (self.value ^ other.value);
if disagree.count_ones() != 1 {
return None;
}
let pivot = disagree.trailing_zeros() as usize;
let care = (self.care | other.care) & !(1u64 << pivot);
let value = (self.value | other.value) & care;
Some((pivot, Subcube { n: self.n, care, value }))
}
pub fn footprint(&self) -> Vec<Corner> {
let free: Vec<u64> = (0..self.n as u64).filter(|i| self.care & (1u64 << i) == 0).collect();
let mut out = Vec::with_capacity(1usize << free.len());
for mask in 0..(1u64 << free.len()) {
let mut c = self.value;
for (j, &i) in free.iter().enumerate() {
if mask & (1u64 << j) != 0 {
c |= 1u64 << i;
}
}
out.push(c);
}
out
}
}
#[derive(Clone, Debug)]
pub struct Cover {
pub n: usize,
pub blockers: Vec<Subcube>,
}
impl Cover {
pub fn of_cnf(cnf: &DimacsCnf) -> Cover {
let n = cnf.num_vars;
let blockers = cnf.clauses.iter().map(|c| Subcube::blocker(c, n)).collect();
Cover { n, blockers }
}
pub fn vertex_energy(&self, corner: Corner) -> usize {
self.blockers.iter().filter(|b| b.covers(corner)).count()
}
pub fn blocks(&self, corner: Corner) -> bool {
self.blockers.iter().any(|b| b.covers(corner))
}
pub fn is_tight(&self, corner: Corner) -> bool {
self.vertex_energy(corner) == 1
}
pub fn is_redundant(&self, corner: Corner) -> bool {
self.vertex_energy(corner) >= 2
}
pub fn essential_blockers(&self) -> Vec<usize> {
(0..self.blockers.len())
.filter(|&i| self.blockers[i].footprint().iter().any(|&c| self.vertex_energy(c) == 1))
.collect()
}
pub fn escaping_corner(&self) -> Option<Corner> {
(0u64..(1u64 << self.n)).find(|&c| !self.blocks(c))
}
pub fn is_total(&self) -> bool {
self.escaping_corner().is_none()
}
pub fn solution_count(&self) -> u64 {
(0u64..(1u64 << self.n)).filter(|&c| !self.blocks(c)).count() as u64
}
pub fn has_no_hole(&self) -> bool {
self.is_total()
}
pub fn relaxation_feasible_at_center(&self) -> bool {
self.blockers.iter().all(|b| b.care.count_ones() >= 2)
}
pub fn counting_refutation(&self) -> Option<crate::pigeonhole::CountingCert> {
crate::pigeonhole::counting_certificate(&self.to_expr()?)
}
pub fn hall_refutation(&self) -> Option<crate::matching::HallWitness> {
crate::pigeonhole::hall_refutation(&self.to_expr()?)
}
pub fn has_unique_hole(&self) -> bool {
self.solution_count() == 1
}
pub fn has_at_least_holes(&self, k: u64) -> bool {
self.solution_count() >= k
}
pub fn separated_by(&self, cut: u64) -> bool {
self.blockers.iter().all(|b| (b.care & cut) == b.care || (b.care & cut) == 0)
}
pub fn variable_interaction(&self, i: usize, j: usize) -> bool {
i != j
&& self.blockers.iter().any(|b| b.care & (1u64 << i) != 0 && b.care & (1u64 << j) != 0)
}
pub fn to_expr(&self) -> Option<crate::ProofExpr> {
use crate::ProofExpr;
let lit = |v: usize, positive: bool| {
let a = ProofExpr::Atom(format!("x{v}"));
if positive { a } else { ProofExpr::Not(Box::new(a)) }
};
let mut clauses = Vec::with_capacity(self.blockers.len());
for b in &self.blockers {
let lits = b.clause_literals();
if lits.is_empty() {
return None;
}
let mut it = lits.into_iter();
let (v0, p0) = it.next().unwrap();
clauses.push(it.fold(lit(v0, p0), |acc, (v, p)| ProofExpr::Or(Box::new(acc), Box::new(lit(v, p)))));
}
let mut it = clauses.into_iter();
let first = it.next()?;
Some(it.fold(first, |acc, c| ProofExpr::And(Box::new(acc), Box::new(c))))
}
pub fn prove_total_certified(&self) -> crate::sat::UnsatOutcome {
match self.to_expr() {
Some(e) => crate::sat::prove_unsat(&e),
None => crate::sat::UnsatOutcome::Unsupported,
}
}
pub fn neighbors(&self, i: usize) -> Vec<(usize, usize, Subcube)> {
(0..self.blockers.len())
.filter(|&j| j != i)
.filter_map(|j| self.blockers[i].resolve(&self.blockers[j]).map(|(pivot, r)| (j, pivot, r)))
.collect()
}
pub fn clauses(&self) -> Vec<Vec<Lit>> {
self.blockers
.iter()
.map(|b| b.clause_literals().into_iter().map(|(v, p)| Lit::new(v as u32, p)).collect())
.collect()
}
pub fn blocker_orbits(&self, generators: &[CubeSym]) -> Option<Vec<Vec<usize>>> {
let mut index: HashMap<Subcube, usize> = HashMap::new();
for (i, b) in self.blockers.iter().enumerate() {
index.entry(*b).or_insert(i);
}
let m = self.blockers.len();
let mut seen = vec![false; m];
let mut orbits = Vec::new();
for start in 0..m {
if seen[start] {
continue;
}
let mut orbit = Vec::new();
let mut stack = vec![start];
seen[start] = true;
while let Some(i) = stack.pop() {
orbit.push(i);
for g in generators {
let image = g.map_subcube(&self.blockers[i]);
let &j = index.get(&image)?; if !seen[j] {
seen[j] = true;
stack.push(j);
}
}
}
orbit.sort_unstable();
orbits.push(orbit);
}
Some(orbits)
}
pub fn discovered_rule_symmetry(&self) -> RuleSymmetry {
let clauses = self.clauses();
let generators = crate::symmetry_detect::find_generators(self.n, &clauses);
let rule_orbits = clause_orbits(&clauses, &generators).len();
RuleSymmetry { n: self.n, blockers: clauses.len(), generators: generators.len(), rule_orbits }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RuleSymmetry {
pub n: usize,
pub blockers: usize,
pub generators: usize,
pub rule_orbits: usize,
}
pub fn clause_orbits(clauses: &[Vec<Lit>], generators: &[Perm]) -> Vec<Vec<usize>> {
let index: HashMap<Vec<u32>, usize> = clauses
.iter()
.enumerate()
.map(|(i, c)| (crate::symmetry_detect::clause_key(c), i))
.collect();
let m = clauses.len();
let mut seen = vec![false; m];
let mut orbits = Vec::new();
for start in 0..m {
if seen[start] {
continue;
}
let mut orbit = Vec::new();
let mut stack = vec![start];
seen[start] = true;
while let Some(i) = stack.pop() {
orbit.push(i);
for g in generators {
let key = crate::symmetry_detect::clause_key(&g.apply_clause(&clauses[i]));
if let Some(&j) = index.get(&key) {
if !seen[j] {
seen[j] = true;
stack.push(j);
}
}
}
}
orbit.sort_unstable();
orbits.push(orbit);
}
orbits
}
pub fn php_perm_symmetries(n: usize) -> Vec<Perm> {
let holes = n.saturating_sub(1);
let num_vars = n * holes;
let var = |p: usize, h: usize| p * holes + h;
let mut gens = Vec::new();
for p in 0..n.saturating_sub(1) {
let mut images: Vec<Lit> = (0..num_vars as u32).map(Lit::pos).collect();
for h in 0..holes {
images.swap(var(p, h), var(p + 1, h));
}
gens.push(Perm::from_images(images));
}
for h in 0..holes.saturating_sub(1) {
let mut images: Vec<Lit> = (0..num_vars as u32).map(Lit::pos).collect();
for p in 0..n {
images.swap(var(p, h), var(p, h + 1));
}
gens.push(Perm::from_images(images));
}
gens
}
pub fn pigeonhole_rule_symmetry(n: usize) -> RuleSymmetry {
let (cnf, _) = crate::families::php(n);
let generators = php_perm_symmetries(n);
let rule_orbits = clause_orbits(&cnf.clauses, &generators).len();
RuleSymmetry { n, blockers: cnf.clauses.len(), generators: generators.len(), rule_orbits }
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AbstractRefutation {
pub rule_types: usize,
pub invariant: &'static str,
pub witness: crate::pigeonhole::CountingCert,
}
pub fn pigeonhole_abstract_refutation(n: usize) -> Option<AbstractRefutation> {
let (cnf, _) = crate::families::php(n);
let rule_types = clause_orbits(&cnf.clauses, &php_perm_symmetries(n)).len();
let witness = crate::pigeonhole::certify_pigeonhole_unsat(n as u128, n.saturating_sub(1) as u128)?;
Some(AbstractRefutation { rule_types, invariant: "Hall/matching: pigeons > holes", witness })
}
pub fn apply_renaming(clauses: &[Vec<Lit>], flips: &[bool]) -> Vec<Vec<Lit>> {
clauses
.iter()
.map(|c| {
c.iter()
.map(|l| if flips[l.var() as usize] { l.negated() } else { *l })
.collect()
})
.collect()
}
pub fn renaming_to_horn(num_vars: usize, clauses: &[Vec<Lit>]) -> Option<Vec<bool>> {
use crate::twosat::{self, Lit as TLit, TwoSatOutcome};
let flit = |l: &Lit| {
if l.is_positive() {
TLit::pos(l.var() as usize)
} else {
TLit::neg(l.var() as usize)
}
};
let mut two_sat: Vec<(TLit, TLit)> = Vec::new();
for c in clauses {
for i in 0..c.len() {
for j in (i + 1)..c.len() {
two_sat.push((flit(&c[i]), flit(&c[j]))); }
}
}
match twosat::solve(&two_sat, num_vars) {
TwoSatOutcome::Sat(flips) => Some(flips),
TwoSatOutcome::Unsat(_) => None,
}
}
pub fn automorphism_group_size(num_vars: usize, clauses: &[Vec<Lit>]) -> usize {
let generators = crate::symmetry_detect::find_generators(num_vars, clauses);
let key = |p: &Perm| -> Vec<(u32, bool)> {
(0..num_vars)
.map(|v| {
let l = p.apply(Lit::pos(v as u32));
(l.var(), l.is_positive())
})
.collect()
};
let id = Perm::identity(num_vars);
let mut seen: BTreeSet<Vec<(u32, bool)>> = [key(&id)].into_iter().collect();
let mut group = vec![id];
let mut i = 0;
while i < group.len() {
let g = group[i].clone();
i += 1;
for s in &generators {
let h = s.compose(&g);
if seen.insert(key(&h)) {
group.push(h);
}
}
if group.len() > 5_000_000 {
break;
}
}
group.len()
}
pub fn symmetry_entropy_bits(num_vars: usize, clauses: &[Vec<Lit>]) -> f64 {
(automorphism_group_size(num_vars, clauses) as f64).log2()
}
pub fn find_random_core(num_vars: usize, clauses: &[Vec<Lit>], max_steps: usize) -> Option<Vec<Vec<Lit>>> {
let mut current = clauses.to_vec();
for _ in 0..max_steps {
let cut = clauses_to_expr(¤t).is_some_and(|e| {
crate::pigeonhole::decide_pigeonhole_unsat(&e)
|| crate::xorsat::refute_via_parity(&e)
|| crate::pseudo_boolean::refute_clausal(&e)
});
if cut {
return None; }
match carve(num_vars, ¤t) {
CarveOutcome::Sat | CarveOutcome::Unsat => return None,
CarveOutcome::Core { clauses: core, .. } if core.len() < current.len() => {
current = core;
}
CarveOutcome::Core { clauses: core, .. } => {
let eliminated = bounded_variable_elimination(num_vars, &core);
if eliminated.len() < core.len() {
current = eliminated;
} else {
return Some(core); }
}
}
}
Some(current)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AdvanceStatus {
Decided(bool),
StructurelessResidue { core: usize },
}
#[derive(Clone, Debug, PartialEq)]
pub struct AdvanceStep {
pub lever: &'static str,
pub clauses: usize,
pub symmetry_bits: f64,
}
pub fn auto_advance(
num_vars: usize,
clauses: &[Vec<Lit>],
max_steps: usize,
) -> (AdvanceStatus, Vec<AdvanceStep>) {
let mut current = clauses.to_vec();
let mut trace = Vec::new();
for _ in 0..max_steps {
let bits = symmetry_entropy_bits(num_vars, ¤t);
let cut = clauses_to_expr(¤t).is_some_and(|e| {
crate::pigeonhole::decide_pigeonhole_unsat(&e)
|| crate::xorsat::refute_via_parity(&e)
|| crate::pseudo_boolean::refute_clausal(&e)
});
if cut {
trace.push(AdvanceStep { lever: "certified cut → UNSAT", clauses: current.len(), symmetry_bits: bits });
return (AdvanceStatus::Decided(false), trace);
}
match carve(num_vars, ¤t) {
CarveOutcome::Sat => {
trace.push(AdvanceStep { lever: "carve → SAT", clauses: 0, symmetry_bits: bits });
return (AdvanceStatus::Decided(true), trace);
}
CarveOutcome::Unsat => {
trace.push(AdvanceStep { lever: "carve → UNSAT", clauses: 0, symmetry_bits: bits });
return (AdvanceStatus::Decided(false), trace);
}
CarveOutcome::Core { clauses: core, .. } if core.len() < current.len() => {
trace.push(AdvanceStep { lever: "carve (unit/pure/subsume)", clauses: core.len(), symmetry_bits: bits });
current = core;
continue;
}
CarveOutcome::Core { clauses: core, .. } => {
let eliminated = bounded_variable_elimination(num_vars, &core);
if eliminated.len() < core.len() {
trace.push(AdvanceStep { lever: "variable elimination (project a dimension)", clauses: eliminated.len(), symmetry_bits: bits });
current = eliminated;
continue;
}
trace.push(AdvanceStep { lever: "irreducible core — no structure left (branch)", clauses: core.len(), symmetry_bits: bits });
return (AdvanceStatus::StructurelessResidue { core: core.len() }, trace);
}
}
}
(AdvanceStatus::StructurelessResidue { core: current.len() }, trace)
}
#[derive(Clone, Debug, PartialEq)]
pub struct Diagnosis {
pub clauses: usize,
pub symmetry_bits: f64,
pub rule_quotient: usize,
pub cut: Option<Shadow>,
pub antipodal: bool,
pub renamable_horn: bool,
pub components: usize,
pub autark_section: bool,
pub core_clauses: usize,
}
pub fn diagnose(num_vars: usize, clauses: &[Vec<Lit>]) -> Diagnosis {
let generators = crate::symmetry_detect::find_generators(num_vars, clauses);
let rule_quotient = clause_orbits(clauses, &generators).len();
let symmetry_bits = symmetry_entropy_bits(num_vars, clauses);
let cut = clauses_to_expr(clauses).and_then(|e| {
if crate::pigeonhole::decide_pigeonhole_unsat(&e) {
Some(Shadow::Counting)
} else if crate::xorsat::refute_via_parity(&e) {
Some(Shadow::Parity)
} else if crate::pseudo_boolean::refute_clausal(&e) {
Some(Shadow::CuttingPlanes)
} else {
None
}
});
let (_, assigned) = pure_literal_reduce(num_vars, clauses);
let core_clauses = match carve(num_vars, clauses) {
CarveOutcome::Core { clauses: c, .. } => c.len(),
_ => 0,
};
Diagnosis {
clauses: clauses.len(),
symmetry_bits,
rule_quotient,
cut,
antipodal: is_antipodally_symmetric(clauses),
renamable_horn: renaming_to_horn(num_vars, clauses).is_some(),
components: components(num_vars, clauses).len(),
autark_section: !assigned.is_empty(),
core_clauses,
}
}
pub fn applicable_levers(d: &Diagnosis) -> Vec<&'static str> {
let mut levers = Vec::new();
if let Some(s) = d.cut {
levers.push(match s {
Shadow::Counting => "counting/Hall cut (one-punch)",
Shadow::Parity => "GF(2) parity cut (one-punch)",
Shadow::CuttingPlanes => "cutting-planes cut (one-punch)",
});
}
if d.symmetry_bits > 0.0 {
levers.push("symmetry breaking (lex-leader prune)");
}
if d.antipodal {
levers.push("antipodal / center-inversion (recursive)");
}
if d.renamable_horn {
levers.push("renamable-Horn (poly via 2-SAT renaming)");
}
if d.components > 1 {
levers.push("component decomposition");
}
if d.autark_section || d.core_clauses < d.clauses {
levers.push("autarky / carving (unit, pure-literal, subsumption)");
}
if levers.is_empty() {
levers.push("no global structure — backdoor + branch the residue (the honest wall)");
}
levers
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StructuralProfile {
pub clauses: usize,
pub quotient: usize,
pub cut: Option<Shadow>,
pub core_clauses: usize,
}
pub fn structural_profile(num_vars: usize, clauses: &[Vec<Lit>]) -> StructuralProfile {
let generators = crate::symmetry_detect::find_generators(num_vars, clauses);
let quotient = clause_orbits(clauses, &generators).len();
let cut = clauses_to_expr(clauses).and_then(|e| {
if crate::pigeonhole::decide_pigeonhole_unsat(&e) {
Some(Shadow::Counting)
} else if crate::xorsat::refute_via_parity(&e) {
Some(Shadow::Parity)
} else if crate::pseudo_boolean::refute_clausal(&e) {
Some(Shadow::CuttingPlanes)
} else {
None
}
});
let core_clauses = match carve(num_vars, clauses) {
CarveOutcome::Sat | CarveOutcome::Unsat => 0,
CarveOutcome::Core { clauses: c, .. } => bounded_variable_elimination(num_vars, &c).len(),
};
StructuralProfile { clauses: clauses.len(), quotient, cut, core_clauses }
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Shadow {
Counting,
Parity,
CuttingPlanes,
}
pub fn apply_perm_to_model(perm: &Perm, model: &[bool]) -> Vec<bool> {
let mut out = model.to_vec();
for v in 0..model.len() {
let image = perm.apply(Lit::pos(v as u32));
out[image.var() as usize] = if image.is_positive() { model[v] } else { !model[v] };
}
out
}
pub fn model_orbit(model: &[bool], generators: &[Perm]) -> Vec<Vec<bool>> {
let mut seen = BTreeSet::new();
seen.insert(model.to_vec());
let mut stack = vec![model.to_vec()];
while let Some(m) = stack.pop() {
for g in generators {
let image = apply_perm_to_model(g, &m);
if seen.insert(image.clone()) {
stack.push(image);
}
}
}
seen.into_iter().collect()
}
pub fn canonical_model(model: &[bool], generators: &[Perm]) -> Vec<bool> {
model_orbit(model, generators).into_iter().min().unwrap()
}
pub fn perm_group_closure(generators: &[Perm], num_vars: usize) -> Vec<Perm> {
let mut seen = std::collections::HashSet::new();
let id = Perm::identity(num_vars);
let mut frontier = vec![id.clone()];
seen.insert(id);
while let Some(g) = frontier.pop() {
for h in generators {
let gh = h.compose(&g);
if seen.insert(gh.clone()) {
frontier.push(gh);
}
}
}
seen.into_iter().collect()
}
pub fn stabilizer(model: &[bool], group: &[Perm]) -> Vec<Perm> {
group.iter().filter(|g| apply_perm_to_model(g, model) == model).cloned().collect()
}
pub fn witness_perspective(model: &[bool], generators: &[Perm], num_vars: usize) -> Vec<(Vec<bool>, Perm)> {
let group = perm_group_closure(generators, num_vars);
let mut seen = BTreeSet::new();
let mut out = Vec::new();
let mut by_dest: Vec<(Vec<bool>, Perm)> = Vec::new();
for g in &group {
let dest = apply_perm_to_model(g, model);
if seen.insert(dest.clone()) {
by_dest.push((dest, g.clone()));
}
}
by_dest.sort_by(|a, b| a.0.cmp(&b.0));
let here = by_dest.iter().position(|(d, _)| d == model).unwrap();
out.push((model.to_vec(), Perm::identity(num_vars)));
for (i, pair) in by_dest.into_iter().enumerate() {
if i != here {
out.push(pair);
}
}
out
}
pub fn burnside_orbit_count(models: &[Vec<bool>], group: &[Perm]) -> usize {
let total_fixed: usize = group
.iter()
.map(|g| models.iter().filter(|m| apply_perm_to_model(g, m.as_slice()) == **m).count())
.sum();
total_fixed / group.len()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ProofRung {
Trivial,
Counting,
Parity,
ModCount { p: u64 },
Nullstellensatz { min_degree: usize },
BeyondBudget,
}
pub fn weakest_crushing_rung(num_vars: usize, clauses: &[Vec<Lit>], ns_budget: usize) -> ProofRung {
weakest_crushing_rung_with_char(num_vars, clauses, ns_budget, &[])
}
pub fn weakest_crushing_rung_with_char(
num_vars: usize,
clauses: &[Vec<Lit>],
ns_budget: usize,
primes: &[u64],
) -> ProofRung {
if let CarveOutcome::Unsat = carve(num_vars, clauses) {
return ProofRung::Trivial;
}
let Some(e) = clauses_to_expr(clauses) else { return ProofRung::BeyondBudget };
if crate::pigeonhole::counting_certificate(&e).is_some() || crate::pigeonhole::hall_refutation(&e).is_some() {
return ProofRung::Counting;
}
if crate::xorsat::refute_via_parity(&e) {
return ProofRung::Parity;
}
if !primes.is_empty() {
if let Some(rec) = crate::modp::recover_from_cnf(num_vars, clauses) {
if primes.contains(&rec.modulus) {
if let crate::modp::ModpOutcome::Unsat(combo) =
crate::modp::solve(&rec.equations, rec.num_vars, rec.modulus)
{
if crate::modp::is_refutation(&rec.equations, rec.num_vars, rec.modulus, &combo) {
return ProofRung::ModCount { p: rec.modulus };
}
}
}
}
}
let cap = ns_budget.min(num_vars);
if let Some(d) = (1..=cap).find(|&d| crate::polycalc::nullstellensatz_refutes(num_vars, clauses, d)) {
return ProofRung::Nullstellensatz { min_degree: d };
}
ProofRung::BeyondBudget
}
pub fn partition_into_orbits(models: &[Vec<bool>], generators: &[Perm]) -> Vec<Vec<Vec<bool>>> {
let model_set: BTreeSet<Vec<bool>> = models.iter().cloned().collect();
let mut assigned: BTreeSet<Vec<bool>> = BTreeSet::new();
let mut orbits = Vec::new();
for m in models {
if assigned.contains(m) {
continue;
}
let orbit: Vec<Vec<bool>> =
model_orbit(m, generators).into_iter().filter(|x| model_set.contains(x)).collect();
for x in &orbit {
assigned.insert(x.clone());
}
orbits.push(orbit);
}
orbits
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct CarveStats {
pub nodes: usize,
pub punches: usize,
pub max_depth: usize,
}
pub fn autocarve(num_vars: usize, clauses: &[Vec<Lit>], budget: usize) -> Option<bool> {
autocarve_measured(num_vars, clauses, budget).0
}
pub fn autocarve_measured(
num_vars: usize,
clauses: &[Vec<Lit>],
budget: usize,
) -> (Option<bool>, CarveStats) {
let mut stats = CarveStats::default();
let verdict = autocarve_rec(num_vars, clauses, budget, 0, &mut stats);
(verdict, stats)
}
fn autocarve_rec(
num_vars: usize,
clauses: &[Vec<Lit>],
budget: usize,
depth: usize,
stats: &mut CarveStats,
) -> Option<bool> {
stats.nodes += 1;
stats.max_depth = stats.max_depth.max(depth);
if stats.nodes > budget {
return None;
}
let core = match carve(num_vars, clauses) {
CarveOutcome::Sat => return Some(true),
CarveOutcome::Unsat => return Some(false),
CarveOutcome::Core { clauses, .. } => clauses,
};
for component in components(num_vars, &core) {
let cut = clauses_to_expr(&component).is_some_and(|e| {
crate::pigeonhole::counting_certificate(&e).is_some()
|| crate::pigeonhole::decide_pigeonhole_unsat(&e)
|| crate::xorsat::refute_via_parity(&e)
|| crate::pseudo_boolean::refute_clausal(&e)
});
if cut {
stats.punches += 1;
return Some(false); }
let pivot = component[0][0].var();
let mut component_sat = false;
for value in [false, true] {
let mut branch = component.clone();
branch.push(vec![Lit::new(pivot, value)]);
match autocarve_rec(num_vars, &branch, budget, depth + 1, stats) {
Some(true) => {
component_sat = true;
break;
}
Some(false) => {}
None => return None,
}
}
if !component_sat {
return Some(false); }
}
Some(true)
}
pub fn crush(num_vars: usize, clauses: &[Vec<Lit>], budget: usize) -> Option<bool> {
let (core, _) = pure_literal_reduce(num_vars, clauses);
if core.is_empty() {
return Some(true); }
for component in components(num_vars, &core) {
match search_cost(num_vars, &component, true, budget) {
SearchCost::Decided { sat: false, .. } => return Some(false), SearchCost::Decided { sat: true, .. } => {} SearchCost::Exceeded { .. } => return None, }
}
Some(true) }
fn resolve_on_var(cp: &[Lit], cn: &[Lit], v: usize) -> Option<Vec<Lit>> {
let mut lits: Vec<Lit> = Vec::new();
for &l in cp.iter().chain(cn.iter()) {
if l.var() as usize != v && !lits.contains(&l) {
lits.push(l);
}
}
if lits.iter().any(|l| lits.contains(&l.negated())) {
return None; }
Some(lits)
}
pub fn eliminate_variable(v: usize, clauses: &[Vec<Lit>]) -> Vec<Vec<Lit>> {
let (pv, nv) = (Lit::new(v as u32, true), Lit::new(v as u32, false));
let mut result: Vec<Vec<Lit>> =
clauses.iter().filter(|c| !c.contains(&pv) && !c.contains(&nv)).cloned().collect();
let pos: Vec<&Vec<Lit>> = clauses.iter().filter(|c| c.contains(&pv)).collect();
let neg: Vec<&Vec<Lit>> = clauses.iter().filter(|c| c.contains(&nv)).collect();
for cp in &pos {
for cn in &neg {
if let Some(resolvent) = resolve_on_var(cp, cn, v) {
result.push(resolvent);
}
}
}
result
}
pub fn bounded_variable_elimination(num_vars: usize, clauses: &[Vec<Lit>]) -> Vec<Vec<Lit>> {
let mut current = clauses.to_vec();
loop {
let mut eliminated = false;
for v in 0..num_vars {
let pos = current.iter().filter(|c| c.contains(&Lit::new(v as u32, true))).count();
let neg = current.iter().filter(|c| c.contains(&Lit::new(v as u32, false))).count();
if pos == 0 || neg == 0 {
continue;
}
let candidate = eliminate_variable(v, ¤t);
if candidate.len() <= current.len() {
current = candidate;
eliminated = true;
}
}
if !eliminated {
break;
}
}
current
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CarveOutcome {
Sat,
Unsat,
Core { clauses: Vec<Vec<Lit>>, forced: Vec<Lit> },
}
fn find_pure_literal(num_vars: usize, clauses: &[Vec<Lit>]) -> Option<Lit> {
let mut pos = vec![false; num_vars];
let mut neg = vec![false; num_vars];
for c in clauses {
for l in c {
if l.is_positive() {
pos[l.var() as usize] = true;
} else {
neg[l.var() as usize] = true;
}
}
}
(0..num_vars).find_map(|v| match (pos[v], neg[v]) {
(true, false) => Some(Lit::new(v as u32, true)),
(false, true) => Some(Lit::new(v as u32, false)),
_ => None,
})
}
fn subsume_once(clauses: &mut Vec<Vec<Lit>>) -> bool {
for i in 0..clauses.len() {
for j in 0..clauses.len() {
if i != j
&& clauses[i].len() < clauses[j].len()
&& clauses[i].iter().all(|l| clauses[j].contains(l))
{
clauses.remove(j);
return true;
}
}
}
false
}
pub fn carve(num_vars: usize, clauses: &[Vec<Lit>]) -> CarveOutcome {
let mut current: Vec<Vec<Lit>> = clauses.to_vec();
let mut forced: Vec<Lit> = Vec::new();
loop {
if current.iter().any(|c| c.is_empty()) {
return CarveOutcome::Unsat;
}
if current.is_empty() {
return CarveOutcome::Sat;
}
let mut changed = false;
if let Some(unit) = current.iter().find(|c| c.len() == 1).map(|c| c[0]) {
forced.push(unit);
let neg = unit.negated();
current.retain(|c| !c.contains(&unit));
for c in &mut current {
c.retain(|&l| l != neg);
}
changed = true;
} else if let Some(pure) = find_pure_literal(num_vars, ¤t) {
forced.push(pure);
current.retain(|c| !c.contains(&pure));
changed = true;
} else if subsume_once(&mut current) {
changed = true;
}
if !changed {
return CarveOutcome::Core { clauses: current, forced };
}
}
}
pub fn pure_literal_reduce(num_vars: usize, clauses: &[Vec<Lit>]) -> (Vec<Vec<Lit>>, Vec<Lit>) {
let mut current: Vec<Vec<Lit>> = clauses.to_vec();
let mut assigned = Vec::new();
loop {
let mut pos = vec![false; num_vars];
let mut neg = vec![false; num_vars];
for c in ¤t {
for l in c {
if l.is_positive() {
pos[l.var() as usize] = true;
} else {
neg[l.var() as usize] = true;
}
}
}
let pure = (0..num_vars).find_map(|v| match (pos[v], neg[v]) {
(true, false) => Some(Lit::new(v as u32, true)),
(false, true) => Some(Lit::new(v as u32, false)),
_ => None,
});
let Some(l) = pure else { break };
assigned.push(l);
current.retain(|c| !c.iter().any(|&x| x == l));
}
(current, assigned)
}
pub fn components(num_vars: usize, clauses: &[Vec<Lit>]) -> Vec<Vec<Vec<Lit>>> {
fn find(parent: &mut [usize], mut x: usize) -> usize {
while parent[x] != x {
parent[x] = parent[parent[x]];
x = parent[x];
}
x
}
let mut parent: Vec<usize> = (0..num_vars.max(1)).collect();
for clause in clauses {
let vars: Vec<usize> = clause.iter().map(|l| l.var() as usize).collect();
for pair in vars.windows(2) {
let (a, b) = (find(&mut parent, pair[0]), find(&mut parent, pair[1]));
parent[a] = b;
}
}
let mut groups: HashMap<usize, Vec<Vec<Lit>>> = HashMap::new();
for clause in clauses {
let root = clause.first().map(|l| find(&mut parent, l.var() as usize)).unwrap_or(0);
groups.entry(root).or_default().push(clause.clone());
}
groups.into_values().collect()
}
pub fn decompose_and_crush(num_vars: usize, clauses: &[Vec<Lit>]) -> bool {
components(num_vars, clauses).iter().any(|comp| {
clauses_to_expr(comp).is_some_and(|e| {
crate::pigeonhole::decide_pigeonhole_unsat(&e)
|| crate::xorsat::refute_via_parity(&e)
|| crate::pseudo_boolean::refute_clausal(&e)
})
})
}
pub fn is_antipodally_symmetric(clauses: &[Vec<Lit>]) -> bool {
let key = |c: &[Lit]| -> Vec<u32> {
let mut k: Vec<u32> = c.iter().map(|l| l.var() * 2 + u32::from(!l.is_positive())).collect();
k.sort_unstable();
k.dedup();
k
};
let original: BTreeSet<Vec<u32>> = clauses.iter().map(|c| key(c)).collect();
let flipped: BTreeSet<Vec<u32>> = clauses
.iter()
.map(|c| key(&c.iter().map(|l| l.negated()).collect::<Vec<_>>()))
.collect();
original == flipped
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LadderStats {
pub nodes: usize,
pub max_depth: usize,
pub cut_closures: usize,
pub pruned: usize,
}
pub fn decide_laddered(num_vars: usize, clauses: &[Vec<Lit>]) -> (bool, LadderStats) {
let mut stats = LadderStats { nodes: 0, max_depth: 0, cut_closures: 0, pruned: 0 };
let sat = ladder(clauses, vec![None; num_vars], 0, &mut stats);
(sat, stats)
}
pub fn decide_laddered_sym(num_vars: usize, clauses: &[Vec<Lit>], use_cut: bool) -> (bool, LadderStats) {
let generators = crate::symmetry_detect::find_generators(num_vars, clauses);
let mut stats = LadderStats { nodes: 0, max_depth: 0, cut_closures: 0, pruned: 0 };
let sat = ladder_sym(clauses, vec![None; num_vars], 0, &generators, use_cut, &mut stats);
(sat, stats)
}
pub fn decide_laddered_nocut(num_vars: usize, clauses: &[Vec<Lit>]) -> (bool, LadderStats) {
let mut stats = LadderStats { nodes: 0, max_depth: 0, cut_closures: 0, pruned: 0 };
let sat = ladder_sym(clauses, vec![None; num_vars], 0, &[], false, &mut stats);
(sat, stats)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SearchCost {
Decided { sat: bool, nodes: usize },
Exceeded { budget: usize },
}
pub fn search_cost(num_vars: usize, clauses: &[Vec<Lit>], use_cut: bool, budget: usize) -> SearchCost {
let mut nodes = 0usize;
match cost_rec(clauses, vec![None; num_vars], use_cut, budget, &mut nodes) {
Some(sat) => SearchCost::Decided { sat, nodes },
None => SearchCost::Exceeded { budget },
}
}
pub fn search_cost_antipodal(num_vars: usize, clauses: &[Vec<Lit>], budget: usize) -> SearchCost {
let mut nodes = 0usize;
match antipodal_rec(clauses, vec![None; num_vars], budget, &mut nodes) {
Some(sat) => SearchCost::Decided { sat, nodes },
None => SearchCost::Exceeded { budget },
}
}
fn antipodal_rec(
clauses: &[Vec<Lit>],
assignment: Vec<Option<bool>>,
budget: usize,
nodes: &mut usize,
) -> Option<bool> {
*nodes += 1;
if *nodes > budget {
return None;
}
let residual = restrict(clauses, &assignment);
if residual.iter().any(|c| c.is_empty()) {
return Some(false);
}
if residual.is_empty() {
return Some(true);
}
let pivot = residual[0][0].var() as usize;
let values: &[bool] = if is_antipodally_symmetric(&residual) {
&[false]
} else {
&[false, true]
};
for &value in values {
let mut next = assignment.clone();
next[pivot] = Some(value);
match antipodal_rec(clauses, next, budget, nodes) {
Some(true) => return Some(true),
Some(false) => {}
None => return None,
}
}
Some(false)
}
fn cost_rec(
clauses: &[Vec<Lit>],
assignment: Vec<Option<bool>>,
use_cut: bool,
budget: usize,
nodes: &mut usize,
) -> Option<bool> {
*nodes += 1;
if *nodes > budget {
return None;
}
let residual = restrict(clauses, &assignment);
if residual.iter().any(|c| c.is_empty()) {
return Some(false);
}
if residual.is_empty() {
return Some(true);
}
if use_cut {
if let Some(e) = clauses_to_expr(&residual) {
if crate::pigeonhole::decide_pigeonhole_unsat(&e)
|| crate::xorsat::refute_via_parity(&e)
|| crate::pseudo_boolean::refute_clausal(&e)
{
return Some(false);
}
}
}
let Some(pivot) = assignment.iter().position(|a| a.is_none()) else {
return Some(true);
};
for value in [false, true] {
let mut next = assignment.clone();
next[pivot] = Some(value);
match cost_rec(clauses, next, use_cut, budget, nodes) {
Some(true) => return Some(true),
Some(false) => {}
None => return None,
}
}
Some(false)
}
fn ladder_sym(
clauses: &[Vec<Lit>],
assignment: Vec<Option<bool>>,
depth: usize,
generators: &[Perm],
use_cut: bool,
stats: &mut LadderStats,
) -> bool {
stats.nodes += 1;
stats.max_depth = stats.max_depth.max(depth);
let residual = restrict(clauses, &assignment);
if residual.iter().any(|c| c.is_empty()) {
return false;
}
if residual.is_empty() {
return true;
}
if use_cut {
if let Some(e) = clauses_to_expr(&residual) {
if crate::pigeonhole::decide_pigeonhole_unsat(&e)
|| crate::xorsat::refute_via_parity(&e)
|| crate::pseudo_boolean::refute_clausal(&e)
{
stats.cut_closures += 1;
return false;
}
}
}
let Some(pivot) = assignment.iter().position(|a| a.is_none()) else {
return true;
};
for value in [false, true] {
let mut next = assignment.clone();
next[pivot] = Some(value);
if violates_lex_leader(&next, generators) {
stats.pruned += 1;
continue; }
if ladder_sym(clauses, next, depth + 1, generators, use_cut, stats) {
return true;
}
}
false
}
fn violates_lex_leader(a: &[Option<bool>], generators: &[Perm]) -> bool {
let n = a.len();
for sigma in generators {
let mut b = vec![None; n];
for v in 0..n {
if let Some(val) = a[v] {
let image = sigma.apply(Lit::pos(v as u32));
b[image.var() as usize] = Some(if image.is_positive() { val } else { !val });
}
}
if (0..n).any(|v| a[v].is_some() != b[v].is_some()) {
continue;
}
for v in 0..n {
if let (Some(av), Some(bv)) = (a[v], b[v]) {
if av != bv {
if !bv {
return true;
}
break;
}
}
}
}
false
}
fn ladder(
clauses: &[Vec<Lit>],
assignment: Vec<Option<bool>>,
depth: usize,
stats: &mut LadderStats,
) -> bool {
stats.nodes += 1;
stats.max_depth = stats.max_depth.max(depth);
let residual = restrict(clauses, &assignment);
if residual.iter().any(|c| c.is_empty()) {
return false; }
if residual.is_empty() {
return true; }
if let Some(unit) = residual.iter().find(|c| c.len() == 1) {
let l = unit[0];
let mut next = assignment.clone();
next[l.var() as usize] = Some(l.is_positive());
return ladder(clauses, next, depth + 1, stats);
}
if let Some(e) = clauses_to_expr(&residual) {
if crate::pigeonhole::decide_pigeonhole_unsat(&e)
|| crate::xorsat::refute_via_parity(&e)
|| crate::pseudo_boolean::refute_clausal(&e)
{
stats.cut_closures += 1;
return false;
}
}
let pivot = residual[0][0].var() as usize;
for value in [false, true] {
let mut next = assignment.clone();
next[pivot] = Some(value);
if ladder(clauses, next, depth + 1, stats) {
return true;
}
}
false
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CoverVerdict {
Total { cut: Option<Shadow> },
Escapes,
Unknown,
}
impl Cover {
pub fn auto_certify(&self) -> CoverVerdict {
let Some(e) = self.to_expr() else { return CoverVerdict::Unknown };
if crate::pigeonhole::decide_pigeonhole_unsat(&e) {
return CoverVerdict::Total { cut: Some(Shadow::Counting) };
}
if crate::xorsat::refute_via_parity(&e) {
return CoverVerdict::Total { cut: Some(Shadow::Parity) };
}
if crate::pseudo_boolean::refute_clausal(&e) {
return CoverVerdict::Total { cut: Some(Shadow::CuttingPlanes) };
}
match crate::sat::prove_unsat(&e) {
crate::sat::UnsatOutcome::Refuted => CoverVerdict::Total { cut: None },
crate::sat::UnsatOutcome::Sat(_) => CoverVerdict::Escapes,
crate::sat::UnsatOutcome::Unsupported => CoverVerdict::Unknown,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FamilySignature {
pub num_vars: usize,
pub clauses: usize,
pub rule_types: usize,
pub shadow: Option<Shadow>,
}
pub fn abstract_signature(num_vars: usize, clauses: &[Vec<Lit>]) -> FamilySignature {
let generators = crate::symmetry_detect::find_generators(num_vars, clauses);
let rule_types = clause_orbits(clauses, &generators).len();
let shadow = clauses_to_expr(clauses).and_then(|e| {
if crate::pigeonhole::decide_pigeonhole_unsat(&e) {
Some(Shadow::Counting)
} else if crate::xorsat::refute_via_parity(&e) {
Some(Shadow::Parity)
} else if crate::pseudo_boolean::refute_clausal(&e) {
Some(Shadow::CuttingPlanes)
} else {
None
}
});
FamilySignature { num_vars, clauses: clauses.len(), rule_types, shadow }
}
pub fn restrict(clauses: &[Vec<Lit>], assignment: &[Option<bool>]) -> Vec<Vec<Lit>> {
let mut out = Vec::new();
'clause: for c in clauses {
let mut residual = Vec::new();
for &l in c {
match assignment.get(l.var() as usize).copied().flatten() {
Some(value) => {
if value == l.is_positive() {
continue 'clause; }
}
None => residual.push(l),
}
}
out.push(residual);
}
out
}
fn to_twosat_lit(l: Lit) -> crate::twosat::Lit {
if l.is_positive() {
crate::twosat::Lit::pos(l.var() as usize)
} else {
crate::twosat::Lit::neg(l.var() as usize)
}
}
pub fn decide_2sat(clauses: &[Vec<Lit>], num_vars: usize) -> bool {
let mut pairs = Vec::with_capacity(clauses.len());
for c in clauses {
match c.as_slice() {
[] => return false,
[a] => pairs.push((to_twosat_lit(*a), to_twosat_lit(*a))),
[a, b] => pairs.push((to_twosat_lit(*a), to_twosat_lit(*b))),
_ => panic!("decide_2sat given a width-{} clause", c.len()),
}
}
matches!(crate::twosat::solve(&pairs, num_vars), crate::twosat::TwoSatOutcome::Sat(_))
}
pub fn greedy_2sat_backdoor(clauses: &[Vec<Lit>], num_vars: usize) -> Vec<usize> {
let mut chosen = vec![false; num_vars];
let mut backdoor = Vec::new();
loop {
let mut freq = vec![0usize; num_vars];
let mut any_wide = false;
for c in clauses {
let free = c.iter().filter(|l| !chosen[l.var() as usize]).count();
if free > 2 {
any_wide = true;
for l in c {
if !chosen[l.var() as usize] {
freq[l.var() as usize] += 1;
}
}
}
}
if !any_wide {
break;
}
let best = (0..num_vars).max_by_key(|&v| freq[v]).unwrap();
chosen[best] = true;
backdoor.push(best);
}
backdoor.sort_unstable();
backdoor
}
pub fn is_strong_backdoor_to_2sat(clauses: &[Vec<Lit>], num_vars: usize, backdoor: &[usize]) -> bool {
let k = backdoor.len();
if k > 24 {
return false;
}
for mask in 0u32..(1u32 << k) {
let mut assignment = vec![None; num_vars];
for (i, &v) in backdoor.iter().enumerate() {
assignment[v] = Some(mask & (1 << i) != 0);
}
if restrict(clauses, &assignment).iter().any(|c| c.len() > 2) {
return false;
}
}
true
}
pub fn decide_sat_via_2sat_backdoor(clauses: &[Vec<Lit>], num_vars: usize, backdoor: &[usize]) -> bool {
for mask in 0u32..(1u32 << backdoor.len()) {
let mut assignment = vec![None; num_vars];
for (i, &v) in backdoor.iter().enumerate() {
assignment[v] = Some(mask & (1 << i) != 0);
}
let residual = restrict(clauses, &assignment);
if decide_2sat(&residual, num_vars) {
return true;
}
}
false
}
pub fn canonical_blocker(b: &Subcube, generators: &[CubeSym]) -> Subcube {
let mut best = *b;
let mut seen = BTreeSet::new();
seen.insert(*b);
let mut stack = vec![*b];
while let Some(x) = stack.pop() {
for g in generators {
let y = g.map_subcube(&x);
if seen.insert(y) {
if y < best {
best = y;
}
stack.push(y);
}
}
}
best
}
pub fn blocker_orbit(b: &Subcube, generators: &[CubeSym]) -> Vec<Subcube> {
let mut seen = BTreeSet::new();
seen.insert(*b);
let mut stack = vec![*b];
while let Some(x) = stack.pop() {
for g in generators {
let y = g.map_subcube(&x);
if seen.insert(y) {
stack.push(y);
}
}
}
seen.into_iter().collect()
}
pub fn symmetric_resolution_closure(
cover: &Cover,
generators: &[CubeSym],
max_rounds: usize,
max_reps: usize,
) -> (usize, bool) {
let empty = Subcube { n: cover.n, care: 0, value: 0 };
let mut reps: BTreeSet<Subcube> =
cover.blockers.iter().map(|b| canonical_blocker(b, generators)).collect();
let mut refuted = reps.contains(&empty);
for _ in 0..max_rounds {
if refuted {
break;
}
let current: Vec<Subcube> = reps.iter().copied().collect();
let orbits: Vec<Vec<Subcube>> =
current.iter().map(|d| blocker_orbit(d, generators)).collect();
let mut added = false;
'outer: for c in ¤t {
for orbit in &orbits {
for image in orbit {
if let Some((_, r)) = c.resolve(image) {
let canon = canonical_blocker(&r, generators);
if reps.insert(canon) {
added = true;
if canon == empty {
refuted = true;
break 'outer;
}
if reps.len() > max_reps {
break 'outer;
}
}
}
}
}
}
if !added {
break;
}
}
(reps.len(), refuted)
}
pub fn symmetric_resolution_growth(
cover: &Cover,
generators: &[CubeSym],
rounds: usize,
) -> Vec<(usize, usize)> {
let mut raw: BTreeSet<Subcube> = cover.blockers.iter().copied().collect();
let mut out = Vec::new();
for _ in 0..rounds {
let current: Vec<Subcube> = raw.iter().copied().collect();
for i in 0..current.len() {
for j in (i + 1)..current.len() {
if let Some((_, r)) = current[i].resolve(¤t[j]) {
raw.insert(r);
}
}
}
let orbits: BTreeSet<Subcube> =
raw.iter().map(|b| canonical_blocker(b, generators)).collect();
out.push((raw.len(), orbits.len()));
}
out
}
pub fn backdoor_branch_orbit_count(backdoor: &[usize], generators: &[Perm]) -> u64 {
let k = backdoor.len();
let position: HashMap<usize, usize> = backdoor.iter().enumerate().map(|(i, &v)| (v, i)).collect();
let mut induced: Vec<(Vec<usize>, u32)> = Vec::new();
for g in generators {
let mut perm = vec![0usize; k];
let mut flip = 0u32;
let mut preserves = true;
for (i, &v) in backdoor.iter().enumerate() {
let image = g.apply(Lit::pos(v as u32));
match position.get(&(image.var() as usize)) {
Some(&j) => {
perm[i] = j;
if !image.is_positive() {
flip |= 1 << j;
}
}
None => {
preserves = false;
break;
}
}
}
if preserves {
induced.push((perm, flip));
}
}
let total = 1u32 << k;
let mut seen = vec![false; total as usize];
let mut orbits = 0u64;
for start in 0..total {
if seen[start as usize] {
continue;
}
orbits += 1;
let mut stack = vec![start];
seen[start as usize] = true;
while let Some(m) = stack.pop() {
for (perm, flip) in &induced {
let mut image = 0u32;
for i in 0..k {
if m & (1 << i) != 0 {
image |= 1 << perm[i];
}
}
image ^= flip;
if !seen[image as usize] {
seen[image as usize] = true;
stack.push(image);
}
}
}
}
orbits
}
pub fn clauses_to_expr(clauses: &[Vec<Lit>]) -> Option<crate::ProofExpr> {
use crate::ProofExpr;
let lit = |l: &Lit| {
let a = ProofExpr::Atom(format!("x{}", l.var()));
if l.is_positive() { a } else { ProofExpr::Not(Box::new(a)) }
};
fn balanced(
mut nodes: Vec<ProofExpr>,
combine: impl Fn(Box<ProofExpr>, Box<ProofExpr>) -> ProofExpr,
) -> ProofExpr {
while nodes.len() > 1 {
let mut next = Vec::with_capacity((nodes.len() + 1) / 2);
let mut it = nodes.into_iter();
while let Some(a) = it.next() {
match it.next() {
Some(b) => next.push(combine(Box::new(a), Box::new(b))),
None => next.push(a),
}
}
nodes = next;
}
nodes.into_iter().next().expect("balanced() requires a non-empty node list")
}
let mut built = Vec::with_capacity(clauses.len());
for c in clauses {
if c.is_empty() {
return None;
}
let lits: Vec<ProofExpr> = c.iter().map(|l| lit(l)).collect();
built.push(balanced(lits, |a, b| ProofExpr::Or(a, b)));
}
if built.is_empty() {
return None;
}
Some(balanced(built, |a, b| crate::ProofExpr::And(a, b)))
}
#[derive(Clone, Debug)]
pub struct CubeSym {
pub perm: Vec<usize>,
pub flip: Vec<bool>,
}
impl CubeSym {
pub fn identity(n: usize) -> CubeSym {
CubeSym { perm: (0..n).collect(), flip: vec![false; n] }
}
pub fn map_corner(&self, c: Corner) -> Corner {
let mut out = 0u64;
for j in 0..self.perm.len() {
let mut bit = (c >> j) & 1;
if self.flip[j] {
bit ^= 1;
}
out |= bit << self.perm[j];
}
out
}
pub fn map_subcube(&self, s: &Subcube) -> Subcube {
let mut care = 0u64;
let mut value = 0u64;
for j in 0..self.perm.len() {
if s.care & (1u64 << j) != 0 {
let pj = self.perm[j];
care |= 1u64 << pj;
let mut bit = (s.value >> j) & 1;
if self.flip[j] {
bit ^= 1;
}
value |= bit << pj;
}
}
Subcube { n: s.n, care, value }
}
pub fn map_fractional(&self, point: &[f64]) -> Vec<f64> {
let mut out = vec![0.0; self.perm.len()];
for j in 0..self.perm.len() {
out[self.perm[j]] = if self.flip[j] { 1.0 - point[j] } else { point[j] };
}
out
}
pub fn compose(&self, other: &CubeSym) -> CubeSym {
let n = self.perm.len();
let mut perm = vec![0usize; n];
let mut flip = vec![false; n];
for j in 0..n {
let mid = other.perm[j];
perm[j] = self.perm[mid];
flip[j] = other.flip[j] ^ self.flip[mid];
}
CubeSym { perm, flip }
}
pub fn is_automorphism(&self, cover: &Cover) -> bool {
let original: BTreeSet<Subcube> = cover.blockers.iter().copied().collect();
let mapped: BTreeSet<Subcube> = cover.blockers.iter().map(|b| self.map_subcube(b)).collect();
original == mapped
}
}
fn group_closure(generators: &[CubeSym], n: usize) -> Vec<CubeSym> {
let id = CubeSym::identity(n);
let key = |g: &CubeSym| (g.perm.clone(), g.flip.clone());
let mut seen: BTreeSet<(Vec<usize>, Vec<bool>)> = [key(&id)].into_iter().collect();
let mut group = vec![id];
let mut i = 0;
while i < group.len() {
let g = group[i].clone();
i += 1;
for s in generators {
let h = s.compose(&g);
if seen.insert(key(&h)) {
group.push(h);
}
}
if group.len() > 200_000 {
break;
}
}
group
}
pub fn burnside_corner_orbits(n: usize, generators: &[CubeSym]) -> u64 {
let group = group_closure(generators, n);
let fixed_total: u128 = group
.iter()
.map(|g| (0u64..(1u64 << n)).filter(|&c| g.map_corner(c) == c).count() as u128)
.sum();
(fixed_total / group.len() as u128) as u64
}
pub fn walsh_hadamard_energy(cover: &Cover) -> Vec<f64> {
let size = 1usize << cover.n;
let f: Vec<f64> = (0..size as u64).map(|x| cover.vertex_energy(x) as f64).collect();
(0..size)
.map(|s| {
let acc: f64 = (0..size)
.map(|x| {
if ((s & x) as u64).count_ones() % 2 == 0 { f[x] } else { -f[x] }
})
.sum();
acc / size as f64
})
.collect()
}
pub fn face_vector(cover: &Cover) -> std::collections::BTreeMap<usize, usize> {
let mut fv = std::collections::BTreeMap::new();
for b in &cover.blockers {
*fv.entry(b.dimension()).or_insert(0) += 1;
}
fv
}
pub fn orbit_representatives(n: usize, generators: &[CubeSym]) -> Vec<Corner> {
let total = 1u64 << n;
let mut seen = vec![false; total as usize];
let mut reps = Vec::new();
for start in 0u64..total {
if seen[start as usize] {
continue;
}
reps.push(start);
let mut stack = vec![start];
seen[start as usize] = true;
while let Some(c) = stack.pop() {
for g in generators {
let d = g.map_corner(c);
if !seen[d as usize] {
seen[d as usize] = true;
stack.push(d);
}
}
}
}
reps
}
pub fn orbit_count(n: usize, generators: &[CubeSym]) -> u64 {
orbit_representatives(n, generators).len() as u64
}
pub fn is_total_via_orbits(cover: &Cover, generators: &[CubeSym]) -> Option<bool> {
if !generators.iter().all(|g| g.is_automorphism(cover)) {
return None;
}
let reps = orbit_representatives(cover.n, generators);
Some(reps.iter().all(|&c| cover.blocks(c)))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CollapseStep {
pub generators_used: usize,
pub orbits: u64,
}
pub fn collapse_curve(n: usize, generators: &[CubeSym]) -> Vec<CollapseStep> {
let mut steps = Vec::with_capacity(generators.len() + 1);
steps.push(CollapseStep { generators_used: 0, orbits: 1u64 << n });
for k in 1..=generators.len() {
steps.push(CollapseStep {
generators_used: k,
orbits: orbit_count(n, &generators[..k]),
});
}
steps
}
pub fn php_cover(n: usize) -> Cover {
let (cnf, _) = crate::families::php(n);
Cover::of_cnf(&cnf)
}
pub fn php_symmetries(n: usize) -> Vec<CubeSym> {
let holes = n.saturating_sub(1);
let num_vars = n * holes;
let var = |p: usize, h: usize| p * holes + h;
let mut gens = Vec::new();
for p in 0..n.saturating_sub(1) {
let mut perm: Vec<usize> = (0..num_vars).collect();
for h in 0..holes {
perm.swap(var(p, h), var(p + 1, h));
}
gens.push(CubeSym { perm, flip: vec![false; num_vars] });
}
for h in 0..holes.saturating_sub(1) {
let mut perm: Vec<usize> = (0..num_vars).collect();
for p in 0..n {
perm.swap(var(p, h), var(p, h + 1));
}
gens.push(CubeSym { perm, flip: vec![false; num_vars] });
}
gens
}
pub fn hyperoctahedral_generators(n: usize) -> Vec<CubeSym> {
let mut gens = Vec::new();
for i in 0..n.saturating_sub(1) {
let mut perm: Vec<usize> = (0..n).collect();
perm.swap(i, i + 1);
gens.push(CubeSym { perm, flip: vec![false; n] });
}
if n > 0 {
let mut flip = vec![false; n];
flip[0] = true;
gens.push(CubeSym { perm: (0..n).collect(), flip });
}
gens
}
pub fn cube_group_closure(generators: &[CubeSym], n: usize) -> Vec<CubeSym> {
group_closure(generators, n)
}
pub fn min_resolution_width(cover: &Cover) -> Option<usize> {
let n = cover.n;
let empty = Subcube { n, care: 0, value: 0 };
for w in 0..=n {
let mut set: BTreeSet<Subcube> = cover
.blockers
.iter()
.copied()
.filter(|b| b.care.count_ones() as usize <= w)
.collect();
if set.contains(&empty) {
return Some(w);
}
loop {
let snapshot: Vec<Subcube> = set.iter().copied().collect();
let mut added = false;
for i in 0..snapshot.len() {
for j in (i + 1)..snapshot.len() {
if let Some((_, r)) = snapshot[i].resolve(&snapshot[j]) {
if r.care.count_ones() as usize <= w && set.insert(r) {
added = true;
}
}
}
}
if set.contains(&empty) {
return Some(w);
}
if !added {
break;
}
}
}
None
}
fn cover_key(blockers: &[Subcube]) -> Vec<Subcube> {
let mut k = blockers.to_vec();
k.sort_unstable();
k.dedup();
k
}
fn canonical_key(blockers: &[Subcube], group: &[CubeSym]) -> Vec<Subcube> {
group
.iter()
.map(|g| cover_key(&blockers.iter().map(|b| g.map_subcube(b)).collect::<Vec<_>>()))
.min()
.unwrap_or_else(|| cover_key(blockers))
}
pub fn canonical_cover(cover: &Cover, generators: &[CubeSym]) -> (Vec<Subcube>, usize) {
let group = group_closure(generators, cover.n);
let images: BTreeSet<Vec<Subcube>> = group
.iter()
.map(|g| cover_key(&cover.blockers.iter().map(|b| g.map_subcube(b)).collect::<Vec<_>>()))
.collect();
let best = images.iter().next().cloned().unwrap_or_else(|| cover_key(&cover.blockers));
(best, images.len())
}
pub fn minimal_cover_orbits(n: usize) -> Vec<Cover> {
let generators = hyperoctahedral_generators(n);
let group = group_closure(&generators, n); let mut orbits: HashMap<Vec<Subcube>, Cover> = HashMap::new();
let mut visited: BTreeSet<Vec<Subcube>> = BTreeSet::new();
let mut chosen: Vec<Subcube> = Vec::new();
enumerate_minimal_covers(n, &mut chosen, &group, &mut visited, &mut orbits);
let mut out: Vec<Cover> = orbits.into_values().collect();
out.sort_by(|a, b| cover_key(&a.blockers).cmp(&cover_key(&b.blockers)));
out
}
fn blocker_is_redundant(blockers: &[Subcube], i: usize) -> bool {
blockers[i].footprint().iter().all(|&c| {
blockers
.iter()
.enumerate()
.any(|(j, b)| j != i && b.covers(c))
})
}
fn enumerate_minimal_covers(
n: usize,
chosen: &mut Vec<Subcube>,
group: &[CubeSym],
visited: &mut BTreeSet<Vec<Subcube>>,
orbits: &mut HashMap<Vec<Subcube>, Cover>,
) {
if (0..chosen.len()).any(|i| blocker_is_redundant(chosen, i)) {
return;
}
let canon = canonical_key(chosen, group);
if !visited.insert(canon.clone()) {
return;
}
let cover = Cover { n, blockers: chosen.clone() };
match cover.escaping_corner() {
None => {
orbits.entry(canon).or_insert(cover);
}
Some(c) => {
for care in 1u64..(1u64 << n) {
let blocker = Subcube { n, care, value: c & care };
if !chosen.contains(&blocker) {
chosen.push(blocker);
enumerate_minimal_covers(n, chosen, group, visited, orbits);
chosen.pop();
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cdcl::Lit;
#[test]
fn mod_p_one_hot_instances_land_on_the_modcount_rung_of_the_extended_ladder() {
let (_eqs, cnf, _) = crate::families::mod_p_tseitin_expander(4, 3, 0xC0DE);
assert_eq!(
weakest_crushing_rung(cnf.num_vars, &cnf.clauses, 3),
ProofRung::BeyondBudget,
"legacy: the GF(2) ladder cannot place the mod-3 instance (regression pin)"
);
assert_eq!(
weakest_crushing_rung_with_char(cnf.num_vars, &cnf.clauses, 3, &[3]),
ProofRung::ModCount { p: 3 },
"extended: the characteristic rung fires on the recovered, re-checked GF(3) refutation"
);
assert_eq!(
weakest_crushing_rung_with_char(cnf.num_vars, &cnf.clauses, 3, &[5, 7]),
ProofRung::BeyondBudget,
"the rung is per-prime: without p = 3 enabled the verdict is the legacy one"
);
let (php3, _) = crate::families::php(3);
let mut corpus: Vec<(usize, Vec<Vec<Lit>>)> = vec![(php3.num_vars, php3.clauses)];
for cover in minimal_cover_orbits(2) {
corpus.push((2, cover.clauses()));
}
for cover in minimal_cover_orbits(3).into_iter().take(12) {
corpus.push((3, cover.clauses()));
}
for (nv, clauses) in &corpus {
let legacy = weakest_crushing_rung(*nv, clauses, *nv);
assert_eq!(
weakest_crushing_rung_with_char(*nv, clauses, *nv, &[]),
legacy,
"no primes ⟹ the extended cascade IS the legacy cascade"
);
assert_eq!(
weakest_crushing_rung_with_char(*nv, clauses, *nv, &[3, 5, 7]),
legacy,
"non-one-hot instances are placed identically with the characteristic rungs enabled"
);
}
}
#[test]
fn blocker_is_exactly_the_falsifying_corners() {
let clause = vec![Lit::new(0, true), Lit::new(2, false)];
let b = Subcube::blocker(&clause, 3);
let blocked: BTreeSet<Corner> = b.footprint().into_iter().collect();
let mut expected = BTreeSet::new();
for c in 0u64..8 {
let x0 = c & 1 != 0;
let x2 = c & 4 != 0;
let clause_true = x0 || !x2;
if !clause_true {
expected.insert(c);
}
}
assert_eq!(blocked, expected, "blocker must be the precise falsifying set");
assert_eq!(b.footprint_card(), expected.len() as u64);
assert_eq!(b.dimension(), 1, "3 vars, 2 fixed ⟹ 1 free coordinate");
}
#[test]
fn blocker_dimension_is_codimension_of_clause_width() {
for n in 4..8 {
for w in 1..=4.min(n) {
let clause: Vec<Lit> = (0..w).map(|v| Lit::new(v as u32, v % 2 == 0)).collect();
let b = Subcube::blocker(&clause, n);
assert_eq!(b.dimension(), n - w);
assert_eq!(b.footprint_card(), 1u64 << (n - w));
}
}
}
#[test]
fn cover_is_total_iff_formula_is_unsat() {
for n in 2..=4 {
let cover = php_cover(n);
assert!(cover.is_total(), "PHP({n}) blockers must cover the whole hypercube");
assert_eq!(cover.escaping_corner(), None);
assert_eq!(cover.solution_count(), 0);
}
let sat = DimacsCnf {
num_vars: 3,
clauses: vec![vec![Lit::new(0, true), Lit::new(1, true), Lit::new(2, true)]],
};
let cover = Cover::of_cnf(&sat);
assert!(!cover.is_total());
let model = cover.escaping_corner().expect("a satisfiable cover must leave a corner free");
assert_eq!(cover.vertex_energy(model), 0, "an escaping corner has energy zero");
assert_eq!(cover.blocks(0), true, "the all-false corner is the unique falsifying corner");
assert_eq!(model, 0b001, "the first escaping corner above the blocked all-false corner");
assert_eq!(cover.solution_count(), 7);
}
#[test]
fn vertex_energy_zero_iff_satisfying() {
let cnf = DimacsCnf {
num_vars: 4,
clauses: vec![
vec![Lit::new(0, true), Lit::new(1, false)],
vec![Lit::new(2, true), Lit::new(3, true)],
vec![Lit::new(0, false), Lit::new(2, false)],
],
};
let cover = Cover::of_cnf(&cnf);
for c in 0u64..16 {
let satisfies = cnf.clauses.iter().all(|clause| {
clause.iter().any(|lit| {
let bit = (c >> lit.var() as u64) & 1 != 0;
bit == lit.is_positive()
})
});
assert_eq!(satisfies, cover.vertex_energy(c) == 0, "corner {c:04b}");
}
}
#[test]
fn php_symmetries_are_automorphisms() {
for n in 2..=5 {
let cover = php_cover(n);
for (k, g) in php_symmetries(n).iter().enumerate() {
assert!(g.is_automorphism(&cover), "PHP({n}) generator {k} must be an automorphism");
}
}
}
#[test]
fn symmetry_acts_jointly_on_rules_and_solutions() {
let cnf = DimacsCnf {
num_vars: 3,
clauses: vec![
vec![Lit::new(0, true), Lit::new(2, true)],
vec![Lit::new(1, true), Lit::new(2, true)],
],
};
let cover = Cover::of_cnf(&cnf);
let swap = CubeSym { perm: vec![1, 0, 2], flip: vec![false; 3] };
assert!(swap.is_automorphism(&cover), "swapping x0,x1 preserves the blocker set");
let blk: BTreeSet<Subcube> = cover.blockers.iter().copied().collect();
let mapped: BTreeSet<Subcube> = cover.blockers.iter().map(|b| swap.map_subcube(b)).collect();
assert_eq!(blk, mapped);
let solutions: BTreeSet<Corner> = (0u64..8).filter(|&c| !cover.blocks(c)).collect();
let moved: BTreeSet<Corner> = solutions.iter().map(|&c| swap.map_corner(c)).collect();
assert_eq!(solutions, moved, "the cover symmetry permutes solutions among themselves");
}
#[test]
fn paths_to_random_group_by_the_symmetry_of_the_step() {
let php = crate::families::php(3).0;
let n = php.num_vars;
let generators = crate::symmetry_detect::find_generators(n, &php.clauses);
let mut candidates: Vec<Vec<Lit>> = Vec::new();
for v in 0..n as u32 {
for w in (v + 1)..n as u32 {
for &sv in &[true, false] {
for &sw in &[true, false] {
candidates.push(vec![Lit::new(v, sv), Lit::new(w, sw)]);
}
}
}
}
let orbits = clause_orbits(&candidates, &generators);
assert!(
orbits.len() < candidates.len(),
"{} step-orbits group {} candidate steps",
orbits.len(),
candidates.len()
);
for orbit in &orbits {
let auts: Vec<usize> = orbit
.iter()
.map(|&i| {
let mut f = php.clauses.clone();
f.push(candidates[i].clone());
automorphism_group_size(n, &f)
})
.collect();
assert!(
auts.windows(2).all(|w| w[0] == w[1]),
"same-orbit steps give isomorphic results (identical |Aut|): {auts:?}"
);
}
}
#[test]
fn information_theory_of_the_rigidity_cliff() {
fn sm(s: &mut u64) -> u64 {
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *s;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z ^ (z >> 31)
}
let php = crate::families::php(3).0;
let n = php.num_vars;
let mut clauses = php.clauses.clone();
let mut state = 0x1F0E_0001u64;
let mut bits = Vec::new();
for _ in 0..=4 {
bits.push(symmetry_entropy_bits(n, &clauses));
let mut c: Vec<Lit> = Vec::new();
while c.len() < 2 {
let v = (sm(&mut state) % n as u64) as u32;
if !c.iter().any(|l| l.var() == v) {
c.push(Lit::new(v, sm(&mut state) % 2 == 0));
}
}
clauses.push(c);
}
assert!(bits[0] > 3.0, "PHP(3) carries ~3.58 bits of symmetry: {bits:?}");
assert_eq!(*bits.last().unwrap(), 0.0, "rigid = 0 bits of symmetry (incompressible)");
for w in bits.windows(2) {
assert!(w[1] <= w[0] + 1e-9, "symmetry-information only decreases: {bits:?}");
}
}
#[test]
fn symmetry_bits_are_the_branches_you_can_cut() {
let n = 3;
let cover = php_cover(n); let gens = php_symmetries(n);
let bits = symmetry_entropy_bits(cover.n, &crate::families::php(n).0.clauses);
let orbits = orbit_count(cover.n, &gens);
let full = 1u64 << cover.n;
let reduction = full as f64 / orbits as f64;
assert!(reduction > 1.0, "symmetry cuts branches: {full} corners → {orbits} orbits");
assert!(
reduction.log2() <= bits + 1e-9,
"branch-reduction {:.2} bits ≤ symmetry {bits:.2} bits (orbit-counting bound)",
reduction.log2()
);
}
#[test]
fn the_line_where_symmetry_becomes_rigidity() {
use std::fmt::Write;
fn sm(s: &mut u64) -> u64 {
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *s;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z ^ (z >> 31)
}
let php = crate::families::php(3).0;
let n = php.num_vars;
let mut clauses = php.clauses.clone();
let mut state = 0x11AE_0001u64;
let mut chart = String::from("asymmetric added |Aut|\n");
chart.push_str("---------------- -----\n");
let mut rigid_at = None;
for added in 0..=5 {
let aut = automorphism_group_size(n, &clauses);
let _ = writeln!(chart, "{added:>16} {aut:>5}");
if aut == 1 && rigid_at.is_none() {
rigid_at = Some(added);
}
let mut c: Vec<Lit> = Vec::new();
while c.len() < 2 {
let v = (sm(&mut state) % n as u64) as u32;
if !c.iter().any(|l| l.var() == v) {
c.push(Lit::new(v, sm(&mut state) % 2 == 0));
}
}
clauses.push(c);
}
assert!(automorphism_group_size(n, &php.clauses) >= 6, "the base is symmetric");
assert!(rigid_at.is_some(), "it becomes rigid (|Aut|=1) — the most asymmetric:\n{chart}");
println!("\n{chart}rigid at {rigid_at:?} asymmetric clauses\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("rigidity_line.txt"),
format!("THE LINE — |Aut| as asymmetric clauses are added to PHP(3). Symmetry falls off a cliff to\n1 (rigid = the most asymmetric) within a few clauses: every automorphism must fix every clause,\nso a few generic constraints pin the whole structure. rigid at {rigid_at:?} clauses.\n\n{chart}\n"),
);
}
}
#[test]
fn combinatorics_analysis_geometry_invariants_agree() {
let n = 3;
let cover = php_cover(n); let gens = php_symmetries(n);
let burnside = burnside_corner_orbits(cover.n, &gens);
let walked = orbit_count(cover.n, &gens);
assert_eq!(burnside, walked, "Burnside (fixed points) = orbit walk: {burnside} vs {walked}");
let hat = walsh_hadamard_energy(&cover);
for g in &gens {
for s in 0u64..(1 << cover.n) {
let mut gs = 0u64;
for v in 0..cover.n {
if s & (1 << v) != 0 {
gs |= 1 << g.perm[v];
}
}
assert!(
(hat[s as usize] - hat[gs as usize]).abs() < 1e-9,
"Fourier coefficient constant on the symmetry orbit: S={s:06b} σS={gs:06b}"
);
}
}
let fv = face_vector(&cover);
for g in &gens {
let moved = Cover {
n: cover.n,
blockers: cover.blockers.iter().map(|b| g.map_subcube(b)).collect(),
};
assert_eq!(face_vector(&moved), fv, "the f-vector is a geometric invariant under symmetry");
}
}
#[test]
fn symmetry_collapses_the_cover_check_on_pigeonhole() {
let n = 4; let cover = php_cover(n);
let gens = php_symmetries(n);
let via_orbits =
is_total_via_orbits(&cover, &gens).expect("php symmetries must all be automorphisms");
assert_eq!(via_orbits, cover.is_total(), "orbit verdict must match brute force");
assert!(via_orbits, "PHP(4) is UNSAT ⟹ the cover is total");
let orbits = orbit_count(cover.n, &gens);
let corners = 1u64 << cover.n;
assert!(orbits < corners, "{orbits} orbits must be fewer than {corners} corners");
assert!(orbits * 8 < corners, "expected a >8× collapse, got {orbits} of {corners}");
}
#[test]
fn collapse_curve_is_monotone_and_compounds() {
let n = 4;
let cover = php_cover(n);
let gens = php_symmetries(n);
let curve = collapse_curve(cover.n, &gens);
assert_eq!(curve[0].orbits, 1u64 << cover.n, "starts at 2ⁿ with no symmetry");
for w in curve.windows(2) {
assert!(w[1].orbits <= w[0].orbits, "stacking a generator never grows the orbit count");
}
let first = curve[0].orbits;
let last = curve.last().unwrap().orbits;
assert!(last * 8 < first, "the stacked group collapses the check >8×: {first} → {last}");
}
use crate::sat::UnsatOutcome;
#[test]
fn certified_prover_decides_the_cover_both_ways() {
for n in 2..=4 {
let cover = php_cover(n);
assert!(cover.has_no_hole(), "PHP({n}) cover is total");
assert_eq!(
cover.prove_total_certified(),
UnsatOutcome::Refuted,
"our prover must *certify* the PHP({n}) cover total, not just brute-force it"
);
}
for (n, k) in [(3usize, 2usize), (4, 3)] {
let (cnf, _) = crate::families::clique_coloring(n, k);
let cover = Cover::of_cnf(&cnf);
assert!(cover.has_no_hole(), "K_{n} needs >{k} colors ⟹ total cover");
assert_eq!(cover.prove_total_certified(), UnsatOutcome::Refuted);
}
let sat = DimacsCnf { num_vars: 3, clauses: vec![vec![Lit::new(0, true), Lit::new(1, true)]] };
let cover = Cover::of_cnf(&sat);
assert!(!cover.has_no_hole());
match cover.prove_total_certified() {
UnsatOutcome::Sat(model) => {
let mut corner = 0u64;
for (name, val) in &model {
if *val {
let v: usize = name.trim_start_matches('x').parse().unwrap();
corner |= 1 << v;
}
}
assert_eq!(cover.vertex_energy(corner), 0, "the prover's model is an uncovered corner");
}
other => panic!("expected a model for a satisfiable cover, got {other:?}"),
}
}
#[test]
fn three_clause_is_a_clean_three_bit_blocker() {
let clause = vec![Lit::new(1, true), Lit::new(4, false), Lit::new(6, true)];
let b = Subcube::blocker(&clause, 8);
assert_eq!(b.care.count_ones(), 3, "support of a 3-clause is exactly 3 coordinates");
assert_eq!(b.dimension(), 5, "n − 3 free coordinates (Blocker.freeCoordinates_card)");
let recovered: BTreeSet<(usize, bool)> = b.clause_literals().into_iter().collect();
let expected: BTreeSet<(usize, bool)> = [(1, true), (4, false), (6, true)].into_iter().collect();
assert_eq!(recovered, expected, "blocker ↔ clause is a lossless round-trip");
}
#[test]
fn cube_symmetry_is_a_corner_bijection() {
let s = CubeSym { perm: vec![2, 0, 3, 1], flip: vec![false, true, false, true] };
let images: BTreeSet<Corner> = (0u64..16).map(|c| s.map_corner(c)).collect();
assert_eq!(images.len(), 16, "map_corner is injective ⟹ a bijection on the 2ⁿ corners");
assert_eq!(images, (0u64..16).collect::<BTreeSet<_>>());
}
#[test]
fn separator_iff_no_interaction_crosses_the_cut() {
let cnf = DimacsCnf {
num_vars: 5,
clauses: vec![
vec![Lit::new(0, true), Lit::new(1, true)],
vec![Lit::new(2, true), Lit::new(3, false)],
vec![Lit::new(3, true), Lit::new(4, true)],
],
};
let cover = Cover::of_cnf(&cnf);
for cut in 0u64..(1 << 5) {
let separated = cover.separated_by(cut);
let no_cross = (0..5).all(|i| {
(0..5).all(|j| {
let i_in = cut & (1 << i) != 0;
let j_in = cut & (1 << j) != 0;
!(i_in && !j_in && cover.variable_interaction(i, j))
})
});
assert_eq!(separated, no_cross, "cut {cut:05b}");
}
}
#[test]
fn difficulty_classes_name_the_hole_count() {
let one = DimacsCnf {
num_vars: 3,
clauses: vec![vec![Lit::new(0, true), Lit::new(1, true), Lit::new(2, true)]],
};
let cover = Cover::of_cnf(&one);
assert!(!cover.has_no_hole());
assert!(!cover.has_unique_hole());
assert!(cover.has_at_least_holes(7));
assert!(!cover.has_at_least_holes(8));
let mut clauses = Vec::new();
for forbidden in 0u64..7 {
let lits: Vec<Lit> =
(0..3).map(|v| Lit::new(v as u32, forbidden & (1 << v) == 0)).collect();
clauses.push(lits);
}
let unique = Cover::of_cnf(&DimacsCnf { num_vars: 3, clauses });
assert!(unique.has_unique_hole(), "all corners but 0b111 forbidden ⟹ exactly one hole");
assert_eq!(unique.escaping_corner(), Some(0b111));
assert!(php_cover(3).has_no_hole());
}
#[test]
fn pigeonhole_rules_collapse_to_two_orbits_at_every_scale() {
for n in 2..=12 {
let sig = pigeonhole_rule_symmetry(n);
assert_eq!(
sig.rule_orbits, 2,
"PHP({n}): {} blockers must collapse to 2 rule-orbits, got {}",
sig.blockers, sig.rule_orbits
);
}
let big = pigeonhole_rule_symmetry(12);
assert_eq!(big.n * (big.n - 1), 132, "12 pigeons × 11 holes = 132 variables (2^132 corners)");
assert_eq!(big.rule_orbits, 2);
assert!(big.blockers > 700, "{} blockers — large cover, two essential rules", big.blockers);
}
#[test]
fn the_detector_discovers_the_pigeonhole_rule_symmetry() {
for n in 2..=5 {
let sig = php_cover(n).discovered_rule_symmetry();
assert!(sig.generators >= 1, "PHP({n}): the detector must find the grid symmetry");
assert_eq!(sig.rule_orbits, 2, "PHP({n}): discovered rule-orbits = {}, expected 2", sig.rule_orbits);
}
}
#[test]
fn random_3sat_rules_do_not_collapse_to_a_constant() {
let cnf = crate::families::random_3sat(14, 40, 0xC0FFEE);
let cover = Cover::of_cnf(&cnf);
let sig = cover.discovered_rule_symmetry();
assert!(sig.rule_orbits > 2, "random hardness has no constant-size rule symmetry: {sig:?}");
assert!(
sig.rule_orbits * 2 > sig.blockers,
"random rules barely merge: {} orbits of {} blockers",
sig.rule_orbits,
sig.blockers
);
}
#[test]
fn orbit_rep_engine_refutes_php3_but_does_not_scale() {
let cover = php_cover(3);
let gens = php_symmetries(3);
let (orbit_types, refuted) = symmetric_resolution_closure(&cover, &gens, 40, 40_000);
assert!(refuted, "the orbit-rep engine derives the empty clause for PHP(3)");
assert_eq!(orbit_types, 12, "PHP(3) saturates at 12 orbit-types — the same as the raw closure");
}
#[test]
fn a_blocker_is_one_point_under_its_free_coordinate_symmetry() {
let cover = php_cover(3);
for b in &cover.blockers {
let footprint: BTreeSet<Corner> = b.footprint().into_iter().collect();
let free_bits: Vec<u64> =
(0..b.n as u64).filter(|i| b.care & (1 << i) == 0).collect();
assert_eq!(free_bits.len(), b.dimension(), "free coordinates = the dimension");
let start = *footprint.iter().next().unwrap();
let orbit: BTreeSet<Corner> = (0..(1u64 << b.dimension()))
.map(|subset| {
let mut c = start;
for (j, &fb) in free_bits.iter().enumerate() {
if subset & (1 << j) != 0 {
c ^= 1 << fb;
}
}
c
})
.collect();
assert_eq!(orbit, footprint, "the free-coordinate group orbit = the footprint (many → one)");
let support: Vec<(usize, bool)> = b.clause_literals();
assert_eq!(support.len(), b.care.count_ones() as usize, "the point lives in the support subspace");
}
}
#[test]
fn every_covered_corner_comes_in_a_pair_unless_a_clause_is_full_width() {
let cover = php_cover(3);
for b in &cover.blockers {
let fp = b.footprint_card();
assert!(fp.is_power_of_two() && fp >= 2, "blocker covers a power-of-two ≥ 2 corners: {fp}");
let corners = b.footprint();
for &c in &corners {
assert!(
corners.iter().any(|&c2| (c ^ c2).count_ones() == 1),
"every covered corner has a free-axis partner also covered (covers come in pairs)"
);
}
}
let full = Subcube::blocker(&[Lit::new(0, true), Lit::new(1, true), Lit::new(2, true)], 3);
assert_eq!(full.footprint_card(), 1, "a full-width clause covers exactly one corner — the exception");
assert_eq!(full.dimension(), 0, "its blocker is a 0-dimensional point, no partner");
}
#[test]
fn the_half_center_is_the_symmetry_fixed_point() {
let n = 5;
let center = vec![0.5_f64; n];
let sigma = CubeSym { perm: vec![2, 0, 4, 1, 3], flip: vec![true, false, true, false, true] };
assert_eq!(sigma.map_fractional(¢er), center, "the center is fixed by the symmetry");
for g in php_symmetries(4) {
let c = vec![0.5_f64; 4 * 3];
assert_eq!(g.map_fractional(&c), c, "every pigeonhole automorphism fixes the center");
}
let off = vec![0.1, 0.2, 0.3, 0.4, 0.5];
assert_ne!(sigma.map_fractional(&off), off, "a non-center point is moved");
}
#[test]
fn pigeonhole_is_lp_feasible_at_the_center_yet_integer_unsat() {
for n in 3..=6 {
let cover = php_cover(n.min(8));
assert!(
cover.relaxation_feasible_at_center(),
"PHP({n}) clauses all have width ≥ 2 ⟹ the ½-center is LP-feasible"
);
let cert = cover.counting_refutation().expect("yet it is integer-UNSAT by counting");
assert!(cert.pigeons > cert.holes, "the integer obstruction the ½-center hides");
}
}
#[test]
fn the_cutting_plane_separates_the_symmetric_center() {
let n = 4;
let cover = php_cover(n);
let center = vec![0.5_f64; cover.n];
for b in &cover.blockers {
assert!(b.clause_lp_value(¢er) >= 1.0 - 1e-9, "clause satisfied at the center");
}
let holes = n - 1;
let var = |p: usize, h: usize| p * holes + h;
for h in 0..holes {
let cardinality: f64 = (0..n).map(|p| center[var(p, h)]).sum();
assert!(
cardinality > 1.0 + 1e-9,
"hole {h}: Σ_p x = {cardinality} > 1 — the cutting plane separates the center"
);
}
}
fn mutilated_chessboard(m: usize) -> (usize, Vec<Vec<Lit>>) {
use std::collections::HashMap;
let sq = |r: usize, c: usize| r * m + c;
let removed = |r: usize, c: usize| (r == 0 && c == 0) || (r == m - 1 && c == m - 1);
let color = |r: usize, c: usize| (r + c) % 2;
let neighbors = |r: usize, c: usize| {
let mut v = Vec::new();
if r > 0 { v.push((r - 1, c)); }
if r + 1 < m { v.push((r + 1, c)); }
if c > 0 { v.push((r, c - 1)); }
if c + 1 < m { v.push((r, c + 1)); }
v
};
let mut var_of: HashMap<(usize, usize), u32> = HashMap::new();
for r in 0..m {
for c in 0..m {
if color(r, c) == 1 && !removed(r, c) {
for (nr, nc) in neighbors(r, c) {
if color(nr, nc) == 0 && !removed(nr, nc) {
let next = var_of.len() as u32;
var_of.entry((sq(r, c), sq(nr, nc))).or_insert(next);
}
}
}
}
}
let mut clauses: Vec<Vec<Lit>> = Vec::new();
for r in 0..m {
for c in 0..m {
if color(r, c) == 1 && !removed(r, c) {
let row: Vec<Lit> = neighbors(r, c)
.into_iter()
.filter(|&(nr, nc)| color(nr, nc) == 0 && !removed(nr, nc))
.map(|(nr, nc)| Lit::new(var_of[&(sq(r, c), sq(nr, nc))], true))
.collect();
clauses.push(row);
}
}
}
for r in 0..m {
for c in 0..m {
if color(r, c) == 0 && !removed(r, c) {
let incident: Vec<u32> = neighbors(r, c)
.into_iter()
.filter(|&(nr, nc)| color(nr, nc) == 1 && !removed(nr, nc))
.map(|(nr, nc)| var_of[&(sq(nr, nc), sq(r, c))])
.collect();
for i in 0..incident.len() {
for j in (i + 1)..incident.len() {
clauses.push(vec![Lit::new(incident[i], false), Lit::new(incident[j], false)]);
}
}
}
}
}
(var_of.len(), clauses)
}
fn selected_pigeonholes(a: usize, b: usize) -> (usize, Vec<Vec<Lit>>) {
let (a_holes, b_holes) = (a - 1, b - 1);
let off = 1 + a * a_holes;
let s = Lit::new(0, true);
let var_a = |p: usize, h: usize| Lit::new((1 + p * a_holes + h) as u32, true);
let var_b = |p: usize, h: usize| Lit::new((off + p * b_holes + h) as u32, true);
let mut clauses = Vec::new();
for p in 0..a {
let mut row: Vec<Lit> = (0..a_holes).map(|h| var_a(p, h)).collect();
row.push(s.negated());
clauses.push(row);
}
for h in 0..a_holes {
for p in 0..a {
for q in (p + 1)..a {
clauses.push(vec![var_a(p, h).negated(), var_a(q, h).negated(), s.negated()]);
}
}
}
for p in 0..b {
let mut row: Vec<Lit> = (0..b_holes).map(|h| var_b(p, h)).collect();
row.push(s);
clauses.push(row);
}
for h in 0..b_holes {
for p in 0..b {
for q in (p + 1)..b {
clauses.push(vec![var_b(p, h).negated(), var_b(q, h).negated(), s]);
}
}
}
(off + b * b_holes, clauses)
}
#[test]
fn branching_the_selector_unlocks_the_masked_cut() {
let (num_vars, clauses) = selected_pigeonholes(4, 5);
let e = clauses_to_expr(&clauses).expect("non-empty");
assert!(
!crate::pigeonhole::decide_pigeonhole_unsat(&e),
"the selector masks the bipartite cut at the root"
);
let (sat, stats) = decide_laddered_sym(num_vars, &clauses, true);
assert!(!sat, "selected pigeonholes is UNSAT");
assert!(stats.cut_closures >= 1, "a cut fires after branching the selector: {stats:?}");
assert!(stats.nodes <= 6, "a couple of branches, then crush: {stats:?}");
}
#[test]
fn the_counting_cut_crushes_the_mutilated_chessboard() {
for m in [4usize, 6, 8] {
let (num_vars, clauses) = mutilated_chessboard(m);
let e = clauses_to_expr(&clauses).expect("non-empty board");
assert!(
crate::pigeonhole::decide_pigeonhole_unsat(&e),
"the counting cut crushes the mutilated {m}×{m} board"
);
assert!(
crate::pigeonhole::hall_refutation(&e).is_some(),
"Hall names the over-subscribed majority colour on the {m}×{m} board"
);
let (sat, stats) = decide_laddered(num_vars, &clauses);
assert!(!sat && stats.cut_closures >= 1 && stats.nodes <= 2, "{m}×{m}: {stats:?}");
}
}
#[test]
fn the_full_hall_cut_beats_simple_counting() {
let cover = Cover::of_cnf(&DimacsCnf {
num_vars: 4,
clauses: vec![
vec![Lit::new(0, true)], vec![Lit::new(1, true)], vec![Lit::new(2, true), Lit::new(3, true)], vec![Lit::new(0, false), Lit::new(1, false)], ],
});
assert_eq!(cover.counting_refutation(), None, "items > slots cannot see the subset violation");
let witness = cover.hall_refutation().expect("Hall's theorem refutes the subset");
assert_eq!(witness.items.len(), 2, "two items competing for one slot: {witness:?}");
assert_eq!(witness.slots.len(), 1, "their shared neighborhood is a single slot");
assert_eq!(cover.auto_certify(), CoverVerdict::Total { cut: Some(Shadow::Counting) });
}
#[test]
fn the_counting_crush_generalizes_beyond_pigeonhole() {
let php = Cover::of_cnf(&crate::families::php(5).0);
let pc = php.counting_refutation().expect("pigeonhole crushed");
assert_eq!((pc.pigeons, pc.holes), (5, 4));
let cc = Cover::of_cnf(&crate::families::clique_coloring(4, 3).0);
let ccert = cc.counting_refutation().expect("clique-coloring crushed by the same invariant");
assert!(ccert.pigeons > ccert.holes, "K_4 over 3 colors: {ccert:?}");
assert!(crate::pigeonhole::check_counting_cert(&ccert), "the certificate re-checks");
}
#[test]
fn tight_and_redundant_vertices_identify_the_essential_core() {
let minimal = Cover {
n: 2,
blockers: vec![
Subcube::blocker(&[Lit::new(0, true)], 2), Subcube::blocker(&[Lit::new(0, false)], 2), ],
};
assert!(minimal.is_total(), "the two halves tile the whole cube");
for c in 0u64..4 {
assert!(minimal.is_tight(c), "corner {c} is covered by exactly one blocker");
}
assert_eq!(minimal.essential_blockers(), vec![0, 1], "both halves are essential");
let mut redundant = minimal.clone();
redundant.blockers.push(Subcube::blocker(&[Lit::new(1, true)], 2));
assert!(redundant.is_redundant(0) && redundant.is_redundant(1), "corners 0,1 now overlapped");
assert_eq!(redundant.essential_blockers(), vec![0, 1], "the added blocker joins no core");
}
#[test]
fn laddered_branch_and_cut_crushes_structured_and_brute_forces_the_rest() {
for n in [4usize, 8, 12, 20] {
let (cnf, _) = crate::families::php(n);
let (sat, stats) = decide_laddered(cnf.num_vars, &cnf.clauses);
assert!(!sat, "PHP({n}) is UNSAT");
assert!(
stats.nodes <= 3 && stats.cut_closures >= 1,
"PHP({n}) crushed by a cut at the root: {stats:?}"
);
}
let (_, t, _) = crate::families::tseitin_expander(8, 0x51);
let (tsat, tstats) = decide_laddered(t.num_vars, &t.clauses);
assert!(!tsat && tstats.cut_closures >= 1, "Tseitin crushed by the parity cut: {tstats:?}");
let rnd = crate::families::random_3sat(12, 22, 0xBEEF);
let (sat, _) = decide_laddered(rnd.num_vars, &rnd.clauses);
let e = clauses_to_expr(&rnd.clauses).unwrap();
let prover_sat = !matches!(crate::sat::prove_unsat(&e), crate::sat::UnsatOutcome::Refuted);
assert_eq!(sat, prover_sat, "the ladder agrees with the certified prover on the residual");
}
#[test]
fn symmetry_pruned_ladder_is_sound_against_brute_force() {
for seed in 0..60u64 {
let clauses_n = 14 + (seed % 14) as usize;
let cnf = crate::families::random_3sat(9, clauses_n, seed.wrapping_mul(0x9E37_79B9_7F4A_7C15));
let brute = (0u64..(1 << cnf.num_vars)).any(|c| {
cnf.clauses.iter().all(|cl| {
cl.iter().any(|l| ((c >> l.var()) & 1 != 0) == l.is_positive())
})
});
let (sym_sat, _) = decide_laddered_sym(cnf.num_vars, &cnf.clauses, true);
assert_eq!(sym_sat, brute, "seed {seed}: symmetry-pruned ladder must match brute force");
let (plain_sat, _) = decide_laddered(cnf.num_vars, &cnf.clauses);
assert_eq!(sym_sat, plain_sat, "seed {seed}: pruning must not change the verdict");
let (nocut_sat, _) = decide_laddered_sym(cnf.num_vars, &cnf.clauses, false);
assert_eq!(nocut_sat, brute, "seed {seed}: cut-free symmetry pruning must match brute force");
}
for n in [4usize, 6, 8] {
let (cnf, _) = crate::families::php(n);
let (sat, stats) = decide_laddered_sym(cnf.num_vars, &cnf.clauses, true);
assert!(!sat && stats.cut_closures >= 1, "PHP({n}) crushed by the cut: {stats:?}");
}
}
#[test]
fn symmetry_pruning_collapses_the_search_with_the_cut_off() {
for n in [3usize, 4] {
let (cnf, _) = crate::families::php(n);
let (sat_pruned, pruned_stats) = decide_laddered_sym(cnf.num_vars, &cnf.clauses, false);
let (sat_plain, plain_stats) = decide_laddered_nocut(cnf.num_vars, &cnf.clauses);
assert!(!sat_pruned && !sat_plain, "PHP({n}) is UNSAT either way");
assert!(
pruned_stats.pruned >= 1,
"symmetry pruning must fire on PHP({n}): {pruned_stats:?}"
);
assert!(
pruned_stats.nodes < plain_stats.nodes,
"PHP({n}): pruned search {} nodes < plain {} nodes",
pruned_stats.nodes,
plain_stats.nodes
);
}
}
#[test]
fn symmetry_aware_counting_collapses_the_count() {
let cnf = crate::families::clique_coloring(3, 3).0;
let nv = cnf.num_vars;
let satisfies = |m: &[bool]| {
cnf.clauses.iter().all(|c| c.iter().any(|l| m[l.var() as usize] == l.is_positive()))
};
let models: Vec<Vec<bool>> = (0u64..(1 << nv))
.filter_map(|x| {
let m: Vec<bool> = (0..nv).map(|v| (x >> v) & 1 != 0).collect();
satisfies(&m).then_some(m)
})
.collect();
let total = models.len();
let generators = crate::symmetry_detect::find_generators(nv, &cnf.clauses);
let orbits = partition_into_orbits(&models, &generators);
assert_eq!(orbits.iter().map(|o| o.len()).sum::<usize>(), total, "orbits partition the solutions");
assert!(orbits.len() < total, "{} orbits ≪ {} solutions — symmetry collapses the count", orbits.len(), total);
for orbit in &orbits {
assert_eq!(model_orbit(&orbit[0], &generators).len(), orbit.len(), "each orbit reconstructs from its rep");
}
}
#[test]
fn renamable_horn_is_a_new_symmetry_for_a_new_class() {
let cl = vec![
vec![Lit::new(0, true), Lit::new(1, true)],
vec![Lit::new(0, true), Lit::new(2, false)],
];
let flips = renaming_to_horn(3, &cl).expect("this formula is renamable to Horn");
let renamed = apply_renaming(&cl, &flips);
for c in &renamed {
assert!(
c.iter().filter(|l| l.is_positive()).count() <= 1,
"after the flip-renaming every clause is Horn: {c:?}"
);
}
fn sm(s: &mut u64) -> u64 {
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *s;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z ^ (z >> 31)
}
let mut state = 0x8077_0001u64;
let mut renamable_seen = 0;
for _ in 0..80 {
let nv = 3 + (sm(&mut state) % 4) as usize;
let m = 2 + (sm(&mut state) % 6) as usize;
let mut clauses: Vec<Vec<Lit>> = Vec::new();
for _ in 0..m {
let mut c = Vec::new();
for var in 0..nv {
if sm(&mut state) % 2 == 0 {
c.push(Lit::new(var as u32, sm(&mut state) % 2 == 0));
}
}
if !c.is_empty() {
clauses.push(c);
}
}
if clauses.is_empty() {
continue;
}
let sat = |cs: &[Vec<Lit>]| {
(0u64..(1u64 << nv)).any(|x| {
cs.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
})
};
if let Some(flips) = renaming_to_horn(nv, &clauses) {
renamable_seen += 1;
let renamed = apply_renaming(&clauses, &flips);
assert!(
renamed.iter().all(|c| c.iter().filter(|l| l.is_positive()).count() <= 1),
"a found renaming must yield a Horn formula"
);
assert_eq!(sat(&clauses), sat(&renamed), "the flip-renaming preserves satisfiability");
}
}
assert!(renamable_seen > 0, "the fuzz should hit renamable-Horn instances");
}
#[test]
fn symmetry_generates_the_solution_orbit_from_one_model() {
let cnf = crate::families::clique_coloring(3, 3).0;
let nv = cnf.num_vars;
let satisfies = |m: &[bool]| {
cnf.clauses.iter().all(|c| c.iter().any(|l| m[l.var() as usize] == l.is_positive()))
};
let model: Vec<bool> = (0u64..(1 << nv))
.find_map(|x| {
let m: Vec<bool> = (0..nv).map(|v| (x >> v) & 1 != 0).collect();
satisfies(&m).then_some(m)
})
.expect("K₃ is 3-colourable");
let generators = crate::symmetry_detect::find_generators(nv, &cnf.clauses);
let orbit = model_orbit(&model, &generators);
assert!(orbit.len() > 1, "symmetry must generate more than one solution, got {}", orbit.len());
for m in &orbit {
assert!(satisfies(m), "every symmetric image of a model is a model: {m:?}");
}
let all_models = (0u64..(1 << nv))
.filter(|&x| satisfies(&(0..nv).map(|v| (x >> v) & 1 != 0).collect::<Vec<_>>()))
.count();
assert!(orbit.len() <= all_models, "the orbit cannot exceed the model count");
}
#[test]
#[ignore = "timing benchmark"]
fn symmetry_compression_flattens_the_time_to_constant() {
use std::fmt::Write;
use std::time::Instant;
let mut chart = String::from("pigeons symbolic cert\n");
chart.push_str("------------------- -------------\n");
for &n in &[4u128, 64, 10_000, 1_000_000_000, (1u128 << 63), u128::MAX] {
let reps = 2_000_000u32;
let t = Instant::now();
let mut last = None;
for _ in 0..reps {
last = crate::pigeonhole::certify_pigeonhole_unsat(std::hint::black_box(n), n - 1);
}
let ns = t.elapsed().as_secs_f64() * 1e9 / reps as f64;
assert!(last.is_some(), "PHP({n}) is refuted by the symbolic cert");
let _ = writeln!(chart, "{n:<19} {ns:>8.3} ns");
}
println!("\n{chart}");
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("symmetry_compression_flat_time.txt"),
format!("SYMMETRY-COMPRESSION FLAT TIME — pigeonhole on its orbit-type quotient is two rule-types\nand a count, decided in O(1). Constant nanoseconds from n=4 to n=2^128, where the CNF could\nnever be built. The clause-level cut is linear in the input; the quotient cut is flat.\n\n{chart}\n"),
);
}
}
#[test]
#[ignore = "measurement"]
fn the_asymmetry_is_the_hardness_knob() {
use std::fmt::Write;
fn sm(s: &mut u64) -> u64 {
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *s;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z ^ (z >> 31)
}
let (php, _) = crate::families::php(5);
let nv = php.num_vars;
let mut state = 0x4D55_0001u64;
let mut chart = String::from("asymmetry(k) verdict nodes punches\n");
chart.push_str("------------ ----------- ----- -------\n");
for &k in &[0usize, 1, 2, 3, 4] {
let mut clauses = php.clauses.clone();
for _ in 0..k {
let mut c: Vec<Lit> = Vec::new();
while c.len() < 3 {
let v = (sm(&mut state) % nv as u64) as u32;
if !c.iter().any(|l| l.var() == v) {
c.push(Lit::new(v, sm(&mut state) % 2 == 0));
}
}
clauses.push(c);
}
let (verdict, stats) = autocarve_measured(nv, &clauses, 500_000);
let _ = writeln!(chart, "{k:>12} {:<11} {:>5} {:>7}", format!("{verdict:?}"), stats.nodes, stats.punches);
}
println!("\n{chart}");
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("asymmetry_is_the_knob.txt"),
format!("THE ASYMMETRY IS THE HARDNESS KNOB — PHP(5) + k asymmetric clauses. Autocarve crushes the\nsymmetric core but branches the perturbation, so cost grows with k (distance from symmetric),\nnot with n. Near-symmetric is near-easy.\n\n{chart}\n"),
);
}
}
#[test]
#[ignore = "timing benchmark"]
fn autocarve_timings_and_punches() {
use std::fmt::Write;
use std::time::Instant;
let mut chart = String::from("instance vars verdict punches nodes time\n");
chart.push_str("-------------------- ---- ------- ------- ------ ---------\n");
let mut row = |name: String, nv: usize, cl: &[Vec<Lit>]| {
let t = Instant::now();
let mut last = (None, CarveStats::default());
let reps = 200;
for _ in 0..reps {
last = autocarve_measured(nv, cl, 2_000_000);
}
let us = t.elapsed().as_secs_f64() * 1e6 / reps as f64;
let (verdict, stats) = last;
let _ = writeln!(
chart,
"{name:<20} {nv:>4} {:<7} {:>7} {:>6} {:>7.2}µs",
format!("{verdict:?}"),
stats.punches,
stats.nodes,
us
);
};
for n in 4..=8 {
let (cnf, _) = crate::families::php(n);
row(format!("pigeonhole({n})"), cnf.num_vars, &cnf.clauses);
}
for m in [4usize, 6, 8] {
let (nv, cl) = mutilated_chessboard(m);
row(format!("mutilated({m}x{m})"), nv, &cl);
}
let (sel_nv, sel_cl) = selected_pigeonholes(4, 5);
row("masked-php(4,5)".to_string(), sel_nv, &sel_cl);
let (_, t, _) = crate::families::tseitin_expander(10, 0x9);
row("tseitin(10)".to_string(), t.num_vars, &t.clauses);
println!("\n{chart}");
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("autocarve_timings.txt"),
format!("AUTOCARVE TIMINGS — recursive carve→decompose→cut→branch, per instance.\nPunches = certified cuts that fired; the cut is polynomial so time stays flat as n grows.\n\n{chart}\n"),
);
}
}
#[test]
fn autocarving_lets_the_rules_fall_out() {
let (sel_nv, sel_cl) = selected_pigeonholes(4, 5);
assert_eq!(autocarve(sel_nv, &sel_cl, 200_000), Some(false), "masked pigeonhole falls out under autocarve");
let (a_nv, a_cl) = selected_pigeonholes(4, 4);
let s2 = a_nv as u32; let mut nested: Vec<Vec<Lit>> = Vec::new();
for c in &a_cl {
let mut c2 = c.clone();
c2.push(Lit::new(s2, false)); nested.push(c2);
}
let (b, _) = crate::families::php(3);
let off = s2 + 1;
for c in &b.clauses {
let mut c2: Vec<Lit> = c.iter().map(|l| Lit::new(l.var() + off, l.is_positive())).collect();
c2.push(Lit::new(s2, true)); nested.push(c2);
}
let nested_nv = (off + b.num_vars as u32) as usize;
assert_eq!(autocarve(nested_nv, &nested, 500_000), Some(false), "nested masked pigeonholes fall out");
fn sm(s: &mut u64) -> u64 {
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *s;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z ^ (z >> 31)
}
let mut state = 0xFA11_0007u64;
for _ in 0..60 {
let nv = 4 + (sm(&mut state) % 4) as usize;
let m = 3 + (sm(&mut state) % 8) as usize;
let mut cl: Vec<Vec<Lit>> = Vec::new();
for _ in 0..m {
let mut c = Vec::new();
for var in 0..nv {
if sm(&mut state) % 3 == 0 {
c.push(Lit::new(var as u32, sm(&mut state) % 2 == 0));
}
}
if !c.is_empty() {
cl.push(c);
}
}
if cl.is_empty() {
continue;
}
let brute = (0u64..(1u64 << nv)).any(|x| {
cl.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
});
if let Some(sat) = autocarve(nv, &cl, 1_000_000) {
assert_eq!(sat, brute, "autocarve must match brute force: {cl:?}");
}
}
}
#[test]
fn the_unified_crush_pipeline_composes_every_lever() {
let (php, _) = crate::families::php(4);
let mut clauses = php.clauses.clone();
let v = php.num_vars as u32;
clauses.push(vec![Lit::new(v, true), Lit::new(0, true)]); clauses.push(vec![Lit::new(v + 1, true), Lit::new(v + 2, false)]); clauses.push(vec![Lit::new(v + 1, false), Lit::new(v + 2, true)]);
let num_vars = (v + 3) as usize;
assert_eq!(crush(num_vars, &clauses, 200_000), Some(false), "the pipeline crushes the composite");
fn sm(s: &mut u64) -> u64 {
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *s;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z ^ (z >> 31)
}
let mut state = 0xC0DE_9999u64;
for _ in 0..60 {
let nv = 4 + (sm(&mut state) % 4) as usize;
let m = 3 + (sm(&mut state) % 8) as usize;
let mut cl: Vec<Vec<Lit>> = Vec::new();
for _ in 0..m {
let mut c = Vec::new();
for var in 0..nv {
if sm(&mut state) % 3 == 0 {
c.push(Lit::new(var as u32, sm(&mut state) % 2 == 0));
}
}
if !c.is_empty() {
cl.push(c);
}
}
if cl.is_empty() {
continue;
}
let brute = (0u64..(1u64 << nv)).any(|x| {
cl.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
});
if let Some(sat) = crush(nv, &cl, 1_000_000) {
assert_eq!(sat, brute, "crush must match brute force: {cl:?}");
}
}
}
#[test]
fn variable_elimination_carves_a_dimension_soundly() {
let cl = vec![
vec![Lit::new(0, true), Lit::new(1, true)],
vec![Lit::new(0, false), Lit::new(2, true)],
];
let projected = eliminate_variable(0, &cl);
assert!(
projected.iter().all(|c| c.iter().all(|l| l.var() != 0)),
"the a-axis is carved away: {projected:?}"
);
assert!(
projected.iter().any(|c| {
let s: std::collections::BTreeSet<u32> = c.iter().map(|l| l.var()).collect();
s == [1u32, 2].into_iter().collect()
}),
"the resolvent (b ∨ c) survives the projection"
);
fn sm(s: &mut u64) -> u64 {
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *s;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z ^ (z >> 31)
}
let mut state = 0xD1AE_0001u64;
for _ in 0..60 {
let nv = 4 + (sm(&mut state) % 4) as usize;
let m = 3 + (sm(&mut state) % 8) as usize;
let mut cl: Vec<Vec<Lit>> = Vec::new();
for _ in 0..m {
let mut c = Vec::new();
for var in 0..nv {
if sm(&mut state) % 3 == 0 {
c.push(Lit::new(var as u32, sm(&mut state) % 2 == 0));
}
}
if !c.is_empty() {
cl.push(c);
}
}
if cl.is_empty() {
continue;
}
let sat = |clauses: &[Vec<Lit>]| {
(0u64..(1u64 << nv)).any(|x| {
clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
})
};
let brute = sat(&cl);
assert_eq!(brute, sat(&eliminate_variable(0, &cl)), "single elimination preserves SAT: {cl:?}");
assert_eq!(brute, sat(&bounded_variable_elimination(nv, &cl)), "bounded VE preserves SAT: {cl:?}");
}
}
#[test]
fn carve_peels_the_hypercube_to_a_verdict_or_the_core() {
let unsat = vec![
vec![Lit::new(0, true)],
vec![Lit::new(0, false), Lit::new(1, true)],
vec![Lit::new(1, false)],
];
assert_eq!(carve(2, &unsat), CarveOutcome::Unsat);
let (php, _) = crate::families::php(4);
assert!(
matches!(carve(php.num_vars, &php.clauses), CarveOutcome::Core { .. }),
"pigeonhole carves to its irreducible core"
);
fn sm(s: &mut u64) -> u64 {
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *s;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z ^ (z >> 31)
}
let mut state = 0xCA47_E000u64;
for _ in 0..60 {
let nv = 4 + (sm(&mut state) % 4) as usize;
let m = 3 + (sm(&mut state) % 8) as usize;
let mut cl: Vec<Vec<Lit>> = Vec::new();
for _ in 0..m {
let mut c = Vec::new();
for var in 0..nv {
if sm(&mut state) % 3 == 0 {
c.push(Lit::new(var as u32, sm(&mut state) % 2 == 0));
}
}
if !c.is_empty() {
cl.push(c);
}
}
if cl.is_empty() {
continue;
}
let sat = |clauses: &[Vec<Lit>]| {
(0u64..(1u64 << nv)).any(|x| {
clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
})
};
let brute = sat(&cl);
match carve(nv, &cl) {
CarveOutcome::Sat => assert!(brute, "carve says SAT: {cl:?}"),
CarveOutcome::Unsat => assert!(!brute, "carve says UNSAT: {cl:?}"),
CarveOutcome::Core { clauses, .. } => {
assert_eq!(brute, sat(&clauses), "the carved core must preserve SAT: {cl:?}")
}
}
}
}
#[test]
fn pure_literal_autarky_cuts_sections_and_keeps_the_core() {
let (php, _) = crate::families::php(4);
let (core, assigned) = pure_literal_reduce(php.num_vars, &php.clauses);
assert!(assigned.is_empty(), "pigeonhole has no pure literals");
assert_eq!(core.len(), php.clauses.len(), "the hard core survives untouched");
let easy = vec![
vec![Lit::new(0, true), Lit::new(1, true)],
vec![Lit::new(1, true), Lit::new(2, true)],
];
let (core_easy, _) = pure_literal_reduce(3, &easy);
assert!(core_easy.is_empty(), "an all-positive formula reduces to empty — SAT, no section left");
let mut wrapped = php.clauses.clone();
let shell = php.num_vars as u32;
wrapped.push(vec![Lit::new(shell, true), Lit::new(0, true)]); let (core_w, assigned_w) = pure_literal_reduce(php.num_vars + 1, &wrapped);
assert!(!assigned_w.is_empty(), "the shell's pure literal is cut away");
let e = clauses_to_expr(&core_w).expect("non-empty core");
assert!(crate::pigeonhole::decide_pigeonhole_unsat(&e), "the surviving pigeonhole core is crushed");
fn sm(s: &mut u64) -> u64 {
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *s;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z ^ (z >> 31)
}
let mut state = 0xA17A_4321u64;
for _ in 0..60 {
let nv = 4 + (sm(&mut state) % 4) as usize;
let m = 3 + (sm(&mut state) % 7) as usize;
let mut cl: Vec<Vec<Lit>> = Vec::new();
for _ in 0..m {
let mut c = Vec::new();
for v in 0..nv {
if sm(&mut state) % 3 == 0 {
c.push(Lit::new(v as u32, sm(&mut state) % 2 == 0));
}
}
if !c.is_empty() {
cl.push(c);
}
}
if cl.is_empty() {
continue;
}
let sat = |clauses: &[Vec<Lit>]| {
(0u64..(1u64 << nv)).any(|x| {
clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
})
};
let (reduced, _) = pure_literal_reduce(nv, &cl);
assert_eq!(sat(&cl), sat(&reduced), "pure-literal reduction must preserve SAT: {cl:?}");
}
}
#[test]
fn component_decomposition_unlocks_a_buried_cut() {
let (php, _) = crate::families::php(4);
let php_vars = php.num_vars as u32;
let mut clauses: Vec<Vec<Lit>> = php.clauses.clone();
let rnd = crate::families::random_3sat(10, 18, 0xD00D);
for c in &rnd.clauses {
clauses.push(c.iter().map(|l| Lit::new(l.var() + php_vars, l.is_positive())).collect());
}
let num_vars = php_vars as usize + rnd.num_vars;
let e = clauses_to_expr(&clauses).expect("non-empty");
assert!(
!crate::pigeonhole::decide_pigeonhole_unsat(&e)
&& !crate::xorsat::refute_via_parity(&e)
&& !crate::pseudo_boolean::refute_clausal(&e),
"the monolithic mixed formula is recognized by no cut"
);
assert!(decompose_and_crush(num_vars, &clauses), "decomposition refutes the union");
assert_eq!(components(num_vars, &clauses).len(), 2, "pigeonhole ⊔ random = two components");
}
#[test]
fn the_antipodal_map_is_the_center_inversion() {
let n = 5;
let antipode = CubeSym { perm: (0..n).collect(), flip: vec![true; n] };
assert_eq!(antipode.map_fractional(&vec![0.5; n]), vec![0.5; n], "center-inversion fixes ½");
let edges = [(0u32, 1u32), (1, 2), (2, 3), (3, 0)];
let mut c4 = Vec::new();
for (u, v) in edges {
c4.push(vec![Lit::new(u, true), Lit::new(v, true)]);
c4.push(vec![Lit::new(u, false), Lit::new(v, false)]);
}
assert!(is_antipodally_symmetric(&c4), "2-colouring an even cycle is self-complementary");
assert!(!is_antipodally_symmetric(&crate::families::php(4).0.clauses), "pigeonhole is not");
}
#[test]
fn recursive_antipodal_breaking_is_sound_and_collapses_blocks() {
let blocks = |k: usize| {
let mut cl = Vec::new();
for i in 0..k {
let (a, b) = (2 * i as u32, 2 * i as u32 + 1);
cl.push(vec![Lit::new(a, true), Lit::new(b, true)]);
cl.push(vec![Lit::new(a, false), Lit::new(b, false)]);
}
(2 * k, cl)
};
for k in 2..=6 {
let (nv, cl) = blocks(k);
let anti = search_cost_antipodal(nv, &cl, 1_000_000);
let plain = search_cost(nv, &cl, false, 1_000_000);
assert!(matches!(anti, SearchCost::Decided { sat: true, .. }), "blocks are SAT: {anti:?}");
let (an, pn) = (
match anti { SearchCost::Decided { nodes, .. } => nodes, _ => usize::MAX },
match plain { SearchCost::Decided { nodes, .. } => nodes, _ => usize::MAX },
);
assert!(an <= pn, "k={k}: antipodal {an} ≤ plain {pn} nodes");
}
fn sm(s: &mut u64) -> u64 {
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *s;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z ^ (z >> 31)
}
let mut state = 0x5EED_1234u64;
for _ in 0..50 {
let nv = 4 + (sm(&mut state) % 4) as usize;
let m = 3 + (sm(&mut state) % 8) as usize;
let mut cl: Vec<Vec<Lit>> = Vec::new();
for _ in 0..m {
let mut c = Vec::new();
for v in 0..nv {
if sm(&mut state) % 3 == 0 {
c.push(Lit::new(v as u32, sm(&mut state) % 2 == 0));
}
}
if !c.is_empty() {
cl.push(c);
}
}
if cl.is_empty() {
continue;
}
let brute = (0u64..(1u64 << nv)).any(|x| {
cl.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
});
let anti = search_cost_antipodal(nv, &cl, 1_000_000);
assert!(
matches!(anti, SearchCost::Decided { sat, .. } if sat == brute),
"antipodal search must match brute force: {anti:?} vs {brute}"
);
}
}
#[test]
fn the_exponential_gap_measured_and_banked() {
use std::fmt::Write;
let budget = 400_000usize;
let cost = |c: SearchCost| match c {
SearchCost::Decided { nodes, .. } => nodes,
SearchCost::Exceeded { budget } => budget,
};
let mut chart = String::from(" n vars cut nodes no-cut nodes (resolution)\n");
chart.push_str("-- ----- --------- -------------------------\n");
let mut nocut_curve = Vec::new();
for n in 2..=8 {
let (cnf, _) = crate::families::php(n);
let cut = search_cost(cnf.num_vars, &cnf.clauses, true, budget);
let nocut = search_cost(cnf.num_vars, &cnf.clauses, false, budget);
assert!(
matches!(cut, SearchCost::Decided { nodes, .. } if nodes <= 2),
"PHP({n}): cut must close in O(1) nodes, got {cut:?}"
);
let nc = cost(nocut);
nocut_curve.push(nc);
let nocut_str = if matches!(nocut, SearchCost::Exceeded { .. }) {
format!("≥{budget} (exploded)")
} else {
format!("{nc}")
};
let _ = writeln!(chart, "{n:>2} {:>5} {:>9} {nocut_str}", cnf.num_vars, cost(cut));
}
assert!(nocut_curve.windows(2).all(|w| w[1] >= w[0]), "raw search grows monotonically: {nocut_curve:?}");
assert!(*nocut_curve.last().unwrap() >= 100_000, "PHP(8) raw search is vast vs the cut's 1 node: {nocut_curve:?}");
assert!(nocut_curve[4] >= 1000, "the gap to the cut's single node is already vast by PHP(6): {nocut_curve:?}");
println!("\n{chart}");
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("exponential_gap.txt"),
format!("EXPONENTIAL GAP — same branch engine, certified cut ON vs OFF (raw resolution).\nThe counting cut closes pigeonhole at the root in ONE node at every scale; raw resolution\ngrows exponentially and explodes past {budget} nodes.\n\n{chart}\n"),
);
}
}
#[test]
fn auto_cut_classifies_and_crushes_every_family() {
use CoverVerdict::Total;
let php = Cover::of_cnf(&crate::families::php(5).0);
assert_eq!(php.auto_certify(), Total { cut: Some(Shadow::Counting) });
let cc = Cover::of_cnf(&crate::families::clique_coloring(4, 3).0);
assert_eq!(cc.auto_certify(), Total { cut: Some(Shadow::Counting) });
let (_, t, _) = crate::families::tseitin_expander(8, 0x51);
assert_eq!(Cover::of_cnf(&t).auto_certify(), Total { cut: Some(Shadow::Parity) });
let sat = DimacsCnf { num_vars: 3, clauses: vec![vec![Lit::new(0, true), Lit::new(1, true)]] };
assert_eq!(Cover::of_cnf(&sat).auto_certify(), CoverVerdict::Escapes);
}
#[test]
fn measuring_randomness_the_quotient_climbs_as_structure_decays() {
use std::fmt::Write;
fn sm(s: &mut u64) -> u64 {
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *s;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z ^ (z >> 31)
}
let (php, _) = crate::families::php(5);
let nv = php.num_vars;
let mut state = 0x4A22_0001u64;
let mut chart = String::from("injected clauses quotient ratio\n");
chart.push_str("-------- ------- -------- -----\n");
let mut ratios = Vec::new();
for &k in &[0usize, 4, 8, 16, 32] {
let mut clauses = php.clauses.clone();
for _ in 0..k {
let mut c: Vec<Lit> = Vec::new();
while c.len() < 3 {
let v = (sm(&mut state) % nv as u64) as u32;
if !c.iter().any(|l| l.var() == v) {
c.push(Lit::new(v, sm(&mut state) % 2 == 0));
}
}
clauses.push(c);
}
let generators = crate::symmetry_detect::find_generators(nv, &clauses);
let quotient = clause_orbits(&clauses, &generators).len();
let ratio = quotient as f64 / clauses.len() as f64;
ratios.push(ratio);
let _ = writeln!(chart, "{k:>8} {:>7} {quotient:>8} {ratio:.3}", clauses.len());
}
assert!(ratios[0] < 0.15, "pristine pigeonhole is highly compressible: {}", ratios[0]);
assert!(ratios[1] > 0.9, "just four random clauses annihilate the symmetry (cliff): {ratios:?}");
println!("\n{chart}");
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("randomness_measure.txt"),
format!("MEASURING RANDOMNESS — a symmetry is a compression, so quotient-size (orbit-types ÷\nclauses) measures incompressibility (computable shadow of Kolmogorov complexity). Pigeonhole\nis maximally compressible; injecting random clauses erodes the symmetry and the quotient climbs\ntoward 1 — ordered → random as a continuous gradient.\n\n{chart}\n"),
);
}
}
#[test]
fn asymmetry_not_randomness_annihilates_the_structure() {
use crate::symmetry_detect::{clause_key, find_generators};
let php = crate::families::php(3).0;
let nv = php.num_vars;
let quotient = |cl: &[Vec<Lit>]| clause_orbits(cl, &find_generators(nv, cl)).len();
let base = quotient(&php.clauses);
let seed = vec![Lit::new(0, false), Lit::new(3, false)]; let mut broken = php.clauses.clone();
broken.push(seed.clone());
assert!(quotient(&broken) > base, "one asymmetric clause breaks the symmetry");
let generators = php_perm_symmetries(3);
let mut seen: BTreeSet<Vec<u32>> = [clause_key(&seed)].into_iter().collect();
let mut orbit = vec![seed.clone()];
let mut stack = vec![seed.clone()];
while let Some(c) = stack.pop() {
for g in &generators {
let img = g.apply_clause(&c);
if seen.insert(clause_key(&img)) {
orbit.push(img.clone());
stack.push(img);
}
}
}
let mut symmetrized = php.clauses.clone();
symmetrized.extend(orbit.iter().cloned());
assert!(
quotient(&symmetrized) <= base + 1,
"the SAME clause, symmetrized ({} added), preserves the structure",
orbit.len()
);
}
#[test]
fn pseudorandom_is_kolmogorov_simple_but_symmetry_blind() {
let a = crate::families::random_3sat(14, 50, 0x00AB_CDEF);
let b = crate::families::random_3sat(14, 50, 0x00AB_CDEF);
assert_eq!(a.clauses, b.clauses, "same seed ⟹ byte-identical: Kolmogorov complexity ≤ the seed");
let generators = crate::symmetry_detect::find_generators(a.num_vars, &a.clauses);
let quotient = clause_orbits(&a.clauses, &generators).len();
assert!(
quotient * 2 > a.clauses.len(),
"symmetry-blind: quotient {quotient} near the {} clauses, despite being seed-simple",
a.clauses.len()
);
let c = crate::families::random_3sat(14, 50, 0x00AB_CDF0);
assert_ne!(a.clauses, c.clauses, "a different seed is a different object — the seed is the structure");
}
#[test]
fn breaking_the_symmetry_recovers_the_re_checkable_witness() {
let php = crate::families::php(5).0;
let e = clauses_to_expr(&php.clauses).expect("non-empty");
let cert = crate::pigeonhole::counting_certificate(&e).expect("the counting break fires");
assert!(crate::pigeonhole::check_counting_cert(&cert), "the recovered refutation witness re-checks: {cert:?}");
let hall = crate::pigeonhole::hall_refutation(&e).expect("the Hall break fires");
assert!(!hall.items.is_empty(), "the Hall break names the violating subset (a witness)");
let cc = crate::families::clique_coloring(3, 3).0;
let nv = cc.num_vars;
let satisfies = |m: &[bool]| {
cc.clauses.iter().all(|c| c.iter().any(|l| m[l.var() as usize] == l.is_positive()))
};
let one_model: Vec<bool> = (0u64..(1 << nv))
.find_map(|x| {
let m: Vec<bool> = (0..nv).map(|v| (x >> v) & 1 != 0).collect();
satisfies(&m).then_some(m)
})
.expect("clique_coloring(3,3) is SAT");
let generators = crate::symmetry_detect::find_generators(nv, &cc.clauses);
let witnesses = model_orbit(&one_model, &generators);
assert!(witnesses.len() > 1, "the symmetry recovers many witnesses from one");
for w in &witnesses {
assert!(satisfies(w), "every recovered witness is a genuine model");
}
}
#[test]
fn symmetry_break_the_witness_to_its_canonical_representative() {
let cc = crate::families::clique_coloring(3, 3).0;
let nv = cc.num_vars;
let satisfies =
|m: &[bool]| cc.clauses.iter().all(|c| c.iter().any(|l| m[l.var() as usize] == l.is_positive()));
let models: Vec<Vec<bool>> = (0u64..(1 << nv))
.filter_map(|x| {
let m: Vec<bool> = (0..nv).map(|v| (x >> v) & 1 != 0).collect();
satisfies(&m).then_some(m)
})
.collect();
let generators = crate::symmetry_detect::find_generators(nv, &cc.clauses);
for m in &models {
let canon = canonical_model(m, &generators);
for sib in model_orbit(m, &generators) {
assert_eq!(canonical_model(&sib, &generators), canon, "orbit-mates share a canonical witness");
}
assert!(satisfies(&canon), "the canonical witness is itself a genuine model");
assert!(canon <= *m, "the canonical witness is the lex-least of its orbit");
}
let canonicals: BTreeSet<Vec<bool>> =
models.iter().map(|m| canonical_model(m, &generators)).collect();
let orbits = partition_into_orbits(&models, &generators);
assert_eq!(canonicals.len(), orbits.len(), "one canonical witness per orbit");
assert!(canonicals.len() < models.len(), "the symmetry genuinely compressed the witness set");
}
#[test]
fn symmetry_break_across_the_witnesss_perspective_of_other_witnesses() {
let cc = crate::families::clique_coloring(3, 3).0;
let nv = cc.num_vars;
let satisfies =
|m: &[bool]| cc.clauses.iter().all(|c| c.iter().any(|l| m[l.var() as usize] == l.is_positive()));
let models: Vec<Vec<bool>> = (0u64..(1 << nv))
.filter_map(|x| {
let m: Vec<bool> = (0..nv).map(|v| (x >> v) & 1 != 0).collect();
satisfies(&m).then_some(m)
})
.collect();
let generators = crate::symmetry_detect::find_generators(nv, &cc.clauses);
let group = perm_group_closure(&generators, nv);
assert!(group.len() > 1, "clique_coloring(3,3) has a nontrivial symmetry group");
let mut saw_redundant_perspective = false;
for m in &models {
let persp = witness_perspective(m, &generators, nv);
let orbit = model_orbit(m, &generators);
let stab = stabilizer(m, &group);
assert_eq!(group.len(), orbit.len() * stab.len(), "orbit-stabilizer holds from m's frame");
assert_eq!(persp.len(), orbit.len(), "one representative transformation per distinct witness");
if stab.len() > 1 {
saw_redundant_perspective = true; }
assert_eq!(persp[0].0, *m, "the witness sees itself first");
assert!(persp[0].1.is_identity(), "it sees itself through the identity");
let mut destinations = BTreeSet::new();
for (dest, sigma) in &persp {
assert_eq!(apply_perm_to_model(sigma, m), *dest, "σ·m is the witness it claims");
assert!(satisfies(dest), "every witness in the perspective is a genuine model");
assert!(destinations.insert(dest.clone()), "no witness is named twice — redundancy is gone");
}
assert_eq!(destinations, orbit.iter().cloned().collect(), "the perspective covers the whole orbit");
}
assert!(saw_redundant_perspective, "at least one witness had a nontrivial stabilizer to break");
}
#[test]
fn burnside_counts_the_essentially_distinct_witnesses() {
let check = |nv: usize, clauses: &[Vec<Lit>]| {
let satisfies =
|m: &[bool]| clauses.iter().all(|c| c.iter().any(|l| m[l.var() as usize] == l.is_positive()));
let models: Vec<Vec<bool>> = (0u64..(1u64 << nv))
.filter_map(|x| {
let m: Vec<bool> = (0..nv).map(|v| (x >> v) & 1 != 0).collect();
satisfies(&m).then_some(m)
})
.collect();
let generators = crate::symmetry_detect::find_generators(nv, clauses);
let group = perm_group_closure(&generators, nv);
let total_fixed: usize = group
.iter()
.map(|g| models.iter().filter(|m| apply_perm_to_model(g, m.as_slice()) == **m).count())
.sum();
assert_eq!(total_fixed % group.len(), 0, "Burnside sum divisible by |G|");
let direct = partition_into_orbits(&models, &generators).len();
let burnside = burnside_orbit_count(&models, &group);
let canonicals: BTreeSet<Vec<bool>> =
models.iter().map(|m| canonical_model(m, &generators)).collect();
assert_eq!(direct, burnside, "Burnside average == direct orbit partition");
assert_eq!(burnside, canonicals.len(), "Burnside count == #distinct canonical witnesses");
(models.len(), burnside, group.len())
};
let cc = crate::families::clique_coloring(3, 3).0;
let (raw, essential, gsize) = check(cc.num_vars, &cc.clauses);
assert!(gsize > 1, "clique_coloring(3,3) has a nontrivial group");
assert!(essential < raw, "symmetry genuinely compressed the witness count");
for seed in 0u64..40 {
let cnf = crate::families::random_3sat(6, 18, seed.wrapping_mul(0x9E37_79B9_7F4A_7C15));
check(6, &cnf.clauses);
}
}
fn decorrelated_seed(tag: u64, i: u64) -> u64 {
let mut z = tag.wrapping_mul(0xD1B5_4A32_D192_ED03).wrapping_add(i).wrapping_add(0x9E3779B97F4A7C15);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
z ^ (z >> 31)
}
#[test]
fn satisfiable_random_3sat_is_the_typical_case_below_the_threshold() {
let is_sat = |nv: usize, cl: &[Vec<Lit>]| {
(0u64..(1u64 << nv)).any(|x| cl.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive())))
};
let n = 14usize;
let trials = 60u64;
let sat_count = |m: usize| {
(0..trials)
.filter(|&s| {
let cnf = crate::families::random_3sat(n, m, decorrelated_seed(m as u64, s));
is_sat(n, &cnf.clauses)
})
.count()
};
let low = sat_count(2 * n); let high = sat_count(6 * n); assert!(low > 0, "satisfiable random 3-SATs exist — the premise 'they can't be SAT' is false");
assert!(2 * low > trials as usize, "below threshold, random 3-SAT is satisfiable in the MAJORITY: {low}/{trials}");
assert!(high < low, "the satisfiability rate collapses across the density threshold — the phase transition");
assert!(5 * high < trials as usize, "above threshold, random 3-SAT is overwhelmingly UNSAT: {high}/{trials}");
}
#[test]
fn the_satisfiability_threshold_climbs_from_3sat_to_4sat() {
let is_sat = |nv: usize, cl: &[Vec<Lit>]| {
(0u64..(1u64 << nv)).any(|x| cl.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive())))
};
let n = 14usize;
let trials = 60usize;
let sat_rate = |k: usize, m: usize| {
let tag = (k as u64) << 32 ^ m as u64;
(0..trials as u64)
.filter(|&s| is_sat(n, &crate::families::random_ksat(k, n, m, decorrelated_seed(tag, s)).clauses))
.count()
};
let three_at_6 = sat_rate(3, 6 * n);
let four_at_6 = sat_rate(4, 6 * n);
assert!(4 * three_at_6 < trials, "3-SAT at α=6 is above its 4.27 threshold → overwhelmingly UNSAT: {three_at_6}/{trials}");
assert!(4 * four_at_6 > 3 * trials, "4-SAT at α=6 is below its 9.93 threshold → overwhelmingly SAT: {four_at_6}/{trials}");
assert!(four_at_6 > three_at_6, "the threshold climbed: 4-SAT tolerates a density that already broke 3-SAT");
let four_at_14 = sat_rate(4, 14 * n);
assert!(four_at_14 < four_at_6, "4-SAT's own phase transition: SAT-rate collapses from α=6 to α=14");
assert!(2 * sat_rate(3, 4 * n) > trials, "3-SAT at α=4 (below 4.27) is SAT-majority");
assert!(2 * sat_rate(3, 6 * n) < trials, "3-SAT at α=6 (above 4.27) is UNSAT-majority");
}
#[test]
fn the_proof_complexity_ladder_separates_and_localizes_the_wall() {
let e_of = |cl: &[Vec<Lit>]| clauses_to_expr(cl).unwrap();
let php = crate::families::php(4).0;
assert_eq!(
weakest_crushing_rung(php.num_vars, &php.clauses, php.num_vars),
ProofRung::Counting,
"PHP is a counting refutation"
);
assert!(!crate::xorsat::refute_via_parity(&e_of(&php.clauses)), "pigeonhole is invisible to GF(2) parity");
let (_, par) = crate::families::parity_unsat(8, 12, 0xA5A5);
assert_eq!(
weakest_crushing_rung(par.num_vars, &par.clauses, par.num_vars),
ProofRung::Parity,
"an XOR contradiction is a parity refutation"
);
let pe = e_of(&par.clauses);
assert!(
crate::pigeonhole::counting_certificate(&pe).is_none() && crate::pigeonhole::hall_refutation(&pe).is_none(),
"a parity contradiction is invisible to counting / Hall"
);
let is_sat = |nv: usize, cl: &[Vec<Lit>]| {
(0u64..(1u64 << nv)).any(|x| cl.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive())))
};
let residue = (0u64..600)
.find_map(|seed| {
let c = crate::families::random_3sat(5, 26, seed.wrapping_mul(0x9E37_79B9_7F4A_7C15));
(!is_sat(5, &c.clauses) && automorphism_group_size(5, &c.clauses) == 1).then_some(c)
})
.expect("a rigid UNSAT random 3-SAT exists — the finite hard residue is real");
match weakest_crushing_rung(5, &residue.clauses, 5) {
ProofRung::Nullstellensatz { min_degree } => {
assert!(min_degree >= 3, "the residue needs genuine algebraic degree, got {min_degree}")
}
other => panic!("the rigid residue should land on the NS rung, got {other:?}"),
}
let re = e_of(&residue.clauses);
assert!(
crate::pigeonhole::counting_certificate(&re).is_none() && !crate::xorsat::refute_via_parity(&re),
"the rigid residue is invisible to every narrow cut — that silence IS the wall"
);
let cc = crate::families::clique_coloring(3, 3).0; assert!(is_sat(cc.num_vars, &cc.clauses), "clique_coloring(3,3) is SAT");
assert_eq!(
weakest_crushing_rung(cc.num_vars, &cc.clauses, 2),
ProofRung::BeyondBudget,
"a satisfiable instance fires no cut"
);
assert_eq!(
weakest_crushing_rung(5, &residue.clauses, 2),
ProofRung::BeyondBudget,
"below its degree, hard-UNSAT looks identical to SAT — the detectors cannot tell them apart"
);
}
#[test]
fn the_finite_hard_residue_exists_even_though_unbounded_random_cannot() {
let sat = |nv: usize, cl: &[Vec<Lit>]| {
(0u64..(1u64 << nv)).any(|x| {
cl.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
})
};
let mut found = None;
for seed in 0u64..600 {
let c = crate::families::random_3sat(5, 26, seed.wrapping_mul(0x9E37_79B9_7F4A_7C15));
if !sat(5, &c.clauses) && automorphism_group_size(5, &c.clauses) == 1 {
found = Some(c);
break;
}
}
let cnf = found.expect("a rigid UNSAT random 3-SAT exists — the finite hard residue is real");
let d = diagnose(5, &cnf.clauses);
assert_eq!(d.symmetry_bits, 0.0, "rigid — |Aut| = 1, no symmetry");
assert_eq!(d.cut, None, "no counting/parity/cardinality cut applies");
let min_degree = (1..=5).find(|°| crate::polycalc::nullstellensatz_refutes(5, &cnf.clauses, deg));
assert!(min_degree.is_some(), "decided by Nullstellensatz within the dimension cap (≤ n)");
assert!(min_degree.unwrap() >= 3, "it needed real algebra (degree ≥ clause width), not a cheap cut");
}
#[test]
fn we_break_on_whatever_symmetry_exists_never_assuming() {
let mut found = None;
for seed in 0u64..400 {
let cnf = crate::families::random_3sat(8, 11, seed.wrapping_mul(0x9E37_79B9_7F4A_7C15));
if automorphism_group_size(cnf.num_vars, &cnf.clauses) > 1 {
found = Some(cnf);
break;
}
}
let cnf = found.expect("some random instance carries accidental symmetry — we don't assume");
let bits = symmetry_entropy_bits(cnf.num_vars, &cnf.clauses);
assert!(bits > 0.0, "we FOUND accidental symmetry in the random instance: {bits} bits");
let (sym_sat, pruned) = decide_laddered_sym(cnf.num_vars, &cnf.clauses, false);
let (plain_sat, plain) = decide_laddered_nocut(cnf.num_vars, &cnf.clauses);
assert_eq!(sym_sat, plain_sat, "breaking on the accidental symmetry preserves the verdict");
assert!(
pruned.nodes <= plain.nodes,
"we broke on the accidental symmetry, cutting branches: {} ≤ {}",
pruned.nodes,
plain.nodes
);
}
#[test]
fn the_structure_census() {
use std::fmt::Write;
let mut chart = String::from("family bits cut residue\n");
chart.push_str("-------------------- ----- ------------- -------------------\n");
let mut row = |name: &str, nv: usize, cl: &[Vec<Lit>]| -> Option<Vec<Vec<Lit>>> {
let bits = symmetry_entropy_bits(nv, cl);
let cut = clauses_to_expr(cl).and_then(|e| {
if crate::pigeonhole::decide_pigeonhole_unsat(&e) {
Some(Shadow::Counting)
} else if crate::xorsat::refute_via_parity(&e) {
Some(Shadow::Parity)
} else if crate::pseudo_boolean::refute_clausal(&e) {
Some(Shadow::CuttingPlanes)
} else {
None
}
});
let core = find_random_core(nv, cl, 100);
let residue = match &core {
None => "— (all structure)".to_string(),
Some(c) => format!("{} clauses (RANDOM)", c.len()),
};
let _ = writeln!(chart, "{name:<20} {bits:>5.1} {:<13} {residue}", format!("{cut:?}"));
core
};
let php = crate::families::php(5).0;
assert_eq!(row("pigeonhole(5)", php.num_vars, &php.clauses), None, "pigeonhole: no randomness");
let cc = crate::families::clique_coloring(4, 3).0;
assert_eq!(row("clique_coloring(4,3)", cc.num_vars, &cc.clauses), None, "clique: no randomness");
let (_, t, _) = crate::families::tseitin_expander(8, 0x51);
assert_eq!(row("tseitin(8)", t.num_vars, &t.clauses), None, "tseitin: no randomness");
let rnd = crate::families::random_3sat(14, 58, 0xC0FFEE);
let residue = row("random_3sat(14,58)", rnd.num_vars, &rnd.clauses);
assert!(residue.is_some(), "random is the only family with an irreducible random residue");
println!("\n{chart}");
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("structure_census.txt"),
format!("STRUCTURE CENSUS — every structured family is fully crushed by structure (no random\nresidue); random is the ONLY family that leaves an irreducible core. Structure is always\nexploitable; randomness is the sole irreducible thing.\n\n{chart}\n"),
);
}
}
#[test]
fn finding_the_randomness_isolates_the_structureless_core() {
let php = crate::families::php(4).0;
assert_eq!(find_random_core(php.num_vars, &php.clauses, 50), None, "pigeonhole has no random core");
let rnd = crate::families::random_3sat(10, 26, 0xBEEF);
let mut padded = rnd.clauses.clone();
let shell = rnd.num_vars as u32;
padded.push(vec![Lit::new(shell, true), Lit::new(0, true)]); let nv = rnd.num_vars + 1;
if let Some(core) = find_random_core(nv, &padded, 50) {
assert!(core.iter().all(|c| c.iter().all(|l| l.var() != shell)), "the structural shell is stripped");
let d = diagnose(nv, &core);
assert!(d.cut.is_none(), "the isolated core has no exploitable cut — it's the randomness: {d:?}");
assert_eq!(
find_random_core(nv, &core, 50),
Some(core.clone()),
"the core is a reduction fixpoint — nothing strips it further"
);
}
}
#[test]
fn the_whole_portfolio_agrees_with_brute_force() {
fn sm(s: &mut u64) -> u64 {
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *s;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z ^ (z >> 31)
}
let mut state = 0x6005_0001u64;
for _ in 0..100 {
let nv = 4 + (sm(&mut state) % 5) as usize; let m = 3 + (sm(&mut state) % 12) as usize;
let mut cl: Vec<Vec<Lit>> = Vec::new();
for _ in 0..m {
let mut c = Vec::new();
for v in 0..nv {
if sm(&mut state) % 3 == 0 {
c.push(Lit::new(v as u32, sm(&mut state) % 2 == 0));
}
}
if !c.is_empty() {
cl.push(c);
}
}
if cl.is_empty() {
continue;
}
let brute = (0u64..(1u64 << nv)).any(|x| {
cl.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
});
if let Some(sat) = crush(nv, &cl, 1_000_000) {
assert_eq!(sat, brute, "crush disagrees: {cl:?}");
}
if let Some(sat) = autocarve(nv, &cl, 1_000_000) {
assert_eq!(sat, brute, "autocarve disagrees: {cl:?}");
}
let (sym, _) = decide_laddered_sym(nv, &cl, true);
assert_eq!(sym, brute, "symmetry-pruned ladder disagrees: {cl:?}");
let (plain, _) = decide_laddered(nv, &cl);
assert_eq!(plain, brute, "plain ladder disagrees: {cl:?}");
let (nocut, _) = decide_laddered_nocut(nv, &cl);
assert_eq!(nocut, brute, "cut-free baseline disagrees: {cl:?}");
}
}
#[test]
fn auto_advance_drives_structure_to_its_fixpoint() {
let php = crate::families::php(4).0;
let (status, trace) = auto_advance(php.num_vars, &php.clauses, 50);
assert_eq!(status, AdvanceStatus::Decided(false), "pigeonhole decided: {trace:?}");
assert!(trace.last().unwrap().lever.contains("cut"), "by the cut: {trace:?}");
let mut layered = php.clauses.clone();
layered.push(vec![Lit::new(php.num_vars as u32, true), Lit::new(0, true)]);
let (st, tr) = auto_advance(php.num_vars + 1, &layered, 50);
assert_eq!(st, AdvanceStatus::Decided(false), "layered decided: {tr:?}");
assert!(tr.len() >= 2, "carve then cut — multiple advance steps: {tr:?}");
let rnd = crate::families::random_3sat(14, 58, 0xC0FFEE);
let (sr, rtr) = auto_advance(rnd.num_vars, &rnd.clauses, 50);
assert!(
matches!(sr, AdvanceStatus::StructurelessResidue { .. }),
"random advances to the irreducible residue: {sr:?}"
);
assert!(!rtr.iter().any(|s| s.lever.contains("cut")), "no certified cut on the residue: {rtr:?}");
}
#[test]
fn diagnose_auto_discovers_the_applicable_levers() {
let php = crate::families::php(4).0;
let dp = diagnose(php.num_vars, &php.clauses);
assert_eq!(dp.cut, Some(Shadow::Counting), "pigeonhole offers the counting cut: {dp:?}");
assert!(dp.symmetry_bits > 0.0, "and rich symmetry: {dp:?}");
let lp = applicable_levers(&dp);
assert!(lp.iter().any(|s| s.contains("counting")), "menu lists the counting cut: {lp:?}");
let (_, t, _) = crate::families::tseitin_expander(8, 0x51);
assert_eq!(diagnose(t.num_vars, &t.clauses).cut, Some(Shadow::Parity), "Tseitin offers parity");
let rnd = crate::families::random_3sat(14, 55, 0xC0FFEE);
let dr = diagnose(rnd.num_vars, &rnd.clauses);
assert_eq!(dr.cut, None, "random offers no cut: {dr:?}");
let lr = applicable_levers(&dr);
assert!(
lr.iter().any(|s| s.contains("backdoor") || s.contains("carving") || s.contains("residue")),
"the menu honestly falls back to backdoor/branch: {lr:?}"
);
}
#[test]
fn the_complexity_spectrum_is_quotient_size() {
use std::fmt::Write;
let mut chart = String::from("family clauses quotient cut core\n");
chart.push_str("-------------------- ------- -------- ------------- ----\n");
let mut row = |name: String, nv: usize, cl: &[Vec<Lit>]| -> StructuralProfile {
let p = structural_profile(nv, cl);
let _ = writeln!(
chart,
"{name:<20} {:>7} {:>8} {:<13} {:>4}",
p.clauses, p.quotient, format!("{:?}", p.cut), p.core_clauses
);
p
};
let php = crate::families::php(5).0;
let php_p = row("pigeonhole(5)".into(), php.num_vars, &php.clauses);
let tsei = crate::families::tseitin_expander(8, 0x51).1;
let tsei_p = row("tseitin(8)".into(), tsei.num_vars, &tsei.clauses);
let cc = crate::families::clique_coloring(4, 3).0;
let cc_p = row("clique_coloring(4,3)".into(), cc.num_vars, &cc.clauses);
let rnd = crate::families::random_3sat(14, 50, 0xC0FFEE);
let rnd_p = row("random_3sat(14,50)".into(), rnd.num_vars, &rnd.clauses);
assert!(php_p.quotient <= 3 && php_p.cut.is_some(), "pigeonhole: tiny quotient, a cut: {php_p:?}");
assert!(tsei_p.quotient <= 5 && tsei_p.cut == Some(Shadow::Parity), "tseitin: small quotient, parity: {tsei_p:?}");
assert!(cc_p.quotient <= 3 && cc_p.cut.is_some(), "clique: tiny quotient, a cut: {cc_p:?}");
assert!(
rnd_p.cut.is_none() && rnd_p.quotient * 2 > rnd_p.clauses,
"random: no cut, quotient near the full clause count — the interesting residue: {rnd_p:?}"
);
println!("\n{chart}");
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("complexity_spectrum.txt"),
format!("THE COMPLEXITY SPECTRUM IS QUOTIENT SIZE — how far symmetry collapses the cube predicts\neverything: a tiny orbit-type quotient comes with a single certified cut; a full quotient comes\nwith no cut and an irreducible core. Difficulty is quotient size.\n\n{chart}\n"),
);
}
}
#[test]
fn the_auto_collapse_spreads_across_families() {
use std::fmt::Write;
let mut table = String::from("family vars clauses rule_types shadow\n");
let mut record = |name: &str, sig: &FamilySignature| {
let _ = writeln!(
table,
"{name:<20} {:>4} {:>7} {:>10} {:?}",
sig.num_vars, sig.clauses, sig.rule_types, sig.shadow
);
};
let php = crate::families::php(5).0;
let php_sig = abstract_signature(php.num_vars, &php.clauses);
record("pigeonhole(5)", &php_sig);
assert_eq!(php_sig.shadow, Some(Shadow::Counting), "pigeonhole is a counting cover");
assert!(php_sig.rule_types <= 4, "pigeonhole collapses to a few rule-types");
let cc = crate::families::clique_coloring(4, 3).0;
let cc_sig = abstract_signature(cc.num_vars, &cc.clauses);
record("clique_coloring(4,3)", &cc_sig);
assert!(cc_sig.shadow.is_some(), "clique-coloring is refuted by a shadow");
let (_, tcnf, _) = crate::families::tseitin_expander(8, 0x51);
let t_sig = abstract_signature(tcnf.num_vars, &tcnf.clauses);
record("tseitin(8)", &t_sig);
assert_eq!(t_sig.shadow, Some(Shadow::Parity), "Tseitin is a parity cover");
let rnd = crate::families::random_3sat(14, 40, 0xC0FFEE);
let r_sig = abstract_signature(rnd.num_vars, &rnd.clauses);
record("random_3sat(14,40)", &r_sig);
assert_eq!(r_sig.shadow, None, "random hardness is not a recognized shadow class");
assert!(r_sig.rule_types > 2, "random rules spread across many types — no global collapse");
println!("\n{table}");
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("family_taxonomy.txt"),
format!("ABSTRACT FAMILY TAXONOMY — symmetry-break the rules, probe the shadows\n\n{table}\n"),
);
}
}
#[test]
fn the_abstract_certificate_is_scale_invariant_where_the_closure_explodes() {
for n in [4usize, 8, 16, 32] {
let cert = pigeonhole_abstract_refutation(n).expect("pigeonhole refuted at every scale");
assert_eq!(cert.rule_types, 2, "always exactly two rule-types — the abstraction is scale-invariant");
assert_eq!(cert.witness.pigeons, n as u128);
assert_eq!(cert.witness.holes, (n - 1) as u128);
assert!(cert.witness.pigeons > cert.witness.holes, "the counting invariant refutes");
assert!(crate::pigeonhole::check_counting_cert(&cert.witness), "the O(1) witness re-checks");
let e = clauses_to_expr(&crate::families::php(n).0.clauses).unwrap();
assert_eq!(crate::sat::prove_unsat(&e), crate::sat::UnsatOutcome::Refuted);
}
}
#[test]
fn symmetric_resolution_refutes_pigeonhole_through_a_bounded_orbit_pattern() {
let cover = php_cover(3);
let gens = php_symmetries(3);
let empty = Subcube { n: cover.n, care: 0, value: 0 };
let growth = symmetric_resolution_growth(&cover, &gens, 8);
let last = *growth.last().unwrap();
assert_eq!(last, growth[growth.len() - 2], "the resolution closure reaches a fixpoint");
let (raw_fix, orbit_fix) = last;
assert!(
orbit_fix * 4 < raw_fix,
"orbit-types {orbit_fix} ≪ raw {raw_fix}: symmetry collapses the derived rules"
);
let mut raw: BTreeSet<Subcube> = cover.blockers.iter().copied().collect();
let mut refuted = false;
for _ in 0..8 {
let current: Vec<Subcube> = raw.iter().copied().collect();
for i in 0..current.len() {
for j in (i + 1)..current.len() {
if let Some((_, r)) = current[i].resolve(¤t[j]) {
raw.insert(r);
}
}
}
if raw.contains(&empty) {
refuted = true;
break;
}
}
assert!(refuted, "resolution closes PHP(3) to the empty clause — a refutation on the cube");
}
#[test]
fn symmetric_resolution_growth_is_monotone_and_orbit_bounded() {
let cover = php_cover(3);
let gens = php_symmetries(3);
let growth = symmetric_resolution_growth(&cover, &gens, 6);
for (raw, orbits) in &growth {
assert!(orbits <= raw, "orbit-types never exceed raw rules");
}
for w in growth.windows(2) {
assert!(w[1].0 >= w[0].0, "the raw closure only grows (monotone)");
assert!(w[1].1 >= w[0].1, "and so does the orbit-type set");
}
}
#[test]
fn resolution_nets_a_new_rule_covering_both_neighbors() {
let c = Subcube::blocker(&[Lit::new(0, true), Lit::new(1, true), Lit::new(2, true)], 4);
let d = Subcube::blocker(&[Lit::new(0, true), Lit::new(1, true), Lit::new(2, false)], 4);
let (pivot, resolvent) = c.resolve(&d).expect("neighbors across x2 must resolve");
assert_eq!(pivot, 2);
assert_eq!(resolvent.clause_literals(), vec![(0, true), (1, true)], "resolvent is (x0 ∨ x1)");
let union: BTreeSet<Corner> = c.footprint().into_iter().chain(d.footprint()).collect();
let merged: BTreeSet<Corner> = resolvent.footprint().into_iter().collect();
assert_eq!(merged, union, "the derived rule covers both neighbors and nothing more");
assert_eq!(resolvent.dimension(), c.dimension() + 1, "one pivot freed ⟹ one dimension larger");
}
#[test]
fn resolution_matches_clause_resolution() {
let c = Subcube::blocker(&[Lit::new(0, true), Lit::new(1, false), Lit::new(2, true)], 5);
let d = Subcube::blocker(&[Lit::new(0, false), Lit::new(1, false), Lit::new(3, true)], 5);
let (pivot, r) = c.resolve(&d).expect("resolve on x0");
assert_eq!(pivot, 0);
let got: BTreeSet<(usize, bool)> = r.clause_literals().into_iter().collect();
let want: BTreeSet<(usize, bool)> = [(1, false), (2, true), (3, true)].into_iter().collect();
assert_eq!(got, want);
let e = Subcube::blocker(&[Lit::new(0, true), Lit::new(1, true)], 5);
let f = Subcube::blocker(&[Lit::new(0, false), Lit::new(1, false)], 5);
assert_eq!(e.resolve(&f), None, "a second clash blocks resolution (tautology)");
let g = Subcube::blocker(&[Lit::new(0, true), Lit::new(1, true)], 5);
let h = Subcube::blocker(&[Lit::new(0, true), Lit::new(2, true)], 5);
assert_eq!(g.resolve(&h), None, "no opposite literal ⟹ no resolution");
}
#[test]
fn resolution_commutes_with_symmetry() {
let c = Subcube::blocker(&[Lit::new(0, true), Lit::new(1, false), Lit::new(2, true)], 4);
let d = Subcube::blocker(&[Lit::new(0, false), Lit::new(1, false), Lit::new(3, true)], 4);
let sigma = CubeSym { perm: vec![3, 1, 0, 2], flip: vec![false, true, false, true] };
let (pivot, resolvent) = c.resolve(&d).unwrap();
let (pivot_img, resolvent_img) =
sigma.map_subcube(&c).resolve(&sigma.map_subcube(&d)).expect("the images still resolve");
assert_eq!(pivot_img, sigma.perm[pivot], "the pivot moves with the symmetry");
assert_eq!(
resolvent_img,
sigma.map_subcube(&resolvent),
"resolution and symmetry commute ⟹ derived rules respect the orbits"
);
}
#[test]
fn referencing_one_rule_nets_its_neighbors() {
let cnf = DimacsCnf {
num_vars: 3,
clauses: vec![
vec![Lit::new(0, true), Lit::new(1, true)],
vec![Lit::new(0, false), Lit::new(2, true)],
vec![Lit::new(1, true), Lit::new(2, true)],
],
};
let cover = Cover::of_cnf(&cnf);
let neighbors = cover.neighbors(0);
assert_eq!(neighbors.len(), 1, "only clause 1 is a resolution neighbor of clause 0");
let (j, pivot, resolvent) = &neighbors[0];
assert_eq!(*j, 1);
assert_eq!(*pivot, 0);
let lits: BTreeSet<(usize, bool)> = resolvent.clause_literals().into_iter().collect();
assert_eq!(lits, [(1, true), (2, true)].into_iter().collect(), "the netted rule is (x1 ∨ x2)");
}
#[test]
fn there_is_no_random_only_unfound_structure() {
let cnf = crate::families::random_3sat(11, 20, 0xBEEF);
let backdoor = greedy_2sat_backdoor(&cnf.clauses, cnf.num_vars);
assert!(
backdoor.len() < cnf.num_vars,
"backdoor {} must be smaller than {} variables",
backdoor.len(),
cnf.num_vars
);
assert!(
is_strong_backdoor_to_2sat(&cnf.clauses, cnf.num_vars, &backdoor),
"every fixing of the backdoor must leave a 2-SAT residual"
);
let via_backdoor = decide_sat_via_2sat_backdoor(&cnf.clauses, cnf.num_vars, &backdoor);
let e = clauses_to_expr(&cnf.clauses).expect("non-empty random instance");
match crate::sat::prove_unsat(&e) {
crate::sat::UnsatOutcome::Refuted => assert!(!via_backdoor, "prover says UNSAT"),
crate::sat::UnsatOutcome::Sat(_) => assert!(via_backdoor, "prover says SAT"),
crate::sat::UnsatOutcome::Unsupported => panic!("prover should decide this instance"),
}
}
#[test]
fn a_random_instances_symmetry_is_definite_not_absent() {
let cnf = crate::families::random_3sat(11, 20, 0xBEEF);
let cover = Cover::of_cnf(&cnf);
let sig = cover.discovered_rule_symmetry();
assert!(sig.rule_orbits * 2 > sig.blockers, "global symmetry is small but definite: {sig:?}");
let backdoor = greedy_2sat_backdoor(&cnf.clauses, cnf.num_vars);
assert!(!backdoor.is_empty(), "the structure is there — it is local (a backdoor)");
}
#[test]
fn symmetric_backdoor_branches_collapse_for_speed() {
let n = 4; let (cnf, _) = crate::families::php(n);
let backdoor = greedy_2sat_backdoor(&cnf.clauses, cnf.num_vars);
assert!(is_strong_backdoor_to_2sat(&cnf.clauses, cnf.num_vars, &backdoor));
let sat = decide_sat_via_2sat_backdoor(&cnf.clauses, cnf.num_vars, &backdoor);
assert!(!sat, "PHP(4) is UNSAT: no backdoor branch leaves a satisfiable 2-SAT residual");
let branches = 1u64 << backdoor.len();
let orbits = backdoor_branch_orbit_count(&backdoor, &php_perm_symmetries(n));
assert!(orbits < branches, "symmetry collapses {branches} branches to {orbits} — speed");
}
#[test]
fn geometric_and_scalable_rule_orbits_are_the_same_quotient() {
for n in 2..=7 {
let cover = php_cover(n); let geometric = cover.blocker_orbits(&php_symmetries(n)).unwrap().len();
let (cnf, _) = crate::families::php(n);
let scalable = clause_orbits(&cnf.clauses, &php_perm_symmetries(n)).len();
assert_eq!(geometric, scalable, "PHP({n}): geometric={geometric} scalable={scalable}");
assert_eq!(geometric, 2, "and both see the two essential pigeonhole rules");
}
}
#[test]
#[ignore = "scale-walk; banks the rule-symmetry complexity-limit chart"]
fn rule_symmetry_complexity_limit_chart() {
let mut chart = String::from(" n vars corners blockers gens rule_orbits\n");
chart.push_str("--- ----- ------------ --------- ----- -----------\n");
for n in 2..=24 {
let sig = pigeonhole_rule_symmetry(n);
let vars = n * (n - 1);
chart.push_str(&format!(
"{:>3} {:>5} 2^{:<10} {:>9} {:>5} {}\n",
n, vars, vars, sig.blockers, sig.generators, sig.rule_orbits
));
assert_eq!(sig.rule_orbits, 2, "rule symmetry must stay at 2 at every scale (n = {n})");
}
println!("\n{chart}");
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("rule_symmetry_limits.txt"),
format!(
"RULE-SYMMETRY COMPLEXITY LIMIT — pigeonhole rules collapse to 2 orbits at every scale,\n\
computed in milliseconds over the polynomial blocker set while the cube itself grows\n\
to 2^{{n(n-1)}} corners. Two essential rules describe the entire infinite family.\n\n{chart}\n"
),
);
}
}
}