use crate::cdcl::Lit;
use crate::proof::Perm;
pub(crate) fn clause_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
}
pub fn perm_is_automorphism(clauses: &[Vec<Lit>], sigma: &Perm) -> bool {
let nv = sigma.num_vars();
let moved: Vec<bool> =
(0..nv).map(|v| sigma.apply(Lit::pos(v as u32)) != Lit::pos(v as u32)).collect();
if moved.iter().all(|&m| !m) {
return true;
}
let set: std::collections::HashSet<Vec<u32>> = clauses.iter().map(|c| clause_key(c)).collect();
for c in clauses {
let touches_support =
c.iter().any(|l| (l.var() as usize) < nv && moved[l.var() as usize]);
if touches_support && !set.contains(&clause_key(&sigma.apply_clause(c))) {
return false;
}
}
true
}
pub struct AutomorphismIndex {
nv: usize,
clauses: Vec<Vec<Lit>>,
keys: std::collections::HashSet<Vec<u32>>,
index_by_key: std::collections::HashMap<Vec<u32>, usize>,
by_var: Vec<Vec<usize>>,
base: Vec<Option<bool>>,
base_conflict: bool,
temp_gen: Vec<u32>,
temp_val: Vec<bool>,
gen: u32,
visited_gen: Vec<u32>,
vgen: u32,
}
impl AutomorphismIndex {
pub fn new(nv: usize) -> Self {
AutomorphismIndex {
nv,
clauses: Vec::new(),
keys: std::collections::HashSet::new(),
index_by_key: std::collections::HashMap::new(),
by_var: vec![Vec::new(); nv],
base: vec![None; nv],
base_conflict: false,
temp_gen: vec![0; nv],
temp_val: vec![false; nv],
gen: 0,
visited_gen: Vec::new(),
vgen: 0,
}
}
pub fn with_clauses(nv: usize, clauses: &[Vec<Lit>]) -> Self {
let mut ix = Self::new(nv);
for c in clauses {
ix.insert(c.clone());
}
ix
}
pub fn insert(&mut self, clause: Vec<Lit>) {
let idx = self.clauses.len();
let key = clause_key(&clause);
self.keys.insert(key.clone());
self.index_by_key.insert(key, idx);
let mut touched: Vec<usize> = clause.iter().map(|l| l.var() as usize).filter(|&v| v < self.nv).collect();
touched.sort_unstable();
touched.dedup();
for v in touched {
self.by_var[v].push(idx);
}
self.visited_gen.push(0);
self.clauses.push(clause);
self.base_propagate(idx);
}
fn base_propagate(&mut self, start: usize) {
if self.base_conflict {
return;
}
let mut wl = vec![start];
let mut wi = 0;
while wi < wl.len() {
let ci = wl[wi];
wi += 1;
let mut satisfied = false;
let mut unit: Option<Lit> = None;
let mut more_than_one = false;
for &l in &self.clauses[ci] {
let val = self.base[l.var() as usize].map(|b| b == l.is_positive());
match val {
Some(true) => {
satisfied = true;
break;
}
Some(false) => {}
None => match unit {
None => unit = Some(l),
Some(u) if u == l => {}
Some(u) if u == l.negated() => {
satisfied = true;
break;
}
Some(_) => more_than_one = true,
},
}
}
if satisfied {
continue;
}
match unit {
None => {
self.base_conflict = true;
return;
}
Some(u) if !more_than_one => {
self.base[u.var() as usize] = Some(u.is_positive());
for &cj in &self.by_var[u.var() as usize] {
wl.push(cj);
}
}
_ => {}
}
}
}
pub fn is_automorphism(&mut self, sigma: &Perm) -> bool {
self.vgen = self.vgen.wrapping_add(1);
if self.vgen == 0 {
for g in &mut self.visited_gen {
*g = 0;
}
self.vgen = 1;
}
let involution = (0..self.nv as u32).all(|v| {
let pv = Lit::pos(v);
sigma.apply(sigma.apply(pv)) == pv
});
let mut keybuf: Vec<u32> = Vec::new();
for v in 0..self.nv {
if sigma.apply(Lit::pos(v as u32)) == Lit::pos(v as u32) {
continue;
}
for k in 0..self.by_var[v].len() {
let ci = self.by_var[v][k];
if self.visited_gen[ci] == self.vgen {
continue;
}
self.visited_gen[ci] = self.vgen;
keybuf.clear();
for &l in &self.clauses[ci] {
let m = sigma.apply(l);
keybuf.push(m.var() * 2 + u32::from(!m.is_positive()));
}
keybuf.sort_unstable();
keybuf.dedup();
if !self.keys.contains(keybuf.as_slice()) {
return false;
}
if involution {
if let Some(&partner) = self.index_by_key.get(keybuf.as_slice()) {
self.visited_gen[partner] = self.vgen;
}
}
}
}
true
}
pub fn propagate_to_conflict(&mut self, _num_vars: usize, assume: &[Lit]) -> bool {
if self.base_conflict {
return true;
}
self.gen = self.gen.wrapping_add(1);
if self.gen == 0 {
for g in &mut self.temp_gen {
*g = 0;
}
self.gen = 1;
}
let mut queue: Vec<u32> = Vec::new();
for &l in assume {
let v = l.var() as usize;
let cur = if self.temp_gen[v] == self.gen { Some(self.temp_val[v]) } else { self.base[v] };
match cur {
Some(b) if b == l.is_positive() => {}
Some(_) => return true,
None => {
self.temp_gen[v] = self.gen;
self.temp_val[v] = l.is_positive();
queue.push(v as u32);
}
}
}
let mut qi = 0;
while qi < queue.len() {
let v = queue[qi] as usize;
qi += 1;
for k in 0..self.by_var[v].len() {
let ci = self.by_var[v][k];
let mut satisfied = false;
let mut unit: Option<Lit> = None;
let mut more_than_one = false;
for &l in &self.clauses[ci] {
let vi = l.var() as usize;
let raw = if self.temp_gen[vi] == self.gen { Some(self.temp_val[vi]) } else { self.base[vi] };
match raw.map(|b| b == l.is_positive()) {
Some(true) => {
satisfied = true;
break;
}
Some(false) => {}
None => match unit {
None => unit = Some(l),
Some(u) if u == l => {}
Some(u) if u == l.negated() => {
satisfied = true;
break;
}
Some(_) => more_than_one = true,
},
}
}
if satisfied {
continue;
}
match unit {
None => return true, Some(u) if !more_than_one => {
let uv = u.var() as usize;
self.temp_gen[uv] = self.gen;
self.temp_val[uv] = u.is_positive();
queue.push(uv as u32);
}
_ => {}
}
}
}
false
}
}
pub fn find_generators(num_vars: usize, clauses: &[Vec<Lit>]) -> Vec<Perm> {
if num_vars == 0 {
return Vec::new();
}
let (adj, init_colors) = build_graph(num_vars, clauses);
let adj_sorted: Vec<Vec<usize>> = adj
.iter()
.map(|a| {
let mut s = a.clone();
s.sort_unstable();
s
})
.collect();
let base = refine(&adj, &init_colors);
let mut ctx = Search {
adj: &adj,
adj_sorted: &adj_sorted,
init_colors: &init_colors,
num_vars,
clauses,
parent: (0..adj.len()).collect(),
first_leaf: None,
gens: Vec::new(),
seen: std::collections::HashSet::new(),
budget: 500_000,
};
ctx.search(base);
ctx.gens
}
struct Search<'a> {
adj: &'a [Vec<usize>],
adj_sorted: &'a [Vec<usize>],
init_colors: &'a [u32],
num_vars: usize,
clauses: &'a [Vec<Lit>],
parent: Vec<usize>,
first_leaf: Option<Vec<u32>>,
gens: Vec<Perm>,
seen: std::collections::HashSet<Vec<u32>>,
budget: usize,
}
impl Search<'_> {
fn find(&mut self, mut x: usize) -> usize {
while self.parent[x] != x {
self.parent[x] = self.parent[self.parent[x]];
x = self.parent[x];
}
x
}
fn union(&mut self, a: usize, b: usize) {
let (ra, rb) = (self.find(a), self.find(b));
if ra != rb {
self.parent[ra] = rb;
}
}
fn search(&mut self, coloring: Vec<u32>) {
if self.budget == 0 {
return;
}
self.budget -= 1;
match target_cell(&coloring) {
None => self.visit_leaf(coloring),
Some(cell) => {
let mut explored: Vec<usize> = Vec::new();
for (i, &u) in cell.iter().enumerate() {
if i > 0 {
let ru = self.find(u);
if explored.iter().any(|&r| self.find(r) == ru) {
continue;
}
}
explored.push(u);
let child = individualize(self.adj, &coloring, u);
self.search(child);
}
}
}
}
fn visit_leaf(&mut self, leaf: Vec<u32>) {
let ref0 = match &self.first_leaf {
None => {
self.first_leaf = Some(leaf);
return;
}
Some(r) => r.clone(),
};
let Some((vperm, lperm)) = self.candidate(&ref0, &leaf) else { return };
let key = perm_key(self.num_vars, &lperm);
if !lperm.is_identity() && !self.seen.contains(&key) && perm_is_automorphism(self.clauses, &lperm) {
self.seen.insert(key);
for u in 0..vperm.len() {
self.union(u, vperm[u]);
}
self.gens.push(lperm);
}
}
fn candidate(&self, ref0: &[u32], leaf: &[u32]) -> Option<(Vec<usize>, Perm)> {
let v = ref0.len();
let mut leaf_inv = vec![0usize; v];
for (u, &r) in leaf.iter().enumerate() {
leaf_inv[r as usize] = u;
}
let g: Vec<usize> = (0..v).map(|u| leaf_inv[ref0[u] as usize]).collect();
for u in 0..v {
if self.init_colors[u] != self.init_colors[g[u]] {
return None;
}
let mut mapped: Vec<usize> = self.adj[u].iter().map(|&w| g[w]).collect();
mapped.sort_unstable();
if mapped != self.adj_sorted[g[u]] {
return None;
}
}
let nlit = 2 * self.num_vars;
let mut images = Vec::with_capacity(self.num_vars);
for var in 0..self.num_vars {
let gv = g[2 * var];
if gv >= nlit {
return None;
}
images.push(Lit::new((gv / 2) as u32, gv % 2 == 0));
}
Some((g, Perm::from_images(images)))
}
}
fn literal_perm_to_points(num_vars: usize, sigma: &Perm) -> crate::permgroup::Perm {
let idx = |l: Lit| 2 * l.var() as usize + usize::from(!l.is_positive());
let mut p = vec![0usize; 2 * num_vars];
for v in 0..num_vars as u32 {
for l in [Lit::pos(v), Lit::neg(v)] {
p[idx(l)] = idx(sigma.apply(l));
}
}
p
}
pub fn automorphism_group(num_vars: usize, clauses: &[Vec<Lit>]) -> crate::permgroup::Bsgs {
let gens: Vec<crate::permgroup::Perm> = find_generators(num_vars, clauses)
.iter()
.filter(|g| !g.is_identity())
.map(|g| literal_perm_to_points(num_vars, g))
.collect();
crate::permgroup::schreier_sims(2 * num_vars, &gens)
}
pub fn aut_order(num_vars: usize, clauses: &[Vec<Lit>]) -> u128 {
automorphism_group(num_vars, clauses).order()
}
fn build_graph(num_vars: usize, clauses: &[Vec<Lit>]) -> (Vec<Vec<usize>>, Vec<u32>) {
let nlit = 2 * num_vars;
let vtotal = nlit + clauses.len();
let mut adj = vec![Vec::new(); vtotal];
let mut color = vec![0u32; vtotal];
for var in 0..num_vars {
let (a, b) = (2 * var, 2 * var + 1);
adj[a].push(b);
adj[b].push(a);
}
for (ci, clause) in clauses.iter().enumerate() {
let cv = nlit + ci;
color[cv] = 1 + clause.len() as u32;
for &l in clause {
let lv = (l.var() * 2 + u32::from(!l.is_positive())) as usize;
adj[cv].push(lv);
adj[lv].push(cv);
}
}
for a in adj.iter_mut() {
a.sort_unstable();
a.dedup();
}
(adj, color)
}
fn refine(adj: &[Vec<usize>], colors: &[u32]) -> Vec<u32> {
let v = adj.len();
let mut cur = colors.to_vec();
loop {
let sigs: Vec<(u32, Vec<u32>)> = (0..v)
.map(|u| {
let mut nb: Vec<u32> = adj[u].iter().map(|&w| cur[w]).collect();
nb.sort_unstable();
(cur[u], nb)
})
.collect();
let mut order: Vec<usize> = (0..v).collect();
order.sort_by(|&a, &b| sigs[a].cmp(&sigs[b]));
let mut new = vec![0u32; v];
let mut rank = 0u32;
for i in 0..v {
if i > 0 && sigs[order[i]] != sigs[order[i - 1]] {
rank += 1;
}
new[order[i]] = rank;
}
if new == cur {
return new;
}
cur = new;
}
}
fn individualize(adj: &[Vec<usize>], colors: &[u32], u: usize) -> Vec<u32> {
let mut c = colors.to_vec();
c[u] = u32::MAX;
refine(adj, &c)
}
fn target_cell(colors: &[u32]) -> Option<Vec<usize>> {
let maxc = *colors.iter().max().unwrap();
for c in 0..=maxc {
let members: Vec<usize> = (0..colors.len()).filter(|&u| colors[u] == c).collect();
if members.len() > 1 {
return Some(members);
}
}
None
}
fn perm_key(num_vars: usize, perm: &Perm) -> Vec<u32> {
(0..num_vars as u32)
.map(|v| {
let l = perm.apply(Lit::pos(v));
l.var() * 2 + u32::from(!l.is_positive())
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cdcl::Lit;
use crate::families;
use crate::proof::Perm;
fn swap_pigeon_rows(n: usize, p0: usize, p1: usize) -> Perm {
let holes = n - 1;
let images: Vec<Lit> = (0..n * holes)
.map(|v| {
let (p, h) = (v / holes, v % holes);
let np = if p == p0 {
p1
} else if p == p1 {
p0
} else {
p
};
Lit::pos((np * holes + h) as u32)
})
.collect();
Perm::from_images(images)
}
#[test]
fn identity_is_always_an_automorphism() {
let (cnf, _) = families::php(4);
assert!(perm_is_automorphism(&cnf.clauses, &Perm::identity(cnf.num_vars)));
}
#[test]
fn swapping_pigeon_rows_is_an_automorphism_of_php() {
let (cnf, _) = families::php(4);
for (p0, p1) in [(0, 1), (1, 2), (0, 3), (2, 3)] {
let sigma = swap_pigeon_rows(4, p0, p1);
assert!(
perm_is_automorphism(&cnf.clauses, &sigma),
"swapping pigeons {p0},{p1} must preserve PHP(4)"
);
}
}
#[test]
fn support_restricted_check_matches_brute_force_set_equality() {
let mut state = 0xA5A5_5A5A_DEAD_BEEFu64;
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 num_vars = 5usize;
let brute = |clauses: &[Vec<Lit>], sigma: &Perm| -> bool {
use std::collections::BTreeSet;
let orig: BTreeSet<Vec<u32>> = clauses.iter().map(|c| clause_key(c)).collect();
let mapped: BTreeSet<Vec<u32>> =
clauses.iter().map(|c| clause_key(&sigma.apply_clause(c))).collect();
orig == mapped
};
let mut accepts = 0;
for _ in 0..20_000 {
let nclauses = next() as usize % 6;
let clauses: Vec<Vec<Lit>> = (0..nclauses)
.map(|_| {
let len = 1 + (next() as usize % 3);
let mut c = Vec::new();
for _ in 0..len {
let v = (next() as u32) % num_vars as u32;
let lit = Lit::new(v, next() & 1 == 0);
if !c.contains(&lit) && !c.contains(&lit.negated()) {
c.push(lit);
}
}
c
})
.filter(|c| !c.is_empty())
.collect();
let sigma = {
let mut order: Vec<u32> = (0..num_vars as u32).collect();
for i in (1..num_vars).rev() {
let j = next() as usize % (i + 1);
order.swap(i, j);
}
Perm::from_images((0..num_vars).map(|v| Lit::new(order[v], next() & 1 == 0)).collect())
};
let fast = perm_is_automorphism(&clauses, &sigma);
assert_eq!(fast, brute(&clauses, &sigma), "fast vs brute disagree: clauses={clauses:?}");
if fast {
accepts += 1;
}
}
assert!(accepts > 0, "the differential must exercise genuine acceptances, not just rejects");
}
#[test]
fn incremental_index_matches_stateless_automorphism_check() {
let mut state = 0x1234_5678_9ABC_DEF0u64;
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 num_vars = 5usize;
let mut agree = 0;
for _ in 0..20_000 {
let nclauses = next() as usize % 7;
let clauses: Vec<Vec<Lit>> = (0..nclauses)
.map(|_| {
let len = 1 + (next() as usize % 3);
let mut c = Vec::new();
for _ in 0..len {
let v = (next() as u32) % num_vars as u32;
let lit = Lit::new(v, next() & 1 == 0);
if !c.contains(&lit) && !c.contains(&lit.negated()) {
c.push(lit);
}
}
c
})
.filter(|c| !c.is_empty())
.collect();
let mut ix = AutomorphismIndex::new(num_vars);
for c in &clauses {
ix.insert(c.clone());
}
let sigma = {
let mut order: Vec<u32> = (0..num_vars as u32).collect();
for i in (1..num_vars).rev() {
let j = next() as usize % (i + 1);
order.swap(i, j);
}
Perm::from_images((0..num_vars).map(|v| Lit::new(order[v], next() & 1 == 0)).collect())
};
assert_eq!(
ix.is_automorphism(&sigma),
perm_is_automorphism(&clauses, &sigma),
"incremental index disagrees with stateless check on {clauses:?}"
);
agree += 1;
}
assert_eq!(agree, 20_000);
}
#[test]
fn occurrence_propagation_matches_full_scan_propagation() {
let mut state = 0x0F0F_F0F0_1357_9BDFu64;
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 num_vars = 6usize;
let reference = |clauses: &[Vec<Lit>], assume: &[Lit]| -> bool {
let mut assign: Vec<Option<bool>> = vec![None; num_vars];
for &l in assume {
if !crate::rup::set_true(&mut assign, l) {
return true;
}
}
crate::rup::propagate(clauses, &mut assign)
};
let mut conflicts = 0;
for _ in 0..20_000 {
let nclauses = next() as usize % 8;
let clauses: Vec<Vec<Lit>> = (0..nclauses)
.map(|_| {
let len = 1 + (next() as usize % 4);
(0..len).map(|_| Lit::new((next() as u32) % num_vars as u32, next() & 1 == 0)).collect()
})
.filter(|c: &Vec<Lit>| !c.is_empty())
.collect();
let mut ix = AutomorphismIndex::new(num_vars);
for c in &clauses {
ix.insert(c.clone());
}
let nassume = next() as usize % 4;
let assume: Vec<Lit> =
(0..nassume).map(|_| Lit::new((next() as u32) % num_vars as u32, next() & 1 == 0)).collect();
let got = ix.propagate_to_conflict(num_vars, &assume);
assert_eq!(got, reference(&clauses, &assume), "clauses={clauses:?} assume={assume:?}");
if got {
conflicts += 1;
}
}
assert!(conflicts > 0, "the differential must exercise genuine conflicts");
}
#[test]
fn a_non_symmetry_is_rejected() {
let (cnf, _) = families::php(3);
let holes = 2;
let images: Vec<Lit> = (0..cnf.num_vars)
.map(|v| {
let (p, h) = (v / holes, v % holes);
let np = if p == 0 { 1 } else { p };
Lit::pos((np * holes + h) as u32)
})
.collect();
assert!(!perm_is_automorphism(&cnf.clauses, &Perm::from_images(images)));
}
use std::collections::HashSet;
fn p(v: u32) -> Lit {
Lit::pos(v)
}
fn neg(v: u32) -> Lit {
Lit::neg(v)
}
fn all_var_perms(n: usize) -> Vec<Vec<usize>> {
fn rec(cur: &mut Vec<usize>, rem: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
if rem.is_empty() {
out.push(cur.clone());
return;
}
for i in 0..rem.len() {
let x = rem.remove(i);
cur.push(x);
rec(cur, rem, out);
cur.pop();
rem.insert(i, x);
}
}
let mut out = Vec::new();
rec(&mut Vec::new(), &mut (0..n).collect(), &mut out);
out
}
fn brute_force_group(num_vars: usize, clauses: &[Vec<Lit>]) -> HashSet<Vec<u32>> {
let mut set = HashSet::new();
for vp in all_var_perms(num_vars) {
for phase in 0..(1u32 << num_vars) {
let images: Vec<Lit> =
(0..num_vars).map(|v| Lit::new(vp[v] as u32, (phase >> v) & 1 == 0)).collect();
let perm = Perm::from_images(images);
if perm_is_automorphism(clauses, &perm) {
set.insert(perm_key(num_vars, &perm));
}
}
}
set
}
fn generated_group(num_vars: usize, gens: &[Perm]) -> HashSet<Vec<u32>> {
let id = Perm::identity(num_vars);
let mut set = HashSet::new();
set.insert(perm_key(num_vars, &id));
let mut frontier = vec![id];
while let Some(x) = frontier.pop() {
for g in gens {
let y = g.compose(&x);
let k = perm_key(num_vars, &y);
if set.insert(k) {
frontier.push(y);
}
}
}
set
}
#[test]
fn finds_the_full_php3_symmetry_group() {
let (cnf, _) = families::php(3);
let gens = find_generators(cnf.num_vars, &cnf.clauses);
let group = generated_group(cnf.num_vars, &gens);
assert_eq!(group.len(), 12, "discovered generators must generate the full S_3 × S_2");
}
#[test]
fn discovered_generators_match_brute_force_across_cases() {
let php2 = families::php(2).0;
let php3 = families::php(3).0;
let cases: Vec<(usize, Vec<Vec<Lit>>)> = vec![
(php2.num_vars, php2.clauses),
(php3.num_vars, php3.clauses),
(2, vec![vec![p(0), p(1)], vec![neg(0), neg(1)]]), (3, vec![vec![p(0)], vec![p(0), p(1)]]), (3, vec![vec![p(0), p(1), p(2)]]), ];
for (num_vars, clauses) in cases {
let gens = find_generators(num_vars, &clauses);
let found = generated_group(num_vars, &gens);
let truth = brute_force_group(num_vars, &clauses);
assert_eq!(found, truth, "finder must generate exactly Aut(F) for {clauses:?}");
}
}
#[test]
fn every_discovered_generator_is_a_verified_automorphism() {
let (cnf, _) = families::php(4);
let gens = find_generators(cnf.num_vars, &cnf.clauses);
assert!(!gens.is_empty(), "PHP(4) is highly symmetric — generators must be found");
for g in &gens {
assert!(perm_is_automorphism(&cnf.clauses, g), "every returned generator is sound");
}
}
#[test]
fn an_asymmetric_formula_yields_no_nontrivial_generators() {
let clauses = vec![vec![p(0)], vec![p(0), p(1)], vec![neg(1), p(2)]];
let gens = find_generators(3, &clauses);
assert!(gens.iter().all(|g| g.is_identity()), "no non-trivial symmetry to find");
}
#[test]
fn the_bsgs_backend_computes_the_automorphism_group_order() {
let fact = |k: u128| (1..=k).product::<u128>();
for n in 3..=5usize {
let (cnf, _) = crate::families::php(n);
assert_eq!(
aut_order(cnf.num_vars, &cnf.clauses),
fact(n as u128) * fact((n - 1) as u128),
"|Aut(PHP({n}))| = n!·(n-1)!"
);
}
for &(n, k) in &[(4usize, 3usize), (5, 3)] {
let (cnf, _) = crate::families::clique_coloring(n, k);
assert_eq!(
aut_order(cnf.num_vars, &cnf.clauses),
fact(n as u128) * fact(k as u128),
"|Aut(clique_coloring({n},{k}))| = n!·k!"
);
}
}
#[test]
fn the_bsgs_backend_decides_automorphism_membership() {
let (cnf, _) = crate::families::php(3); let nv = cnf.num_vars;
let bsgs = automorphism_group(nv, &cnf.clauses);
for g in find_generators(nv, &cnf.clauses) {
assert!(bsgs.contains(&literal_perm_to_points(nv, &g)), "a detected generator is a member");
}
assert!(bsgs.contains(&(0..2 * nv).collect::<Vec<_>>()), "the identity is a member");
let mut bad: Vec<usize> = (0..2 * nv).collect();
bad.swap(0, 2); bad.swap(1, 3); assert!(!bsgs.contains(&bad), "a non-automorphism variable swap must be rejected");
}
}