use crate::cdcl::{Lit, SolveResult, Solver};
use crate::complexity::RankedRefutation;
use crate::proof::{Perm, ProofStep, Witness};
use crate::symmetry_detect::{perm_is_automorphism, AutomorphismIndex};
use crate::xorsat::XorEquation;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LyapunovCertificate {
pub initial: u64,
pub minimum: u64,
pub levels: u64,
pub max_dissipation: u64,
pub size_bound: u64,
pub total_steps: u64,
pub monotone: bool,
pub strict_descent: bool,
pub reaches_goal: bool,
}
pub fn verify_lyapunov(potential: &[u64], reaches_goal: bool) -> Option<LyapunovCertificate> {
if potential.is_empty() {
return None;
}
let monotone = potential.windows(2).all(|w| w[1] <= w[0]);
if !monotone {
return None;
}
let mut levels_seq: Vec<u64> = potential.to_vec();
levels_seq.dedup();
let strict_descent = levels_seq.windows(2).all(|w| w[1] < w[0]);
if !strict_descent {
return None;
}
if !reaches_goal {
return None;
}
let mut counts: std::collections::BTreeMap<u64, u64> = std::collections::BTreeMap::new();
for &p in potential {
*counts.entry(p).or_insert(0) += 1;
}
let levels = counts.len() as u64;
let max_dissipation = counts.values().copied().max().unwrap_or(0);
Some(LyapunovCertificate {
initial: potential[0],
minimum: *potential.last().unwrap(),
levels,
max_dissipation,
size_bound: levels * max_dissipation,
total_steps: potential.len() as u64,
monotone,
strict_descent,
reaches_goal,
})
}
pub fn lyapunov_of_symmetry(ranked: &RankedRefutation) -> Option<LyapunovCertificate> {
verify_lyapunov(&ranked.ranks, ranked.refuted)
}
pub fn gaussian_lyapunov(equations: &[XorEquation], num_vars: usize) -> (Vec<u64>, bool) {
let words = (num_vars + 1 + 63) / 64;
let bit = |row: &mut [u64], i: usize| row[i / 64] ^= 1u64 << (i % 64);
let get = |row: &[u64], i: usize| (row[i / 64] >> (i % 64)) & 1 == 1;
let mut rows: Vec<Vec<u64>> = equations
.iter()
.map(|eq| {
let mut r = vec![0u64; words];
for &v in &eq.vars {
if v < num_vars {
bit(&mut r, v);
}
}
if eq.rhs {
bit(&mut r, num_vars); }
r
})
.collect();
let mut trajectory: Vec<u64> = Vec::new();
let mut remaining = num_vars as u64;
let mut used = vec![false; rows.len()];
for col in 0..num_vars {
let pivot = (0..rows.len()).find(|&r| !used[r] && get(&rows[r], col));
if let Some(pr) = pivot {
used[pr] = true;
let pivot_row = rows[pr].clone();
for r in 0..rows.len() {
if r != pr && get(&rows[r], col) {
for w in 0..words {
rows[r][w] ^= pivot_row[w];
}
}
}
remaining -= 1;
trajectory.push(remaining);
}
}
let reached_goal = rows.iter().any(|r| (0..num_vars).all(|v| !get(r, v)) && get(r, num_vars));
if reached_goal {
trajectory.push(0);
}
if trajectory.is_empty() {
trajectory.push(remaining);
}
(trajectory, reached_goal)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CollapsingMeasure {
pub items: usize,
pub bins: usize,
}
fn swap_items(num_vars: usize, bins: usize, a: usize, b: usize) -> Perm {
Perm::from_images(
(0..num_vars)
.map(|idx| {
let (item, bin) = (idx / bins, idx % bins);
let ni = if item == a {
b
} else if item == b {
a
} else {
item
};
Lit::pos((ni * bins + bin) as u32)
})
.collect(),
)
}
pub fn covering_collapse(num_vars: usize, formula: &[Vec<Lit>], items: usize, bins: usize) -> RankedRefutation {
let mut db = formula.to_vec();
let mut index = AutomorphismIndex::with_clauses(num_vars, formula);
let mut steps: Vec<ProofStep> = Vec::new();
let mut ranks: Vec<u64> = Vec::new();
let var = |i: usize, b: usize| (i * bins + b) as u32;
let active = bins + 1;
if active <= items {
for m in (2..=active).rev() {
let bin = m - 2;
let last = m - 1;
for i in 0..last {
let clause = vec![Lit::neg(var(i, bin))];
let witness = Witness::Substitution(swap_items(num_vars, bins, i, last));
if crate::pr::is_pr_indexed(num_vars, &db, &mut index, &clause, &witness) {
db.push(clause.clone());
index.insert(clause.clone());
steps.push(ProofStep::Pr { clause, witness });
ranks.push(m as u64);
}
}
}
}
let mut solver = Solver::new(num_vars);
for c in &db {
solver.add_clause(c.clone());
}
let refuted = match solver.solve() {
SolveResult::Sat(_) => false,
SolveResult::Unsat => {
for lc in solver.learned() {
steps.push(ProofStep::Rup(lc.lits.clone()));
ranks.push(1);
}
crate::pr::check_pr_refutation_fast(num_vars, formula, &steps)
}
};
RankedRefutation { refuted, steps, ranks }
}
pub fn synthesize_measure(num_vars: usize, formula: &[Vec<Lit>]) -> Option<CollapsingMeasure> {
for bins in 1..num_vars {
if num_vars % bins != 0 {
continue;
}
let items = num_vars / bins;
if items <= bins {
continue; }
if perm_is_automorphism(formula, &swap_items(num_vars, bins, 0, 1)) {
return Some(CollapsingMeasure { items, bins });
}
}
None
}
pub fn solve_by_measure_synthesis(
num_vars: usize,
formula: &[Vec<Lit>],
) -> Option<(CollapsingMeasure, RankedRefutation)> {
for bins in 1..num_vars {
if num_vars % bins != 0 {
continue;
}
let items = num_vars / bins;
if items <= bins {
continue;
}
if !perm_is_automorphism(formula, &swap_items(num_vars, bins, 0, 1)) {
continue;
}
let ranked = covering_collapse(num_vars, formula, items, bins);
if ranked.refuted {
return Some((CollapsingMeasure { items, bins }, ranked));
}
}
None
}
use crate::cdcl::Lit as Lit_;
pub trait LyapunovMeasure {
fn num_vars(&self) -> usize;
fn formula(&self) -> &[Vec<Lit_>];
fn initial_potential(&self) -> u64;
fn width(&self) -> u64;
fn descent_step(&self, level: u64, db: &[Vec<Lit_>]) -> Vec<(Vec<Lit_>, Witness)>;
}
pub fn proof_from_measure<M: LyapunovMeasure>(measure: &M) -> RankedRefutation {
let nv = measure.num_vars();
let formula = measure.formula();
let mut db: Vec<Vec<Lit_>> = formula.to_vec();
let mut index = AutomorphismIndex::with_clauses(nv, formula);
let mut steps: Vec<ProofStep> = Vec::new();
let mut ranks: Vec<u64> = Vec::new();
let l = measure.initial_potential();
for level in (1..=l).rev() {
for (clause, witness) in measure.descent_step(level, &db) {
if crate::pr::is_pr_indexed(nv, &db, &mut index, &clause, &witness) {
db.push(clause.clone());
index.insert(clause.clone());
steps.push(ProofStep::Pr { clause, witness });
ranks.push(level);
}
}
}
let mut solver = Solver::new(nv);
for c in &db {
solver.add_clause(c.clone());
}
let refuted = match solver.solve() {
SolveResult::Sat(_) => false,
SolveResult::Unsat => {
for lc in solver.learned() {
steps.push(ProofStep::Rup(lc.lits.clone()));
ranks.push(0);
}
crate::pr::check_pr_refutation_fast(nv, formula, &steps)
}
};
RankedRefutation { refuted, steps, ranks }
}
pub fn proof_induced_measure(n_steps: usize) -> Vec<u64> {
if n_steps == 0 {
return vec![0];
}
(0..n_steps).map(|i| (n_steps - i) as u64).collect() }
#[derive(Clone)]
pub struct CoveringMeasure {
pub num_vars: usize,
pub formula: Vec<Vec<Lit_>>,
pub items: usize,
pub bins: usize,
}
impl LyapunovMeasure for CoveringMeasure {
fn num_vars(&self) -> usize {
self.num_vars
}
fn formula(&self) -> &[Vec<Lit_>] {
&self.formula
}
fn initial_potential(&self) -> u64 {
(self.bins + 1).min(self.items) as u64 }
fn width(&self) -> u64 {
self.items as u64 }
fn descent_step(&self, level: u64, _db: &[Vec<Lit_>]) -> Vec<(Vec<Lit_>, Witness)> {
let m = level as usize;
if m < 2 {
return Vec::new();
}
let bin = m - 2;
let last = m - 1;
(0..last)
.map(|i| {
let clause = vec![Lit_::neg((i * self.bins + bin) as u32)];
let witness = Witness::Substitution(swap_items(self.num_vars, self.bins, i, last));
(clause, witness)
})
.collect()
}
}
pub fn cutting_planes_lyapunov(n: usize) -> (Vec<u64>, bool) {
use crate::pseudo_boolean::PbConstraint;
if n < 2 {
return (vec![0], true);
}
let holes = n - 1;
let var = |i: usize, h: usize| i * holes + h;
let mut constraints: Vec<PbConstraint> = Vec::new();
for i in 0..n {
let lits: Vec<(usize, bool)> = (0..holes).map(|h| (var(i, h), true)).collect();
constraints.push(PbConstraint::clause(&lits)); }
for h in 0..holes {
let lits: Vec<(usize, bool)> = (0..n).map(|i| (var(i, h), true)).collect();
constraints.push(PbConstraint::at_most(&lits, 1)); }
let total = constraints.len() as u64;
let mut acc: Option<PbConstraint> = None;
let mut trajectory: Vec<u64> = Vec::new();
for (k, c) in constraints.into_iter().enumerate() {
acc = Some(match acc.take() {
None => c,
Some(a) => a.add(&c),
});
trajectory.push(total - 1 - k as u64); }
let reached_goal = acc.map_or(false, |a| a.is_contradiction());
(trajectory, reached_goal)
}
#[derive(Clone)]
pub struct PartialCoveringMeasure {
pub base: CoveringMeasure,
pub lo: usize,
pub hi: usize,
}
impl LyapunovMeasure for PartialCoveringMeasure {
fn num_vars(&self) -> usize {
self.base.num_vars
}
fn formula(&self) -> &[Vec<Lit_>] {
&self.base.formula
}
fn initial_potential(&self) -> u64 {
(self.hi.saturating_sub(self.lo) + 1) as u64
}
fn width(&self) -> u64 {
self.base.width()
}
fn descent_step(&self, local_level: u64, db: &[Vec<Lit_>]) -> Vec<(Vec<Lit_>, Witness)> {
let abs = self.lo + (local_level as usize).saturating_sub(1);
if abs < self.lo || abs > self.hi {
return Vec::new();
}
self.base.descent_step(abs as u64, db)
}
}
pub fn compose_collapses(
num_vars: usize,
formula: &[Vec<Lit_>],
stages: &[&dyn LyapunovMeasure],
) -> RankedRefutation {
let mut db: Vec<Vec<Lit_>> = formula.to_vec();
let mut index = AutomorphismIndex::with_clauses(num_vars, formula);
let mut steps: Vec<ProofStep> = Vec::new();
let mut ranks: Vec<u64> = Vec::new();
let mut above: u64 = stages.iter().map(|s| s.initial_potential()).sum();
for stage in stages {
let l = stage.initial_potential();
above -= l; for level in (1..=l).rev() {
for (clause, witness) in stage.descent_step(level, &db) {
if crate::pr::is_pr_indexed(num_vars, &db, &mut index, &clause, &witness) {
db.push(clause.clone());
index.insert(clause.clone());
steps.push(ProofStep::Pr { clause, witness });
ranks.push(above + level);
}
}
}
}
let mut solver = Solver::new(num_vars);
for c in &db {
solver.add_clause(c.clone());
}
let refuted = match solver.solve() {
SolveResult::Sat(_) => false,
SolveResult::Unsat => {
for lc in solver.learned() {
steps.push(ProofStep::Rup(lc.lits.clone()));
ranks.push(0);
}
crate::pr::check_pr_refutation_fast(num_vars, formula, &steps)
}
};
RankedRefutation { refuted, steps, ranks }
}
pub fn extract_xor(num_vars: usize, clauses: &[Vec<Lit_>]) -> Vec<XorEquation> {
use std::collections::HashMap;
let mut groups: HashMap<Vec<usize>, Vec<u32>> = HashMap::new(); let mut members: HashMap<Vec<usize>, Vec<Vec<u32>>> = HashMap::new(); for c in clauses {
let mut vs: Vec<usize> = c.iter().map(|l| l.var() as usize).collect();
vs.sort_unstable();
vs.dedup();
if vs.len() != c.len() || vs.iter().any(|&v| v >= num_vars) {
continue; }
let neg = c.iter().filter(|l| !l.is_positive()).count() as u32;
groups.entry(vs.clone()).or_default().push(neg % 2);
let mut sig: Vec<u32> =
c.iter().map(|l| l.var() * 2 + u32::from(!l.is_positive())).collect();
sig.sort_unstable();
members.entry(vs).or_default().push(sig);
}
let mut eqs = Vec::new();
for (vars, parities) in groups {
let k = vars.len();
if k == 0 || k > 12 {
continue;
}
let expected = 1usize << (k - 1);
if parities.len() != expected || !parities.iter().all(|&p| p == parities[0]) {
continue;
}
let mut sigs = members[&vars].clone();
sigs.sort();
sigs.dedup();
if sigs.len() != expected {
continue; }
let b = 1 - parities[0]; eqs.push(XorEquation::new(vars, b == 1));
}
eqs
}
fn discover_covering(
num_vars: usize,
formula: &[Vec<Lit_>],
) -> Option<(Vec<Vec<usize>>, Vec<Vec<usize>>)> {
let mut rows: Vec<Vec<usize>> = Vec::new();
let mut excl: Vec<(usize, usize)> = Vec::new();
for c in formula {
if c.is_empty() {
return None;
}
if c.iter().all(|l| l.is_positive()) {
rows.push(c.iter().map(|l| l.var() as usize).collect()); } else if c.len() == 2 && c.iter().all(|l| !l.is_positive()) {
excl.push((c[0].var() as usize, c[1].var() as usize)); } else {
return None;
}
}
if rows.is_empty() {
return None;
}
let mut row_of = vec![usize::MAX; num_vars];
for (i, r) in rows.iter().enumerate() {
for &v in r {
if v >= num_vars || row_of[v] != usize::MAX {
return None;
}
row_of[v] = i;
}
}
let mut parent: Vec<usize> = (0..num_vars).collect();
let find = |parent: &mut Vec<usize>, mut x: usize| {
while parent[x] != x {
parent[x] = parent[parent[x]];
x = parent[x];
}
x
};
let mut excl_set: std::collections::HashSet<(usize, usize)> = std::collections::HashSet::new();
for &(a, b) in &excl {
if a >= num_vars || b >= num_vars || row_of[a] == usize::MAX || row_of[b] == usize::MAX {
return None; }
excl_set.insert((a.min(b), a.max(b)));
let (ra, rb) = (find(&mut parent, a), find(&mut parent, b));
if ra != rb {
parent[ra] = rb;
}
}
let mut col_id: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
let mut columns: Vec<Vec<usize>> = Vec::new();
for v in 0..num_vars {
if row_of[v] == usize::MAX {
continue;
}
let root = find(&mut parent, v);
let id = *col_id.entry(root).or_insert_with(|| {
columns.push(Vec::new());
columns.len() - 1
});
columns[id].push(v);
}
for col in &columns {
for i in 0..col.len() {
for j in (i + 1)..col.len() {
if !excl_set.contains(&(col[i].min(col[j]), col[i].max(col[j]))) {
return None;
}
}
}
}
Some((rows, columns))
}
pub fn recover_cardinality_constraints(
num_vars: usize,
formula: &[Vec<Lit_>],
) -> Option<Vec<crate::pseudo_boolean::PbConstraint>> {
use crate::pseudo_boolean::PbConstraint;
let (rows, columns) = discover_covering(num_vars, formula)?;
let mut constraints: Vec<PbConstraint> = Vec::new();
for r in &rows {
constraints.push(PbConstraint::clause(&r.iter().map(|&v| (v, true)).collect::<Vec<_>>())); }
for col in &columns {
constraints.push(PbConstraint::at_most(&col.iter().map(|&v| (v, true)).collect::<Vec<_>>(), 1)); }
Some(constraints)
}
pub fn cardinality_collapse(num_vars: usize, formula: &[Vec<Lit_>]) -> Option<(Vec<u64>, bool, usize)> {
use crate::pseudo_boolean::PbConstraint;
let constraints = recover_cardinality_constraints(num_vars, formula)?;
let total = constraints.len() as u64;
let mut acc: Option<PbConstraint> = None;
let mut trajectory: Vec<u64> = Vec::new();
for (k, c) in constraints.into_iter().enumerate() {
acc = Some(match acc.take() {
None => c,
Some(a) => a.add(&c),
});
trajectory.push(total - 1 - k as u64);
}
let reached_goal = acc.map_or(false, |a| a.is_contradiction());
Some((trajectory, reached_goal, total as usize))
}
pub fn recover_at_most_one(num_vars: usize, clauses: &[Vec<Lit_>]) -> Vec<crate::pseudo_boolean::PbConstraint> {
use crate::pseudo_boolean::PbConstraint;
use std::collections::HashSet;
let mut adj: Vec<HashSet<usize>> = vec![HashSet::new(); num_vars];
for c in clauses {
if c.len() == 2 && c.iter().all(|l| !l.is_positive()) {
let (a, b) = (c[0].var() as usize, c[1].var() as usize);
if a != b && a < num_vars && b < num_vars {
adj[a].insert(b);
adj[b].insert(a);
}
}
}
let mut order: Vec<usize> = (0..num_vars).filter(|&v| !adj[v].is_empty()).collect();
order.sort_by_key(|&v| std::cmp::Reverse(adj[v].len()));
let mut covered: HashSet<(usize, usize)> = HashSet::new();
let mut out: Vec<PbConstraint> = Vec::new();
for &start in &order {
let mut clique = vec![start];
let mut cand: Vec<usize> = adj[start].iter().copied().collect();
cand.sort_by_key(|&v| std::cmp::Reverse(adj[v].len()));
for &v in &cand {
if clique.iter().all(|&u| adj[v].contains(&u)) {
clique.push(v);
}
}
if clique.len() < 2 {
continue;
}
clique.sort_unstable();
let mut fresh = false;
for i in 0..clique.len() {
for j in (i + 1)..clique.len() {
if covered.insert((clique[i], clique[j])) {
fresh = true;
}
}
}
if fresh {
out.push(PbConstraint::at_most(&clique.iter().map(|&v| (v, true)).collect::<Vec<_>>(), 1));
}
}
out
}
fn for_each_combo<F: FnMut(&[usize]) -> bool>(items: &[usize], width: usize, start: usize, cur: &mut Vec<usize>, f: &mut F) -> bool {
if cur.len() == width {
return f(cur);
}
for i in start..items.len() {
cur.push(items[i]);
let cont = for_each_combo(items, width, i + 1, cur, f);
cur.pop();
if !cont {
return false;
}
}
true
}
pub fn recover_at_most_k(num_vars: usize, clauses: &[Vec<Lit_>], k: usize) -> Vec<crate::pseudo_boolean::PbConstraint> {
use crate::pseudo_boolean::PbConstraint;
use std::collections::HashSet;
if k == 0 {
return Vec::new();
}
let width = k + 1;
let code = |l: &Lit_| (l.var() as usize) * 2 + usize::from(!l.is_positive()); let mut forbidden: HashSet<Vec<usize>> = HashSet::new();
let mut cand_set: HashSet<usize> = HashSet::new();
for c in clauses {
if c.len() != width {
continue;
}
let mut g: Vec<usize> = c.iter().map(|l| code(l) ^ 1).collect(); g.sort_unstable();
let distinct_vars = g.windows(2).all(|w| w[0] >> 1 != w[1] >> 1);
g.dedup();
if g.len() == width && distinct_vars && g.iter().all(|&lc| lc >> 1 < num_vars) {
for &lc in &g {
cand_set.insert(lc);
}
forbidden.insert(g);
}
}
if forbidden.is_empty() {
return Vec::new();
}
let mut cand: Vec<usize> = cand_set.iter().copied().collect();
cand.sort_unstable();
let mut seeds: Vec<Vec<usize>> = forbidden.iter().cloned().collect();
seeds.sort();
let mut covered: HashSet<Vec<usize>> = HashSet::new();
let mut out: Vec<PbConstraint> = Vec::new();
let mut budget: usize = 1_000_000;
const MAX_GROUP: usize = 96;
for seed in seeds {
if budget == 0 {
break;
}
if covered.contains(&seed) {
continue; }
let mut group = seed.clone();
let mut group_vars: HashSet<usize> = group.iter().map(|&lc| lc >> 1).collect();
for &v in &cand {
if group_vars.contains(&(v >> 1)) || group.len() >= MAX_GROUP {
continue; }
let mut starved = false;
let ok = for_each_combo(&group, k, 0, &mut Vec::with_capacity(k), &mut |sub| {
if budget == 0 {
starved = true;
return false;
}
budget -= 1;
let mut s = sub.to_vec();
s.push(v);
s.sort_unstable();
forbidden.contains(&s)
});
if starved {
break;
}
if ok {
group.push(v);
group.sort_unstable();
group_vars.insert(v >> 1);
}
}
let mut fresh = false;
for_each_combo(&group, width, 0, &mut Vec::with_capacity(width), &mut |sub| {
if budget == 0 {
return false;
}
budget -= 1;
if covered.insert(sub.to_vec()) {
fresh = true;
}
true
});
if fresh {
let lits: Vec<(usize, bool)> = group.iter().map(|&lc| (lc >> 1, lc & 1 == 0)).collect();
out.push(PbConstraint::at_most(&lits, k as i64));
}
}
out
}
pub const MAX_RECOVERED_CARDINALITY: usize = 4;
pub fn recover_cardinality_substructure(num_vars: usize, clauses: &[Vec<Lit_>]) -> Vec<crate::pseudo_boolean::PbConstraint> {
let mut out = Vec::new();
for k in 1..=MAX_RECOVERED_CARDINALITY {
out.extend(recover_at_most_k(num_vars, clauses, k));
}
out
}
pub fn fused_parity_cardinality_decide(num_vars: usize, clauses: &[Vec<Lit_>]) -> Option<bool> {
if num_vars == 0 {
return None;
}
let eqs = extract_xor(num_vars, clauses);
let amo = recover_cardinality_substructure(num_vars, clauses);
if eqs.is_empty() || amo.is_empty() {
return None;
}
let mut s = Solver::new(num_vars);
for c in clauses {
s.add_clause(c.clone());
}
let mut theories: Vec<Box<dyn crate::cdcl::Theory>> = vec![
Box::new(crate::xor_engine::XorEngine::new(num_vars, &eqs)),
Box::new(crate::pseudo_boolean::CardinalityTheory::new(num_vars, &amo)),
Box::new(SymmetryTheory::new(num_vars, fused_symmetry_group(num_vars, clauses))),
];
match s.solve_with(&mut theories) {
SolveResult::Sat(m) => clauses
.iter()
.all(|c| c.iter().any(|l| m[l.var() as usize] == l.is_positive()))
.then_some(true),
SolveResult::Unsat => Some(false),
}
}
struct SemanticSymmetry {
words: usize,
rhs_bit: usize,
basis: Vec<Vec<u64>>,
pivots: Vec<usize>,
eqs: Vec<(Vec<usize>, bool)>,
non_parity: Vec<Vec<Lit_>>,
}
fn gf2_row(words: usize, rhs_bit: usize, vars: &[usize], rhs: bool, perm: Option<&[usize]>) -> Vec<u64> {
let mut b = vec![0u64; words];
for &v in vars {
let idx = perm.map(|p| p[v]).unwrap_or(v);
b[idx / 64] ^= 1 << (idx % 64);
}
if rhs {
b[rhs_bit / 64] ^= 1 << (rhs_bit % 64);
}
b
}
fn gf2_reduce(v: &mut [u64], basis: &[Vec<u64>], pivots: &[usize]) {
for (row, &piv) in basis.iter().zip(pivots) {
if (v[piv / 64] >> (piv % 64)) & 1 == 1 {
for w in 0..v.len() {
v[w] ^= row[w];
}
}
}
}
fn gf2_lowest(v: &[u64]) -> Option<usize> {
v.iter().enumerate().find_map(|(w, &word)| (word != 0).then(|| w * 64 + word.trailing_zeros() as usize))
}
impl SemanticSymmetry {
fn new(num_vars: usize, clauses: &[Vec<Lit_>]) -> Self {
let eqs_raw = extract_xor(num_vars, clauses);
let rhs_bit = num_vars;
let words = num_vars.div_ceil(64) + 1;
let eqs: Vec<(Vec<usize>, bool)> = eqs_raw.iter().map(|e| (e.vars.clone(), e.rhs)).collect();
let mut xor_sets: std::collections::HashSet<Vec<usize>> = std::collections::HashSet::new();
for (vars, _) in &eqs {
let mut v = vars.clone();
v.sort_unstable();
v.dedup();
xor_sets.insert(v);
}
let non_parity: Vec<Vec<Lit_>> = clauses
.iter()
.filter(|c| {
let mut vs: Vec<usize> = c.iter().map(|l| l.var() as usize).collect();
vs.sort_unstable();
vs.dedup();
!xor_sets.contains(&vs)
})
.cloned()
.collect();
let mut basis: Vec<Vec<u64>> = Vec::new();
let mut pivots: Vec<usize> = Vec::new();
for (vars, rhs) in &eqs {
let mut v = gf2_row(words, rhs_bit, vars, *rhs, None);
gf2_reduce(&mut v, &basis, &pivots);
if let Some(p) = gf2_lowest(&v) {
basis.push(v);
pivots.push(p);
}
}
SemanticSymmetry { words, rhs_bit, basis, pivots, eqs, non_parity }
}
fn is_symmetry(&self, perm: &[usize]) -> bool {
for (vars, rhs) in &self.eqs {
let mut v = gf2_row(self.words, self.rhs_bit, vars, *rhs, Some(perm));
gf2_reduce(&mut v, &self.basis, &self.pivots);
if gf2_lowest(&v).is_some() {
return false; }
}
let sigma = Perm::from_images(perm.iter().map(|&v| Lit_::pos(v as u32)).collect());
perm_is_automorphism(&self.non_parity, &sigma)
}
}
pub struct CardinalitySeams {
pub joint: Vec<(usize, usize)>,
pub seams: Vec<(usize, usize)>,
}
pub fn cardinality_parity_seams(num_vars: usize, clauses: &[Vec<Lit_>]) -> CardinalitySeams {
use std::collections::HashSet;
const PAIR_BUDGET: usize = 20_000;
let cons = recover_cardinality_substructure(num_vars, clauses);
let checker = SemanticSymmetry::new(num_vars, clauses);
let mut seen: HashSet<(usize, usize)> = HashSet::new();
let mut pairs: Vec<(usize, usize)> = Vec::new();
for pb in &cons {
let mut vars: Vec<usize> = pb.terms().map(|(v, _, _)| v).collect();
vars.sort_unstable();
vars.dedup();
for i in 0..vars.len() {
for j in (i + 1)..vars.len() {
if seen.insert((vars[i], vars[j])) {
pairs.push((vars[i], vars[j]));
}
}
}
}
pairs.sort_unstable();
let mut joint = Vec::new();
let mut seams = Vec::new();
for &(a, b) in pairs.iter().take(PAIR_BUDGET) {
let mut perm: Vec<usize> = (0..num_vars).collect();
perm.swap(a, b);
if checker.is_symmetry(&perm) {
joint.push((a, b));
} else {
seams.push((a, b));
}
}
CardinalitySeams { joint, seams }
}
pub fn cardinality_symmetry_break(num_vars: usize, clauses: &[Vec<Lit_>]) -> Vec<Vec<Lit_>> {
use std::collections::BTreeSet;
let joint = cardinality_parity_seams(num_vars, clauses).joint;
if joint.is_empty() {
return Vec::new();
}
let mut parent: Vec<usize> = (0..num_vars).collect();
fn find(parent: &mut [usize], mut x: usize) -> usize {
while parent[x] != x {
parent[x] = parent[parent[x]];
x = parent[x];
}
x
}
for &(a, b) in &joint {
let (ra, rb) = (find(&mut parent, a), find(&mut parent, b));
if ra != rb {
parent[ra] = rb;
}
}
let mut members: std::collections::HashMap<usize, BTreeSet<usize>> = std::collections::HashMap::new();
for &(a, b) in &joint {
let r = find(&mut parent, a);
let set = members.entry(r).or_default();
set.insert(a);
set.insert(b);
}
let mut comps: Vec<Vec<usize>> = members.into_values().map(|s| s.into_iter().collect()).collect();
comps.sort();
let mut out = Vec::new();
for vs in comps {
for w in vs.windows(2) {
out.push(vec![Lit_::pos(w[0] as u32), Lit_::neg(w[1] as u32)]); }
}
out
}
pub fn cardinality_symmetry_generators(num_vars: usize, clauses: &[Vec<Lit_>]) -> Vec<Vec<usize>> {
let seams = cardinality_parity_seams(num_vars, clauses);
let checker = SemanticSymmetry::new(num_vars, clauses);
let is_auto = |perm: &[usize]| -> bool { checker.is_symmetry(perm) };
let mut gens: Vec<Vec<usize>> = Vec::new();
for &(a, b) in &seams.joint {
let mut p: Vec<usize> = (0..num_vars).collect();
p.swap(a, b);
gens.push(p);
}
let mut parent: Vec<usize> = (0..num_vars).collect();
fn find(p: &mut [usize], mut x: usize) -> usize {
while p[x] != x {
p[x] = p[p[x]];
x = p[x];
}
x
}
for &(a, b) in &seams.joint {
let (ra, rb) = (find(&mut parent, a), find(&mut parent, b));
if ra != rb {
parent[ra] = rb;
}
}
let mut orbmap: std::collections::HashMap<usize, std::collections::BTreeSet<usize>> = std::collections::HashMap::new();
for &(a, b) in &seams.joint {
let r = find(&mut parent, a);
let set = orbmap.entry(r).or_default();
set.insert(a);
set.insert(b);
}
let mut orbits: Vec<Vec<usize>> = orbmap.into_values().map(|s| s.into_iter().collect()).collect();
orbits.sort();
let mut blocks: Vec<Vec<usize>> = orbits;
let mut budget: usize = 4000;
loop {
let n = blocks.len();
if n < 2 || budget == 0 {
break;
}
let mut bp: Vec<usize> = (0..n).collect();
let mut level_gens: Vec<Vec<usize>> = Vec::new();
'lvl: for i in 0..n {
for j in (i + 1)..n {
if budget == 0 {
break 'lvl;
}
if blocks[i].len() != blocks[j].len() {
continue;
}
let (ri, rj) = (find(&mut bp, i), find(&mut bp, j));
if ri == rj {
continue; }
budget -= 1;
let mut p: Vec<usize> = (0..num_vars).collect();
for (&a, &b) in blocks[i].iter().zip(&blocks[j]) {
p.swap(a, b);
}
if is_auto(&p) {
bp[ri] = rj; level_gens.push(p);
}
}
}
if level_gens.is_empty() {
break; }
gens.extend(level_gens);
let mut classes: std::collections::BTreeMap<usize, Vec<usize>> = std::collections::BTreeMap::new();
for i in 0..n {
classes.entry(find(&mut bp, i)).or_default().push(i);
}
let mut next: Vec<Vec<usize>> = Vec::new();
for (_, idxs) in classes {
let mut mem: Vec<Vec<usize>> = idxs.iter().map(|&i| blocks[i].clone()).collect();
mem.sort_by_key(|b| b[0]);
next.push(mem.into_iter().flatten().collect());
}
next.sort();
blocks = next;
}
gens
}
pub fn affine_parity_symmetries(num_vars: usize, clauses: &[Vec<Lit_>]) -> Vec<Vec<(Vec<usize>, bool)>> {
if num_vars == 0 || num_vars > 64 {
return Vec::new();
}
let eqs = extract_xor(num_vars, clauses);
if eqs.is_empty() {
return Vec::new();
}
let rows: Vec<u64> = eqs.iter().map(|e| e.vars.iter().fold(0u64, |a, &v| a | (1u64 << v))).collect();
let rhs: Vec<bool> = eqs.iter().map(|e| e.rhs).collect();
let Some(space) = crate::gf2::solve_gf2(num_vars, &rows, &rhs) else {
return Vec::new(); };
let mut xor_sets: std::collections::HashSet<Vec<usize>> = std::collections::HashSet::new();
for e in &eqs {
let mut v = e.vars.clone();
v.sort_unstable();
v.dedup();
xor_sets.insert(v);
}
let mut moved_forbidden = vec![false; num_vars]; for c in clauses {
let mut vs: Vec<usize> = c.iter().map(|l| l.var() as usize).collect();
vs.sort_unstable();
vs.dedup();
if !xor_sets.contains(&vs) {
for &v in &vs {
moved_forbidden[v] = true;
}
}
}
let mut points: Vec<Vec<bool>> = vec![space.particular.clone()];
for k in &space.kernel_basis {
let mut p = space.particular.clone();
for v in 0..num_vars {
p[v] ^= k[v];
}
points.push(p);
}
let satisfies = |x: &[bool]| eqs.iter().all(|e| e.vars.iter().fold(false, |a, &v| a ^ x[v]) == e.rhs);
let make_map = |moved: &[usize], source: Option<usize>| -> Option<Vec<(Vec<usize>, bool)>> {
if moved.is_empty() || moved.iter().any(|&i| moved_forbidden[i]) {
return None;
}
let preserves = points.iter().all(|x| {
let add = source.map_or(true, |j| x[j]);
if !add {
return true;
}
let mut y = x.clone();
for &i in moved {
y[i] ^= true;
}
satisfies(&y)
});
if !preserves {
return None;
}
let mut spec: Vec<(Vec<usize>, bool)> = (0..num_vars).map(|k| (vec![k], false)).collect();
for &i in moved {
spec[i] = match source {
Some(j) => (vec![i, j], false),
None => (vec![i], true),
};
}
Some(spec)
};
let mut maps: Vec<Vec<(Vec<usize>, bool)>> = Vec::new();
for i in 0..num_vars {
for j in 0..num_vars {
if j != i {
if let Some(m) = make_map(&[i], Some(j)) {
maps.push(m);
}
}
}
}
for kappa in kernel_intersect_p(&space.kernel_basis, &moved_forbidden, num_vars) {
let support: Vec<usize> = (0..num_vars).filter(|&i| kappa[i]).collect();
if let Some(m) = make_map(&support, None) {
maps.push(m);
}
for j in 0..num_vars {
if !kappa[j] {
if let Some(m) = make_map(&support, Some(j)) {
maps.push(m);
}
}
}
}
maps.sort();
maps.dedup();
maps
}
fn kernel_intersect_p(kernel_basis: &[Vec<bool>], moved_forbidden: &[bool], num_vars: usize) -> Vec<Vec<bool>> {
let d = kernel_basis.len();
if d == 0 || d > 64 {
return Vec::new();
}
let forbidden: Vec<usize> = (0..num_vars).filter(|&c| moved_forbidden[c]).collect();
let rows: Vec<u64> =
forbidden.iter().map(|&c| (0..d).fold(0u64, |acc, t| if kernel_basis[t][c] { acc | (1u64 << t) } else { acc })).collect();
let rhs = vec![false; rows.len()];
let Some(null) = crate::gf2::solve_gf2(d, &rows, &rhs) else {
return Vec::new();
};
null.kernel_basis
.iter()
.map(|a| {
let mut kappa = vec![false; num_vars];
for (t, &at) in a.iter().enumerate().take(d) {
if at {
for v in 0..num_vars {
kappa[v] ^= kernel_basis[t][v];
}
}
}
kappa
})
.collect()
}
pub fn all_transposition_symmetries(num_vars: usize, clauses: &[Vec<Lit_>]) -> Vec<Vec<usize>> {
if num_vars < 2 || num_vars > 64 {
return Vec::new();
}
let checker = SemanticSymmetry::new(num_vars, clauses);
let mut gens: Vec<Vec<usize>> = Vec::new();
const BUDGET: usize = 4096;
let mut checks = 0usize;
'a: for a in 0..num_vars {
for b in (a + 1)..num_vars {
if checks >= BUDGET {
break 'a;
}
checks += 1;
let mut p: Vec<usize> = (0..num_vars).collect();
p.swap(a, b);
if checker.is_symmetry(&p) {
gens.push(p);
}
}
}
gens
}
#[derive(Clone, PartialEq, Eq, Hash)]
struct AffineMap {
rows: Vec<u64>,
trans: u64,
}
impl AffineMap {
fn identity(n: usize) -> Self {
AffineMap { rows: (0..n).map(|j| 1u64 << j).collect(), trans: 0 }
}
fn from_perm(p: &[usize]) -> Self {
AffineMap { rows: p.iter().map(|&pj| 1u64 << pj).collect(), trans: 0 }
}
fn from_spec(spec: &[(Vec<usize>, bool)], n: usize) -> Self {
let mut rows = vec![0u64; n];
let mut trans = 0u64;
for (j, (xs, b)) in spec.iter().enumerate().take(n) {
rows[j] = xs.iter().fold(0u64, |a, &v| a | (1u64 << v));
if *b {
trans |= 1u64 << j;
}
}
AffineMap { rows, trans }
}
fn is_identity(&self) -> bool {
self.trans == 0 && self.rows.iter().enumerate().all(|(j, &r)| r == (1u64 << j))
}
fn compose(&self, other: &AffineMap) -> AffineMap {
let n = self.rows.len();
let mut rows = vec![0u64; n];
let mut trans = 0u64;
for j in 0..n {
let (mut r, mut t) = (0u64, (self.trans >> j) & 1);
let mut sr = self.rows[j];
while sr != 0 {
let k = sr.trailing_zeros() as usize;
sr &= sr - 1;
r ^= other.rows[k];
t ^= (other.trans >> k) & 1;
}
rows[j] = r;
trans |= t << j;
}
AffineMap { rows, trans }
}
fn to_spec(&self) -> Vec<(Vec<usize>, bool)> {
self.rows
.iter()
.enumerate()
.map(|(j, &r)| ((0..64).filter(|&b| (r >> b) & 1 == 1).collect(), (self.trans >> j) & 1 == 1))
.collect()
}
}
fn affine_group_closure(gens: &[AffineMap], num_vars: usize, cap: usize) -> Option<Vec<AffineMap>> {
use std::collections::HashSet;
let id = AffineMap::identity(num_vars);
let mut seen: HashSet<AffineMap> = HashSet::from([id.clone()]);
let mut frontier = vec![id];
while let Some(g) = frontier.pop() {
for gen in gens {
let h = g.compose(gen);
if seen.insert(h.clone()) {
if seen.len() > cap {
return None;
}
frontier.push(h);
}
}
}
Some(seen.into_iter().collect())
}
fn signed_perm_to_spec(p: &crate::proof::Perm, num_vars: usize) -> Vec<(Vec<usize>, bool)> {
let mut spec: Vec<(Vec<usize>, bool)> = (0..num_vars).map(|w| (vec![w], false)).collect();
for v in 0..num_vars {
let img = p.apply(Lit::pos(v as u32));
let w = img.var() as usize;
if w < num_vars {
spec[w] = (vec![v], !img.is_positive());
}
}
spec
}
pub fn fused_symmetry_group(num_vars: usize, clauses: &[Vec<Lit_>]) -> Vec<Vec<(Vec<usize>, bool)>> {
let perm_gens = fused_permutation_generators(num_vars, clauses);
if num_vars < 1 || num_vars > 64 {
return perm_gens.iter().map(|p| p.iter().map(|&pi| (vec![pi], false)).collect()).collect();
}
let aff_specs = affine_parity_symmetries(num_vars, clauses);
let syntactic = crate::symmetry_detect::find_generators(num_vars, clauses);
let mut gens: Vec<AffineMap> = perm_gens.iter().map(|p| AffineMap::from_perm(p)).collect();
gens.extend(aff_specs.iter().map(|s| AffineMap::from_spec(s, num_vars)));
gens.extend(syntactic.iter().map(|p| AffineMap::from_spec(&signed_perm_to_spec(p, num_vars), num_vars)));
gens.retain(|g| !g.is_identity());
if gens.is_empty() {
return Vec::new();
}
const CAP: usize = 2048;
match affine_group_closure(&gens, num_vars, CAP) {
Some(group) => group.iter().filter(|g| !g.is_identity()).map(|g| g.to_spec()).collect(),
None => gens.iter().map(|g| g.to_spec()).collect(), }
}
pub fn fused_permutation_generators(num_vars: usize, clauses: &[Vec<Lit_>]) -> Vec<Vec<usize>> {
let mut g = cardinality_symmetry_generators(num_vars, clauses);
g.extend(all_transposition_symmetries(num_vars, clauses));
g
}
pub struct SymmetryTheory {
num_vars: usize,
maps: Vec<Vec<(Vec<usize>, bool)>>,
}
impl SymmetryTheory {
pub fn new(num_vars: usize, maps: Vec<Vec<(Vec<usize>, bool)>>) -> Self {
SymmetryTheory { num_vars, maps }
}
pub fn from_perms(num_vars: usize, perms: Vec<Vec<usize>>) -> Self {
let maps = perms.into_iter().map(|p| p.into_iter().map(|pi| (vec![pi], false)).collect()).collect();
SymmetryTheory { num_vars, maps }
}
}
impl crate::cdcl::Theory for SymmetryTheory {
fn propagate(&mut self, trail: &[crate::cdcl::Lit]) -> Vec<Vec<crate::cdcl::Lit>> {
use crate::cdcl::Lit;
let n = self.num_vars;
let mut a: Vec<Option<bool>> = vec![None; n];
for &l in trail {
let v = l.var() as usize;
if v < n {
a[v] = Some(l.is_positive());
}
}
let support_witness = |xset: &[usize], a: &[Option<bool>]| -> Vec<Lit> {
xset.iter().filter_map(|&s| a[s].map(|sv| Lit::new(s as u32, !sv))).collect()
};
let mut out = Vec::new();
for map in &self.maps {
let mut prefix: Vec<Lit> = Vec::new();
for i in 0..n {
let (xset, b) = &map[i];
if xset.len() == 1 && xset[0] == i && !*b {
continue; }
let mut val = *b;
let mut free: Vec<usize> = Vec::new();
for &s in xset {
match a[s] {
Some(sv) => val ^= sv,
None => free.push(s),
}
}
match (a[i], free.len()) {
(Some(vi), 0) if vi == val => {
prefix.push(Lit::new(i as u32, !vi));
prefix.extend(support_witness(xset, &a));
}
(Some(false), 0) => break, (Some(true), 0) => {
let mut c = prefix.clone();
c.extend(support_witness(xset, &a));
c.push(Lit::new(i as u32, false));
out.push(c);
break;
}
(None, 0) if !val => {
let mut c = prefix.clone();
c.extend(support_witness(xset, &a));
c.push(Lit::new(i as u32, false));
out.push(c);
break;
}
(Some(true), 1) => {
let s = free[0];
let mut c = prefix.clone();
c.push(Lit::new(i as u32, false)); for &o in xset {
if o != s {
c.push(Lit::new(o as u32, !a[o].unwrap()));
}
}
c.push(Lit::new(s as u32, !val)); out.push(c);
break;
}
_ => break, }
}
}
out
}
}
#[derive(Clone, Debug)]
pub enum AutoCollapse {
Geometric { measure: CollapsingMeasure, ranked: RankedRefutation },
Cardinality { trajectory: Vec<u64>, reached_goal: bool, constraints: usize },
Algebraic { trajectory: Vec<u64>, reached_goal: bool, xor_equations: usize },
None,
}
pub fn auto_collapse(num_vars: usize, formula: &[Vec<Lit_>]) -> AutoCollapse {
if let Some((measure, ranked)) = solve_by_measure_synthesis(num_vars, formula) {
if ranked.refuted {
return AutoCollapse::Geometric { measure, ranked };
}
}
if let Some((trajectory, reached_goal, constraints)) = cardinality_collapse(num_vars, formula) {
if reached_goal {
return AutoCollapse::Cardinality { trajectory, reached_goal, constraints };
}
}
let eqs = extract_xor(num_vars, formula);
if !eqs.is_empty() {
let (trajectory, reached_goal) = gaussian_lyapunov(&eqs, num_vars);
if reached_goal {
return AutoCollapse::Algebraic { trajectory, reached_goal, xor_equations: eqs.len() };
}
}
AutoCollapse::None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cdcl::Lit;
use crate::families;
#[test]
fn discovers_the_pigeonhole_measure_without_being_told() {
for n in 3..=7 {
let (cnf, _) = families::php(n);
let (measure, ranked) = solve_by_measure_synthesis(cnf.num_vars, &cnf.clauses)
.unwrap_or_else(|| panic!("must synthesize a measure for PHP({n})"));
assert_eq!((measure.items, measure.bins), (n, n - 1), "discovered the pigeonhole shape");
assert!(ranked.refuted, "the collapse must refute");
let bound = ranked
.certify(cnf.num_vars, &cnf.clauses)
.expect("the fallen-out proof certifies correctness AND its own size");
assert!(bound.bound <= (n as u64) * (n as u64), "self-certified O(n²)");
}
}
#[test]
fn discovers_the_clique_coloring_measure() {
for (n, k) in [(5, 4), (7, 6), (9, 8)] {
let (cnf, _) = families::clique_coloring(n, k);
let (measure, ranked) = solve_by_measure_synthesis(cnf.num_vars, &cnf.clauses)
.unwrap_or_else(|| panic!("must synthesize a measure for clique({n},{k})"));
assert_eq!((measure.items, measure.bins), (n, k), "discovered the coloring shape");
assert!(ranked.refuted);
assert!(ranked.certify(cnf.num_vars, &cnf.clauses).is_some());
}
}
#[test]
fn honest_impossibility_when_no_covering_measure_exists() {
let p = |v: u32| Lit::pos(v);
let n = |v: u32| Lit::neg(v);
let f = vec![vec![p(0), p(1)], vec![n(0), p(1)], vec![n(1)]];
assert!(solve_by_measure_synthesis(2, &f).is_none(), "no covering collapse should be claimed");
}
#[test]
fn characterization_measure_cost_equals_proof_size() {
for n in 4..=7 {
let (cnf, _) = families::php(n);
let m = CoveringMeasure {
num_vars: cnf.num_vars,
formula: cnf.clauses.clone(),
items: n,
bins: n - 1,
};
let ranked = proof_from_measure(&m);
assert!(ranked.refuted);
let proof_size = ranked.steps.len();
assert!(proof_size as u64 <= m.initial_potential() * m.width(), "⟸ : proof ≤ L·w");
let induced = proof_induced_measure(proof_size);
let cert = verify_lyapunov(&induced, ranked.refuted).expect("the proof induces a measure");
assert_eq!(cert.total_steps as usize, proof_size, "⟹ : induced measure cost = proof size");
}
}
#[test]
fn no_measure_is_a_checkable_bounded_lower_bound_witness() {
let mut none_count = 0;
for seed in 0u64..16 {
let cnf = families::random_3sat(18, 80, seed); if matches!(auto_collapse(cnf.num_vars, &cnf.clauses), AutoCollapse::None) {
none_count += 1;
}
}
assert!(
none_count >= 13,
"most hard random instances have no measure in our classes (got {none_count}/16)"
);
}
#[test]
fn compose_collapses_wires_stages_into_one_certified_descent() {
for n in [6usize, 7, 8] {
let (cnf, _) = families::php(n);
let base = CoveringMeasure {
num_vars: cnf.num_vars,
formula: cnf.clauses.clone(),
items: n,
bins: n - 1,
};
let mid = n / 2 + 1;
let s1 = PartialCoveringMeasure { base: base.clone(), lo: mid, hi: n };
let s2 = PartialCoveringMeasure { base: base.clone(), lo: 2, hi: mid - 1 };
let composite = compose_collapses(cnf.num_vars, &cnf.clauses, &[&s1, &s2]);
assert!(composite.refuted, "PHP({n}) composite must refute");
assert!(
crate::pr::check_pr_refutation_fast(cnf.num_vars, &cnf.clauses, &composite.steps),
"the composite re-checks against the original formula"
);
let cert = verify_lyapunov(&composite.ranks, composite.refuted)
.expect("the combined banded potential is a valid Lyapunov certificate");
assert!(
cert.monotone && cert.strict_descent && cert.reaches_goal,
"wiring preserves the descent across the stage boundary"
);
let pr_steps =
composite.steps.iter().filter(|s| matches!(s, ProofStep::Pr { .. })).count();
assert!(pr_steps > 0, "both stages contributed certified steps");
}
}
#[test]
fn three_physics_one_checker_and_pigeonhole_has_two_measures() {
let n = 7;
let (php, _) = families::php(n);
let (_, ranked) = solve_by_measure_synthesis(php.num_vars, &php.clauses).unwrap();
let sym = lyapunov_of_symmetry(&ranked).expect("PHP has a symmetry Lyapunov measure");
let (cp_traj, cp_reached) = cutting_planes_lyapunov(n);
let cp = verify_lyapunov(&cp_traj, cp_reached)
.expect("PHP also has a cutting-planes Lyapunov measure");
assert!(sym.total_steps != cp.total_steps || sym.levels != cp.levels, "the two measures differ");
assert!(cp.reaches_goal && cp.strict_descent, "the cutting-planes descent is a valid Lyapunov fn");
let (eqs, tcnf, _) = families::tseitin_expander(10, 7);
let (gx, gr) = gaussian_lyapunov(&eqs, tcnf.num_vars);
assert!(verify_lyapunov(&gx, gr).is_some(), "Tseitin has a parity Lyapunov measure");
}
#[test]
fn unified_agent_routes_a_whole_suite_correctly() {
for n in [4usize, 5, 6, 7] {
let (php, _) = families::php(n);
assert!(
matches!(auto_collapse(php.num_vars, &php.clauses), AutoCollapse::Geometric { .. }),
"PHP({n}) ⇒ geometric"
);
}
for (n, k) in [(5usize, 4usize), (6, 5), (7, 6), (6, 3), (8, 5)] {
let (cq, _) = families::clique_coloring(n, k);
assert!(
matches!(auto_collapse(cq.num_vars, &cq.clauses), AutoCollapse::Geometric { .. }),
"clique({n},{k}) ⇒ geometric"
);
}
for seed in [1u64, 7, 42, 99] {
let (_, ts, _) = families::tseitin_expander(10, seed);
assert!(
matches!(auto_collapse(ts.num_vars, &ts.clauses), AutoCollapse::Algebraic { .. }),
"Tseitin(seed={seed}) ⇒ algebraic"
);
}
}
#[test]
fn extract_xor_recovers_parity_structure_from_cnf_gadgets() {
for seed in [1u64, 7, 42, 100] {
let (_, cnf, _) = families::tseitin_expander(12, seed);
let eqs = extract_xor(cnf.num_vars, &cnf.clauses);
assert!(!eqs.is_empty(), "must recover XOR constraints from the CNF gadgets");
let (_, reached) = gaussian_lyapunov(&eqs, cnf.num_vars);
assert!(reached, "the recovered parity system must expose the contradiction");
}
}
#[test]
fn auto_collapse_recognizes_and_routes_both_physics() {
let (php, _) = families::php(6);
match auto_collapse(php.num_vars, &php.clauses) {
AutoCollapse::Geometric { ranked, measure } => {
assert!(ranked.refuted, "geometric collapse must refute");
assert_eq!((measure.items, measure.bins), (6, 5), "discovered the pigeonhole shape");
}
other => panic!("PHP must route to the geometric collapse, got {other:?}"),
}
let (_, tseitin, _) = families::tseitin_expander(12, 7);
match auto_collapse(tseitin.num_vars, &tseitin.clauses) {
AutoCollapse::Algebraic { reached_goal, xor_equations, .. } => {
assert!(reached_goal, "algebraic collapse must reach the contradiction");
assert!(xor_equations > 0, "must have routed through the recovered parity system");
}
other => panic!("Tseitin must route to the algebraic collapse, got {other:?}"),
}
}
#[test]
fn auto_collapse_is_sound_never_a_false_collapse() {
let (php, _) = families::php(5);
if let AutoCollapse::Geometric { ranked, .. } = auto_collapse(php.num_vars, &php.clauses) {
assert!(crate::pr::check_pr_refutation_fast(php.num_vars, &php.clauses, &ranked.steps));
}
let (_, ts, _) = families::tseitin_expander(10, 42);
if let AutoCollapse::Algebraic { trajectory, reached_goal, .. } =
auto_collapse(ts.num_vars, &ts.clauses)
{
assert!(verify_lyapunov(&trajectory, reached_goal).is_some(), "valid Lyapunov descent");
}
}
#[test]
fn theorem_poly_measure_implies_poly_checkable_proof() {
let cases: Vec<CoveringMeasure> = vec![
{
let (cnf, _) = families::php(7);
CoveringMeasure { num_vars: cnf.num_vars, formula: cnf.clauses, items: 7, bins: 6 }
},
{
let (cnf, _) = families::clique_coloring(8, 7);
CoveringMeasure { num_vars: cnf.num_vars, formula: cnf.clauses, items: 8, bins: 7 }
},
{
let (cnf, _) = families::clique_coloring(9, 4);
CoveringMeasure { num_vars: cnf.num_vars, formula: cnf.clauses, items: 9, bins: 4 }
},
];
for m in &cases {
let l = m.initial_potential();
let w = m.width();
let ranked = proof_from_measure(m);
assert!(ranked.refuted, "the measure-driven construction must refute");
assert!(
crate::pr::check_pr_refutation_fast(m.num_vars, &m.formula, &ranked.steps),
"the produced proof re-checks against the original formula"
);
let descent_steps =
ranked.steps.iter().filter(|s| matches!(s, ProofStep::Pr { .. })).count() as u64;
assert!(descent_steps <= l * w, "descent {descent_steps} must be ≤ L·w = {}", l * w);
assert!(verify_lyapunov(&ranked.ranks, ranked.refuted).is_some());
}
}
#[test]
fn killer_question_the_measure_transcends_resolution() {
for n in [8usize, 12, 16] {
let (cnf, _) = families::php(n);
let m = CoveringMeasure { num_vars: cnf.num_vars, formula: cnf.clauses, items: n, bins: n - 1 };
let ranked = proof_from_measure(&m);
assert!(ranked.refuted);
let descent =
ranked.steps.iter().filter(|s| matches!(s, ProofStep::Pr { .. })).count();
assert!(descent <= n * n, "PHP({n}) measure proof is ≤ n² = {} (resolution: 2^Ω(n))", n * n);
}
}
#[test]
fn one_lyapunov_framework_certifies_both_collapse_mechanisms() {
for n in 3..=6 {
let (cnf, _) = families::php(n);
let (_, ranked) = solve_by_measure_synthesis(cnf.num_vars, &cnf.clauses).unwrap();
let cert = lyapunov_of_symmetry(&ranked).expect("PHP carries a valid Lyapunov function");
assert!(cert.monotone && cert.strict_descent && cert.reaches_goal, "all 4 axioms hold");
assert!(cert.total_steps <= cert.size_bound, "descent bounds the size");
assert!(cert.minimum < cert.initial, "the potential genuinely descends from start to goal");
}
for seed in [1u64, 7, 42] {
let (eqs, cnf, _) = families::tseitin_expander(10, seed);
let (traj, reached) = gaussian_lyapunov(&eqs, cnf.num_vars);
let cert = verify_lyapunov(&traj, reached)
.expect("the Tseitin Gaussian collapse carries a valid Lyapunov function");
assert!(cert.reaches_goal && cert.strict_descent, "the dimension strictly descends to ⊥");
assert_eq!(cert.minimum, 0, "the dimension bottoms out at the 0=1 contradiction");
}
}
#[test]
fn verify_lyapunov_is_sound_and_complete_on_random_trajectories() {
let mut state = 0x5151_A5A5_3C3C_9696u64;
let mut next = || {
state = state.wrapping_add(0x9E3779B97F4A7C15);
let mut z = state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
z ^ (z >> 31)
};
let brute = |v: &[u64]| -> bool {
if v.is_empty() {
return false;
}
let monotone = v.windows(2).all(|w| w[1] <= w[0]);
let mut d = v.to_vec();
d.dedup();
let strict = d.windows(2).all(|w| w[1] < w[0]);
monotone && strict
};
let mut accepts = 0;
for _ in 0..20_000 {
let len = 1 + (next() as usize % 8);
let traj: Vec<u64> = (0..len).map(|_| next() % 6).collect();
let reaches = next() & 1 == 0;
let got = verify_lyapunov(&traj, reaches);
assert_eq!(
got.is_some(),
brute(&traj) && reaches,
"verify_lyapunov must accept exactly the valid Lyapunov trajectories: {traj:?} reaches={reaches}"
);
if let Some(c) = got {
assert!(c.total_steps <= c.size_bound, "accepted ⇒ size bound holds: {traj:?}");
accepts += 1;
}
}
assert!(accepts > 0, "the soundness fuzz must exercise genuine acceptances");
}
#[test]
fn a_synthesized_refutation_is_never_unsound() {
for n in 3..=6 {
let (cnf, _) = families::php(n);
if let Some((_, ranked)) = solve_by_measure_synthesis(cnf.num_vars, &cnf.clauses) {
assert!(
crate::pr::check_pr_refutation_fast(cnf.num_vars, &cnf.clauses, &ranked.steps),
"a synthesized PHP({n}) refutation must re-check"
);
}
}
}
fn cl(lits: &[i32]) -> Vec<Lit> {
lits.iter()
.map(|&l| if l > 0 { Lit::pos((l - 1) as u32) } else { Lit::neg((-l - 1) as u32) })
.collect()
}
#[test]
fn cardinality_collapse_refutes_pigeonhole_by_cutting_planes() {
for n in 3..=7 {
let (cnf, _) = families::php(n);
let (traj, reached, constraints) =
cardinality_collapse(cnf.num_vars, &cnf.clauses).expect("PHP is a covering");
assert!(reached, "PHP({n}) must reach 0≥1 by cutting planes");
assert_eq!(constraints, 2 * n - 1, "n rows + (n-1) columns summed");
assert_eq!(*traj.last().unwrap(), 0, "the descent bottoms out at 0");
}
}
#[test]
fn auto_collapse_routes_an_asymmetric_covering_to_cardinality() {
let formula = vec![
cl(&[1, 2]),
cl(&[3, 4]),
cl(&[5]), cl(&[-1, -3]),
cl(&[-1, -5]),
cl(&[-3, -5]), cl(&[-2, -4]), ];
let nv = 5;
assert!(
solve_by_measure_synthesis(nv, &formula).is_none(),
"this asymmetric covering has no covering symmetry to discover"
);
match auto_collapse(nv, &formula) {
AutoCollapse::Cardinality { reached_goal, constraints, .. } => {
assert!(reached_goal, "cutting planes must reach 0≥1 (3 items, 2 bins)");
assert_eq!(constraints, 5, "3 rows + 2 columns");
}
other => panic!("expected Cardinality collapse, got {other:?}"),
}
}
#[test]
fn cardinality_collapse_is_sound_on_a_feasible_covering() {
let formula = vec![cl(&[1, 2]), cl(&[3, 4]), cl(&[-1, -3]), cl(&[-2, -4])];
let (_, reached, _) = cardinality_collapse(4, &formula).expect("is a covering");
assert!(!reached, "a feasible (items ≤ bins) covering must not yield a contradiction");
assert!(
matches!(auto_collapse(4, &formula), AutoCollapse::None),
"no collapse may be claimed on a satisfiable covering"
);
}
#[test]
fn discover_covering_rejects_non_covering_shapes() {
let shared = vec![cl(&[1, 2]), cl(&[1, 3])];
assert!(discover_covering(3, &shared).is_none(), "a variable in two rows is not a covering");
let partial = vec![cl(&[1, 4]), cl(&[2, 5]), cl(&[3, 6]), cl(&[-1, -2]), cl(&[-2, -3])];
assert!(discover_covering(6, &partial).is_none(), "a non-clique column must be rejected");
}
#[test]
fn recover_cardinality_recovers_the_php_covering() {
let (cnf, _) = families::php(4);
let cons = recover_cardinality_constraints(cnf.num_vars, &cnf.clauses).expect("PHP is a clean covering");
assert_eq!(cons.len(), 4 + 3, "4 pigeon rows + 3 hole columns");
let rnd = families::random_3sat(20, 80, 0xBEEF);
assert!(recover_cardinality_constraints(rnd.num_vars, &rnd.clauses).is_none(), "non-covering ⇒ None");
}
#[test]
fn recovered_constraints_are_implied_by_the_cnf() {
let cnf = vec![
vec![Lit::pos(0), Lit::pos(1)],
vec![Lit::pos(2), Lit::pos(3)],
vec![Lit::neg(0), Lit::neg(2)],
vec![Lit::neg(1), Lit::neg(3)],
];
let cons = recover_cardinality_constraints(4, &cnf).expect("a clean covering");
for x in 0u64..(1 << 4) {
let model_sat = cnf.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 == 1) == l.is_positive()));
if !model_sat {
continue;
}
for pb in &cons {
let sum: i64 = pb.terms().map(|(v, c, s)| if (((x >> v) & 1 == 1) == s) { c } else { 0 }).sum();
assert!(sum >= pb.degree(), "recovered constraint {pb:?} must hold in model {x:04b}");
}
}
}
#[test]
fn live_cardinality_theory_refutes_php_from_recovered_constraints() {
use crate::pseudo_boolean::CardinalityTheory;
for n in 3..=5 {
let (cnf, _) = families::php(n);
let cons = recover_cardinality_constraints(cnf.num_vars, &cnf.clauses).expect("PHP covering");
let mut s = Solver::new(cnf.num_vars);
let mut t: Vec<Box<dyn crate::cdcl::Theory>> = vec![Box::new(CardinalityTheory::new(cnf.num_vars, &cons))];
assert!(matches!(s.solve_with(&mut t), SolveResult::Unsat), "PHP({n}) is UNSAT via recovered cardinality");
}
}
fn xor_gadget(vars: &[u32], rhs: bool) -> Vec<Vec<Lit>> {
let k = vars.len();
(0u32..(1 << k))
.filter(|mask| ((mask.count_ones() % 2) == 1) != rhs)
.map(|mask| (0..k).map(|i| Lit::new(vars[i], (mask >> i) & 1 == 0)).collect())
.collect()
}
#[test]
fn recover_at_most_one_extracts_a_clique_from_mixed_clauses() {
let mut clauses: Vec<Vec<Lit>> = vec![
vec![Lit::neg(0), Lit::neg(1)],
vec![Lit::neg(0), Lit::neg(2)],
vec![Lit::neg(1), Lit::neg(2)], ];
clauses.extend(xor_gadget(&[3, 4], false)); let amo = recover_at_most_one(5, &clauses);
assert_eq!(amo.len(), 1, "exactly one at-most-one group; got {amo:?}");
assert!(discover_covering(5, &clauses).is_none(), "the whole-formula recognizer declines on the mix");
for x in 0u64..(1 << 5) {
let sat = clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 == 1) == l.is_positive()));
if !sat {
continue;
}
for pb in &amo {
let sum: i64 = pb.terms().map(|(v, c, s)| if (((x >> v) & 1 == 1) == s) { c } else { 0 }).sum();
assert!(sum >= pb.degree(), "recovered {pb:?} must hold in model {x:05b}");
}
}
}
#[test]
fn fused_decide_refutes_a_mixed_parity_cardinality_instance() {
let mut clauses: Vec<Vec<Lit>> = vec![
vec![Lit::pos(0), Lit::pos(1), Lit::pos(2)], vec![Lit::neg(0), Lit::neg(1)],
vec![Lit::neg(0), Lit::neg(2)],
vec![Lit::neg(1), Lit::neg(2)], ];
for i in 0..3u32 {
clauses.extend(xor_gadget(&[i, i + 3], false)); }
clauses.extend(xor_gadget(&[3, 4, 5], false)); assert!(!extract_xor(6, &clauses).is_empty(), "a parity substructure is present");
assert!(!recover_at_most_one(6, &clauses).is_empty(), "a cardinality substructure is present");
assert_eq!(fused_parity_cardinality_decide(6, &clauses), Some(false), "the mixed instance is UNSAT");
let brute = (0u64..(1 << 6)).any(|x| clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 == 1) == l.is_positive())));
assert!(!brute, "brute force agrees it is UNSAT");
}
#[test]
fn fused_decide_matches_brute_force() {
let mut st = 0xF00D_CAFEu64;
let mut rng = || {
st ^= st << 13;
st ^= st >> 7;
st ^= st << 17;
st
};
for _ in 0..200 {
let n = 6usize;
let mut clauses: Vec<Vec<Lit>> = Vec::new();
let k = 2 + (rng() % 2) as usize;
let pvars: Vec<u32> = (0..k as u32).collect();
clauses.extend(xor_gadget(&pvars, rng() % 2 == 0));
let cvars: Vec<u32> = vec![3, 4, 5].into_iter().filter(|_| rng() % 2 == 0).collect();
let width = if rng() % 2 == 0 { 2 } else { 3 };
if cvars.len() >= width {
for_each_combo(&cvars.iter().map(|&v| v as usize).collect::<Vec<_>>(), width, 0, &mut Vec::new(), &mut |sub| {
clauses.push(sub.iter().map(|&v| Lit::neg(v as u32)).collect());
true
});
}
for _ in 0..(rng() % 4) {
let mut c: Vec<Lit> = Vec::new();
for v in 0..n as u32 {
if rng() % 3 == 0 {
c.push(Lit::new(v, rng() % 2 == 0));
}
}
if !c.is_empty() {
clauses.push(c);
}
}
if let Some(verdict) = fused_parity_cardinality_decide(n, &clauses) {
let brute = (0u64..(1 << n)).any(|x| clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 == 1) == l.is_positive())));
assert_eq!(verdict, brute, "fused verdict must match brute (clauses={clauses:?})");
}
}
}
#[test]
fn fused_decide_refutes_the_scalable_parity_exactly_one_family() {
for n in [4usize, 6, 8, 10, 12] {
let (cnf, verdict) = families::parity_exactly_one(n);
assert_eq!(verdict, families::ExpectedVerdict::Unsat, "the family is UNSAT by construction");
assert!(!extract_xor(cnf.num_vars, &cnf.clauses).is_empty(), "n={n}: a parity substructure is present");
assert!(!recover_at_most_one(cnf.num_vars, &cnf.clauses).is_empty(), "n={n}: a cardinality substructure is present");
assert_eq!(
fused_parity_cardinality_decide(cnf.num_vars, &cnf.clauses),
Some(false),
"n={n}: the fused parity+cardinality route refutes it",
);
if cnf.num_vars <= 16 {
let brute = (0u64..(1u64 << cnf.num_vars))
.any(|x| cnf.clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 == 1) == l.is_positive())));
assert!(!brute, "n={n}: brute force confirms UNSAT");
}
}
}
#[test]
fn recover_at_most_k_recovers_a_ternary_at_most_two_group() {
let triples = [[0u32, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]];
let clauses: Vec<Vec<Lit>> = triples.iter().map(|t| t.iter().map(|&v| Lit::neg(v)).collect()).collect();
let cons = recover_at_most_k(4, &clauses, 2);
assert_eq!(cons.len(), 1, "one at-most-two group; got {cons:?}");
assert!(recover_at_most_one(4, &clauses).is_empty(), "no pairwise exclusions ⇒ no at-most-one");
assert_eq!(recover_cardinality_substructure(4, &clauses).len(), 1, "the combined recognizer finds it");
for x in 0u64..(1 << 4) {
let sat = clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 == 1) == l.is_positive()));
if !sat {
continue;
}
for pb in &cons {
let sum: i64 = pb.terms().map(|(v, c, s)| if (((x >> v) & 1 == 1) == s) { c } else { 0 }).sum();
assert!(sum >= pb.degree(), "the recovered ≤2 must hold in model {x:04b}");
}
}
let clique = vec![vec![Lit::neg(0), Lit::neg(1)], vec![Lit::neg(0), Lit::neg(2)], vec![Lit::neg(1), Lit::neg(2)]];
assert_eq!(recover_at_most_k(3, &clique, 1).len(), recover_at_most_one(3, &clique).len(), "k=1 ≡ at-most-one");
}
#[test]
fn fused_decide_refutes_a_mixed_at_most_two_parity_instance() {
let mut clauses: Vec<Vec<Lit>> = vec![
vec![Lit::neg(0), Lit::neg(1), Lit::neg(2)], vec![Lit::pos(0), Lit::pos(1)],
vec![Lit::pos(0), Lit::pos(2)],
vec![Lit::pos(1), Lit::pos(2)], ];
for i in 0..3u32 {
clauses.extend(xor_gadget(&[i, i + 3], false)); }
clauses.extend(xor_gadget(&[3, 4, 5], true)); assert!(recover_at_most_one(6, &clauses).is_empty(), "no pairwise exclusions");
assert!(!recover_at_most_k(6, &clauses, 2).is_empty(), "an at-most-two core is present");
assert!(!extract_xor(6, &clauses).is_empty(), "a parity substructure is present");
assert_eq!(fused_parity_cardinality_decide(6, &clauses), Some(false), "the at-most-two mix is UNSAT");
let brute = (0u64..(1 << 6)).any(|x| clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 == 1) == l.is_positive())));
assert!(!brute, "brute force confirms UNSAT");
}
fn exclusion_clique(vars: &[u32], width: usize) -> Vec<Vec<Lit>> {
let items: Vec<usize> = vars.iter().map(|&v| v as usize).collect();
let mut out: Vec<Vec<Lit>> = Vec::new();
for_each_combo(&items, width, 0, &mut Vec::new(), &mut |sub| {
out.push(sub.iter().map(|&v| Lit::neg(v as u32)).collect());
true
});
out
}
#[test]
fn recover_at_most_k_recovers_wider_cores() {
let c3 = exclusion_clique(&[0, 1, 2, 3, 4], 4);
let g3 = recover_at_most_k(5, &c3, 3);
assert_eq!(g3.len(), 1, "one at-most-three group; got {g3:?}");
assert!(!recover_cardinality_substructure(5, &c3).is_empty(), "the combined recognizer (k≤4) finds it");
for x in 0u64..(1 << 5) {
if !c3.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 == 1) == l.is_positive())) {
continue;
}
for pb in &g3 {
let sum: i64 = pb.terms().map(|(v, c, s)| if (((x >> v) & 1 == 1) == s) { c } else { 0 }).sum();
assert!(sum >= pb.degree(), "the recovered ≤3 must hold in {x:05b}");
}
}
let c4 = exclusion_clique(&[0, 1, 2, 3, 4, 5], 5);
let g4 = recover_at_most_k(6, &c4, 4);
assert_eq!(g4.len(), 1, "one at-most-four group; got {g4:?}");
for x in 0u64..(1 << 6) {
if !c4.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 == 1) == l.is_positive())) {
continue;
}
for pb in &g4 {
let sum: i64 = pb.terms().map(|(v, c, s)| if (((x >> v) & 1 == 1) == s) { c } else { 0 }).sum();
assert!(sum >= pb.degree(), "the recovered ≤4 must hold in {x:06b}");
}
}
}
#[test]
fn recover_at_most_k_is_bounded_on_a_large_clique() {
let n = 20u32;
let clauses = exclusion_clique(&(0..n).collect::<Vec<_>>(), 3);
let g = recover_at_most_k(n as usize, &clauses, 2);
assert!(!g.is_empty(), "must recover the at-most-two core");
assert!(g.iter().any(|pb| pb.len() >= 3), "a real multi-member at-most-two group, not just a single triple");
}
#[test]
fn recover_at_most_k_recovers_at_least_two_via_negation() {
let clauses = vec![
vec![Lit::pos(0), Lit::pos(1)],
vec![Lit::pos(0), Lit::pos(2)],
vec![Lit::pos(1), Lit::pos(2)],
];
let g = recover_at_most_k(3, &clauses, 1);
assert_eq!(g.len(), 1, "one at-most-one group over the negated literals; got {g:?}");
assert_eq!(g[0].degree(), 2, "the normalized constraint is ≥ 2");
assert!(g[0].terms().all(|(_, _, s)| s) && g[0].len() == 3, "over the three positive variables");
assert!(recover_at_most_one(3, &clauses).is_empty(), "the positive-only recognizer misses at-least-two");
for x in 0u64..(1 << 3) {
let sat = clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 == 1) == l.is_positive()));
let sum: i64 = g[0].terms().map(|(v, c, s)| if (((x >> v) & 1 == 1) == s) { c } else { 0 }).sum();
assert_eq!(sum >= g[0].degree(), sat, "the recovered ≥2 must agree with the clauses on {x:03b}");
}
}
#[test]
fn cardinality_parity_seams_finds_the_parity_boundary() {
let mut clauses = exclusion_clique(&[0, 1, 2, 3], 3); clauses.extend(xor_gadget(&[0, 1], false)); clauses.extend(xor_gadget(&[2, 3], false)); let s = cardinality_parity_seams(4, &clauses);
assert!(s.joint.contains(&(0, 1)), "0↔1 preserves both structures: {:?}", s.joint);
assert!(s.joint.contains(&(2, 3)), "2↔3 preserves both structures: {:?}", s.joint);
for seam in [(0, 2), (0, 3), (1, 2), (1, 3)] {
assert!(s.seams.contains(&seam), "{seam:?} is a seam the parity blocks: {:?}", s.seams);
}
assert!(!s.joint.iter().any(|p| [(0, 2), (0, 3), (1, 2), (1, 3)].contains(p)), "no cross-pair is joint");
}
#[test]
fn cardinality_symmetry_break_is_sound_and_reduces() {
let clauses = exclusion_clique(&[0, 1, 2, 3], 3); let breaks = cardinality_symmetry_break(4, &clauses);
assert!(!breaks.is_empty(), "the joint symmetry yields lex-leader breaks");
let mut broken = clauses.clone();
broken.extend(breaks);
let count = |cs: &[Vec<Lit>]| (0u64..(1 << 4)).filter(|&x| cs.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 == 1) == l.is_positive()))).count();
let (orig, red) = (count(&clauses), count(&broken));
assert_eq!(orig, 11, "≤2 of four has 11 models");
assert_eq!(red, 3, "the ordered representatives: 0000, 1000, 1100");
assert!(red > 0 && orig > 0, "satisfiability preserved");
}
#[test]
fn cardinality_symmetry_break_preserves_satisfiability() {
let mut st = 0x5EA_5EA_5u64;
let mut rng = || {
st ^= st << 13;
st ^= st >> 7;
st ^= st << 17;
st
};
for _ in 0..150 {
let n = 6usize;
let mut clauses: Vec<Vec<Lit>> = Vec::new();
clauses.extend(xor_gadget(&(0..(2 + (rng() % 2) as u32)).collect::<Vec<_>>(), rng() % 2 == 0));
let cv: Vec<u32> = vec![2, 3, 4, 5].into_iter().filter(|_| rng() % 2 == 0).collect();
let width = 2 + (rng() % 2) as usize;
if cv.len() >= width {
clauses.extend(exclusion_clique(&cv, width));
}
let sat = |cs: &[Vec<Lit>]| (0u64..(1 << n)).any(|x| cs.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 == 1) == l.is_positive())));
let mut broken = clauses.clone();
broken.extend(cardinality_symmetry_break(n, &clauses));
assert_eq!(sat(&clauses), sat(&broken), "break must preserve satisfiability (clauses={clauses:?})");
}
}
#[test]
fn block_symmetry_crosses_the_seams_up_the_chain() {
let mut clauses = exclusion_clique(&[0, 1, 2, 3], 3);
clauses.extend(xor_gadget(&[0, 1], false));
clauses.extend(xor_gadget(&[2, 3], false));
let gens = cardinality_symmetry_generators(4, &clauses);
let has_block = gens.iter().any(|g| g[0] == 2 && g[1] == 3 && g[2] == 0 && g[3] == 1);
assert!(has_block, "a block swap (0 2)(1 3) must cross the seams");
let (sbp, ext) = crate::sym_break::lex_leader_sbp(4, &gens);
assert!(ext >= 4, "the SBP appends prefix-equality aux variables");
let mut broken = clauses.clone();
broken.extend(sbp);
let orig_sat = (0u64..(1 << 4)).any(|x| clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 == 1) == l.is_positive())));
let broken_sat = (0u64..(1u64 << ext)).any(|x| broken.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 == 1) == l.is_positive())));
assert_eq!(orig_sat, broken_sat, "the wreath break preserves satisfiability");
}
#[test]
fn wreath_climb_reaches_the_block_level_above_the_orbits() {
let mut clauses = exclusion_clique(&[0, 1, 2, 3], 3);
clauses.extend(exclusion_clique(&[4, 5, 6, 7], 3));
let gens = cardinality_symmetry_generators(8, &clauses);
assert!(
gens.iter().any(|g| (0..4).all(|k| g[k] == k + 4) && (4..8).all(|k| g[k] == k - 4)),
"the climb must reach the block level (group ↔ group): {gens:?}"
);
for g in &gens {
let sigma = crate::proof::Perm::from_images(g.iter().map(|&v| Lit::pos(v as u32)).collect());
assert!(perm_is_automorphism(&clauses, &sigma), "every emitted generator must be an automorphism: {g:?}");
}
}
#[test]
fn semantic_seams_see_through_the_parity_span() {
let mut clauses = exclusion_clique(&[0, 1, 2, 3], 3);
clauses.extend(xor_gadget(&[0, 1], false));
clauses.extend(xor_gadget(&[0, 2], false));
let syn = crate::proof::Perm::from_images((0..4u32).map(|v| Lit::pos(match v { 0 => 1, 1 => 0, _ => v })).collect());
assert!(!perm_is_automorphism(&clauses, &syn), "0↔1 is a syntactic SEAM (not a clause automorphism)");
let s = cardinality_parity_seams(4, &clauses);
assert!(s.joint.contains(&(0, 1)), "0↔1 is a SEMANTIC joint symmetry the syntactic check misses: {:?}", s.joint);
let sat: Vec<u64> = (0u64..(1 << 4))
.filter(|&x| clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 == 1) == l.is_positive())))
.collect();
for &x in &sat {
let (b0, b1) = (x & 1, (x >> 1) & 1);
let y = (x & !0b11) | (b0 << 1) | b1;
assert!(sat.contains(&y), "swap 0↔1 must map model {x:04b} to a model");
}
}
#[test]
fn affine_shear_symmetry_is_detected_and_breaks_soundly() {
let mut clauses = xor_gadget(&[0, 1], false); clauses.push(vec![Lit::neg(3), Lit::neg(4)]); let n = 5usize;
let maps = affine_parity_symmetries(n, &clauses);
assert!(!maps.is_empty(), "an affine shear symmetry must be detected");
assert!(
maps.iter().any(|m| m[2].0.len() == 2), "at least one detected map is a genuine shear on the free variable: {maps:?}"
);
let apply = |map: &[(Vec<usize>, bool)], x: u64| -> u64 {
let mut y = 0u64;
for (j, (xs, b)) in map.iter().enumerate() {
if xs.iter().fold(*b, |a, &v| a ^ ((x >> v) & 1 == 1)) {
y |= 1 << j;
}
}
y
};
let sat: Vec<u64> = (0u64..(1 << n))
.filter(|&x| clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 == 1) == l.is_positive())))
.collect();
for map in &maps {
for &x in &sat {
assert!(sat.contains(&apply(map, x)), "affine map must send model {x:05b} to a model");
}
}
let (sbp, ext) = crate::sym_break::affine_lex_leader_sbp(n, &maps);
let mut broken = clauses.clone();
broken.extend(sbp);
let broken_sat = (0u64..(1u64 << ext)).any(|x| broken.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 == 1) == l.is_positive())));
assert_eq!(!sat.is_empty(), broken_sat, "the affine break preserves satisfiability");
}
#[test]
fn multi_coordinate_affine_symmetry_the_gl_rung() {
let mut clauses = xor_gadget(&[0, 1], false); clauses.push(vec![Lit::neg(3), Lit::neg(4)]); let n = 5usize;
let maps = affine_parity_symmetries(n, &clauses);
let flip01 = maps.iter().any(|m| {
m[0] == (vec![0], true) && m[1] == (vec![1], true) && (2..n).all(|k| m[k] == (vec![k], false))
});
assert!(flip01, "the GL rung must find the multi-coordinate kernel translation flip{{0,1}}: {maps:?}");
let apply = |map: &[(Vec<usize>, bool)], x: u64| -> u64 {
let mut y = 0u64;
for (j, (xs, b)) in map.iter().enumerate() {
if xs.iter().fold(*b, |a, &v| a ^ ((x >> v) & 1 == 1)) {
y |= 1 << j;
}
}
y
};
let sat: Vec<u64> = (0u64..(1 << n))
.filter(|&x| clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 == 1) == l.is_positive())))
.collect();
for map in &maps {
for &x in &sat {
assert!(sat.contains(&apply(map, x)), "affine generator must send model {x:05b} to a model: {map:?}");
}
}
}
#[test]
fn complete_break_keeps_exactly_one_representative_per_orbit() {
use crate::cdcl::{SolveResult, Solver, Theory};
let clauses = exclusion_clique(&[0, 1, 2, 3], 3);
let group = fused_symmetry_group(4, &clauses);
let mut blocked: Vec<Vec<Lit>> = Vec::new();
let mut count = 0;
loop {
let mut s = Solver::new(4); for c in clauses.iter().chain(blocked.iter()) {
s.add_clause(c.clone());
}
let mut theories: Vec<Box<dyn Theory>> = vec![Box::new(SymmetryTheory::new(4, group.clone()))];
match s.solve_with(&mut theories) {
SolveResult::Sat(m) => {
count += 1;
assert!(count <= 4, "runaway — the complete break should leave only 3");
blocked.push((0..4u32).map(|v| Lit::new(v, !m[v as usize])).collect());
}
SolveResult::Unsat => break,
}
}
assert_eq!(count, 3, "the dynamic complete S₄ break enumerates exactly the 3 orbit representatives");
}
#[test]
fn all_transposition_symmetries_are_sound_and_include_parity_permutations() {
let mut clauses = xor_gadget(&[0, 1, 2], false); clauses.push(vec![Lit::neg(3), Lit::neg(4)]);
let n = 5usize;
let transps = all_transposition_symmetries(n, &clauses);
assert!(
transps.iter().any(|p| p[0] == 1 && p[1] == 0 && (2..n).all(|k| p[k] == k)),
"the parity-variable permutation x0↔x1 must be detected: {transps:?}"
);
let sat: Vec<u64> = (0u64..(1 << n))
.filter(|&x| clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 == 1) == l.is_positive())))
.collect();
for p in &transps {
for &x in &sat {
let mut y = 0u64;
for j in 0..n {
if (x >> p[j]) & 1 == 1 {
y |= 1 << j;
}
}
assert!(sat.contains(&y), "transposition symmetry must map model {x:05b} to a model: {p:?}");
}
}
}
#[test]
fn symmetry_theory_propagates_the_lex_leader() {
use crate::cdcl::{Lit, Theory};
let mut th = SymmetryTheory::from_perms(2, vec![vec![1, 0]]); let forced = th.propagate(&[Lit::new(1, false)]);
assert_eq!(forced.len(), 1, "one forced clause; got {forced:?}");
assert!(forced[0].contains(&Lit::new(0, false)), "must force x0 = 0: {:?}", forced[0]);
let conf = th.propagate(&[Lit::new(0, true), Lit::new(1, false)]);
let is_true = |v: u32| v == 0; assert!(
conf.iter().any(|c| !c.is_empty() && c.iter().all(|l| is_true(l.var()) != l.is_positive())),
"must conflict with an all-false clause: {conf:?}"
);
assert!(th.propagate(&[Lit::new(0, false)]).is_empty(), "x0=0 leaves nothing to force");
}
#[test]
fn symmetry_theory_handles_affine_maps_dynamically() {
use crate::cdcl::{Lit, Theory};
let map = vec![(vec![0usize], false), (vec![1], false), (vec![2, 0], false)]; let mut th = SymmetryTheory::new(3, vec![map]);
let conf = th.propagate(&[Lit::new(0, true), Lit::new(2, true)]);
let is_true = |v: u32| v == 0 || v == 2; assert!(
conf.iter().any(|c| !c.is_empty() && c.iter().all(|l| is_true(l.var()) != l.is_positive())),
"x0=1 ∧ x2=1 must conflict with an all-false clause (violates x2 ≤ x2⊕x0): {conf:?}"
);
assert!(th.propagate(&[Lit::new(0, true), Lit::new(2, false)]).is_empty(), "x0=1 ∧ x2=0 respects the shear");
assert!(th.propagate(&[Lit::new(0, false), Lit::new(2, true)]).is_empty(), "x0=0 leaves the shear inert");
}
#[test]
fn find_generators_contributes_signed_cross_symmetry() {
use crate::cdcl::{Lit, SolveResult, Solver, Theory};
let clauses = vec![vec![Lit::pos(0), Lit::pos(1)], vec![Lit::neg(2), Lit::neg(3)]];
let sigma = crate::proof::Perm::from_images(vec![Lit::neg(2), Lit::neg(3), Lit::neg(0), Lit::neg(1)]);
assert!(perm_is_automorphism(&clauses, &sigma), "the signed cross map is a clause automorphism");
for t in all_transposition_symmetries(4, &clauses) {
for i in 0..4 {
assert!(!((i < 2) != (t[i] < 2)), "an unsigned transposition never crosses the clusters: {t:?}");
}
}
let group = fused_symmetry_group(4, &clauses);
assert!(
group.iter().any(|spec| {
spec.iter().enumerate().any(|(w, (xs, b))| *b && xs.len() == 1 && (w < 2) != (xs[0] < 2))
}),
"the unified group carries the signed cross-cluster symmetry: {group:?}"
);
let count_models = |use_break: bool| -> usize {
let mut blocked: Vec<Vec<Lit>> = Vec::new();
let mut count = 0;
loop {
let mut s = Solver::new(4);
for c in clauses.iter().chain(blocked.iter()) {
s.add_clause(c.clone());
}
let mut theories: Vec<Box<dyn Theory>> =
if use_break { vec![Box::new(SymmetryTheory::new(4, group.clone()))] } else { vec![] };
match s.solve_with(&mut theories) {
SolveResult::Sat(m) => {
count += 1;
assert!(count <= 16, "runaway");
blocked.push((0..4u32).map(|v| Lit::new(v, !m[v as usize])).collect());
}
SolveResult::Unsat => break,
}
}
count
};
assert_eq!(count_models(false), 9, "the raw formula has 9 models");
let broken = count_models(true);
assert!(broken < 9, "the signed symmetry break must reduce the model count, got {broken}");
assert!(broken >= 1, "the break stays satisfiable");
}
}