use std::collections::HashSet;
use crate::cdcl::Lit;
use crate::gf2;
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Affine {
pub n: usize,
pub matrix: Vec<u64>,
pub translation: u64,
}
impl Affine {
pub fn identity(n: usize) -> Self {
Affine { n, matrix: (0..n).map(|i| 1u64 << i).collect(), translation: 0 }
}
pub fn apply(&self, x: u64) -> u64 {
let mut y = 0u64;
for i in 0..self.n {
let dot = (self.matrix[i] & x).count_ones() & 1;
let bit = dot ^ ((self.translation >> i) & 1) as u32;
y |= (bit as u64) << i;
}
y
}
pub fn compose(&self, other: &Affine) -> Affine {
debug_assert_eq!(self.n, other.n);
let mut matrix = vec![0u64; self.n];
for i in 0..self.n {
let mut row = 0u64;
let mut sel = self.matrix[i];
while sel != 0 {
let k = sel.trailing_zeros() as usize;
row ^= other.matrix[k];
sel &= sel - 1;
}
matrix[i] = row;
}
let mut translation = self.translation;
for i in 0..self.n {
let dot = (self.matrix[i] & other.translation).count_ones() & 1;
translation ^= (dot as u64) << i;
}
Affine { n: self.n, matrix, translation }
}
pub fn is_bijection(&self) -> bool {
gf2::is_invertible_gf2(self.n as u32, &self.matrix)
}
}
pub fn agl_order(n: u32) -> u128 {
(1u128 << n) * gf2::gl_order(n)
}
pub fn affine_subspace_agl_order(n: u32, k: u32) -> u128 {
debug_assert!(k <= n);
gf2::gl_order(k) * gf2::gl_order(n - k) * (1u128 << (k * (n - k))) * (1u128 << k)
}
pub fn all_affine_bijections(n: usize) -> Vec<Affine> {
assert!(n <= 4, "exhaustive AGL enumeration is for ground-truth tests only (n ≤ 4)");
let mut out = Vec::new();
let total_matrices = 1u64 << (n * n);
for code in 0..total_matrices {
let matrix: Vec<u64> = (0..n).map(|i| (code >> (i * n)) & ((1 << n) - 1)).collect();
if !gf2::is_invertible_gf2(n as u32, &matrix) {
continue;
}
for translation in 0..(1u64 << n) {
out.push(Affine { n, matrix: matrix.clone(), translation });
}
}
out
}
pub fn models_of(num_vars: usize, clauses: &[Vec<Lit>]) -> Vec<u64> {
assert!(num_vars <= 24, "model enumeration is brute force — small n only");
(0u64..(1u64 << num_vars))
.filter(|&x| {
clauses.iter().all(|c| {
c.iter().any(|l| ((x >> l.var()) & 1 == 1) == l.is_positive())
})
})
.collect()
}
pub fn affine_symmetries(num_vars: usize, clauses: &[Vec<Lit>]) -> Vec<Affine> {
let models: HashSet<u64> = models_of(num_vars, clauses).into_iter().collect();
all_affine_bijections(num_vars)
.into_iter()
.filter(|phi| models.iter().all(|&m| models.contains(&phi.apply(m))))
.collect()
}
fn eqs_to_rows(eqs: &[crate::xorsat::XorEquation]) -> (Vec<u64>, Vec<bool>) {
let mut rows = Vec::with_capacity(eqs.len());
let mut rhs = Vec::with_capacity(eqs.len());
for eq in eqs {
let mut mask = 0u64;
for &v in &eq.vars {
mask |= 1u64 << v;
}
rows.push(mask);
rhs.push(eq.rhs);
}
(rows, rhs)
}
pub fn recover_linear_system(num_vars: usize, clauses: &[Vec<Lit>]) -> Option<(Vec<u64>, Vec<bool>)> {
if num_vars > 63 {
return None;
}
let eqs = crate::lyapunov::extract_xor(num_vars, clauses);
if eqs.is_empty() {
return None;
}
Some(eqs_to_rows(&eqs))
}
pub fn affine_refutation_drat(num_vars: usize, clauses: &[Vec<Lit>]) -> Option<Vec<Vec<Lit>>> {
if num_vars > 63 {
return None;
}
let eqs = crate::lyapunov::extract_xor(num_vars, clauses);
match crate::xorsat::solve(&eqs, num_vars) {
crate::xorsat::XorOutcome::Unsat(refutation) => crate::xor_drat::emit_xor_drat(&eqs, &refutation),
crate::xorsat::XorOutcome::Sat(_) => None,
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AffineOutcome {
Refuted(Option<Vec<Vec<Lit>>>),
Forced(Vec<Vec<Lit>>),
Unchanged,
}
fn canon(clause: &[Lit]) -> Vec<(u32, bool)> {
let mut k: Vec<(u32, bool)> = clause.iter().map(|l| (l.var(), l.is_positive())).collect();
k.sort_unstable();
k.dedup();
k
}
pub fn affine_reduce(num_vars: usize, clauses: &[Vec<Lit>]) -> AffineOutcome {
if num_vars > 63 {
return AffineOutcome::Unchanged;
}
let eqs = crate::lyapunov::extract_xor(num_vars, clauses);
if eqs.is_empty() {
return AffineOutcome::Unchanged;
}
if let crate::xorsat::XorOutcome::Unsat(refutation) = crate::xorsat::solve(&eqs, num_vars) {
return AffineOutcome::Refuted(crate::xor_drat::emit_xor_drat(&eqs, &refutation));
}
let (rows, rhs) = eqs_to_rows(&eqs);
let Some(ss) = gf2::solve_gf2(num_vars, &rows, &rhs) else {
return AffineOutcome::Unchanged; };
let existing: HashSet<Vec<(u32, bool)>> = clauses.iter().map(|c| canon(c)).collect();
let mut forced: Vec<Vec<Lit>> = Vec::new();
let push_new = |clause: Vec<Lit>, forced: &mut Vec<Vec<Lit>>| {
if !existing.contains(&canon(&clause)) {
forced.push(clause);
}
};
let mut is_forced = vec![false; num_vars];
for v in 0..num_vars {
if ss.kernel_basis.iter().all(|k| !k[v]) {
is_forced[v] = true;
push_new(vec![Lit::new(v as u32, ss.particular[v])], &mut forced);
}
}
for u in 0..num_vars {
if is_forced[u] {
continue;
}
for v in (u + 1)..num_vars {
if is_forced[v] {
continue;
}
if ss.kernel_basis.iter().all(|k| k[u] == k[v]) {
let (lu, lv) = (u as u32, v as u32);
if ss.particular[u] ^ ss.particular[v] {
push_new(vec![Lit::pos(lu), Lit::pos(lv)], &mut forced);
push_new(vec![Lit::neg(lu), Lit::neg(lv)], &mut forced);
} else {
push_new(vec![Lit::neg(lu), Lit::pos(lv)], &mut forced);
push_new(vec![Lit::pos(lu), Lit::neg(lv)], &mut forced);
}
}
}
}
if forced.is_empty() {
AffineOutcome::Unchanged
} else {
AffineOutcome::Forced(forced)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum VarSub {
Const(bool),
Survive(u32),
Alias(u32, bool),
}
#[derive(Clone, Debug)]
pub struct AffineCanonical {
pub num_vars: usize,
pub clauses: Vec<Vec<Lit>>,
sub: Vec<VarSub>,
}
impl AffineCanonical {
pub fn lift(&self, reduced_model: &[bool]) -> Vec<bool> {
self.sub
.iter()
.map(|s| match *s {
VarSub::Const(c) => c,
VarSub::Survive(ni) => reduced_model[ni as usize],
VarSub::Alias(ni, flip) => reduced_model[ni as usize] ^ flip,
})
.collect()
}
}
pub enum AffineCanon {
Refuted(Option<Vec<Vec<Lit>>>),
Canonical(AffineCanonical),
Unchanged,
}
pub fn affine_canonicalize(num_vars: usize, clauses: &[Vec<Lit>]) -> AffineCanon {
if num_vars > 63 {
return AffineCanon::Unchanged;
}
let eqs = crate::lyapunov::extract_xor(num_vars, clauses);
if eqs.is_empty() {
return AffineCanon::Unchanged;
}
if let crate::xorsat::XorOutcome::Unsat(refutation) = crate::xorsat::solve(&eqs, num_vars) {
return AffineCanon::Refuted(crate::xor_drat::emit_xor_drat(&eqs, &refutation));
}
let (rows, rhs) = eqs_to_rows(&eqs);
let Some(ss) = gf2::solve_gf2(num_vars, &rows, &rhs) else {
return AffineCanon::Unchanged; };
let kdim = ss.kernel_basis.len();
let column = |v: usize| -> Vec<bool> { (0..kdim).map(|i| ss.kernel_basis[i][v]).collect() };
let zero = vec![false; kdim];
let mut sub = vec![VarSub::Const(false); num_vars];
let mut groups: std::collections::HashMap<Vec<bool>, Vec<usize>> = std::collections::HashMap::new();
for v in 0..num_vars {
let col = column(v);
if col == zero {
sub[v] = VarSub::Const(ss.particular[v]); } else {
groups.entry(col).or_default().push(v);
}
}
let mut classes: Vec<Vec<usize>> = groups
.into_values()
.map(|mut g| {
g.sort_unstable();
g
})
.collect();
classes.sort_unstable_by_key(|g| g[0]);
for (new_index, members) in classes.iter().enumerate() {
let rep = members[0];
let rep_par = ss.particular[rep];
for &v in members {
sub[v] = if v == rep {
VarSub::Survive(new_index as u32)
} else {
VarSub::Alias(new_index as u32, ss.particular[v] ^ rep_par) };
}
}
let reduced_nv = classes.len();
if !sub.iter().any(|s| matches!(s, VarSub::Const(_) | VarSub::Alias(_, _))) {
return AffineCanon::Unchanged; }
let mut out: Vec<Vec<Lit>> = Vec::new();
'clause: for c in clauses {
let mut seen: std::collections::HashMap<u32, bool> = std::collections::HashMap::new();
let mut lits: Vec<Lit> = Vec::new();
for l in c {
let (ni, pol) = match sub[l.var() as usize] {
VarSub::Const(cst) => {
if cst == l.is_positive() {
continue 'clause; }
continue; }
VarSub::Survive(ni) => (ni, l.is_positive()),
VarSub::Alias(ni, flip) => (ni, l.is_positive() ^ flip),
};
match seen.get(&ni) {
Some(&prev) if prev != pol => continue 'clause, Some(_) => continue, None => {
seen.insert(ni, pol);
lits.push(Lit::new(ni, pol));
}
}
}
out.push(lits); }
AffineCanon::Canonical(AffineCanonical { num_vars: reduced_nv, clauses: out, sub })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agl_order_matches_the_enumeration() {
for n in 1..=4u32 {
assert_eq!(
all_affine_bijections(n as usize).len() as u128,
agl_order(n),
"n={n}: enumerated affine bijections must equal |AGL(n,2)| = 2ⁿ·|GL(n,2)|"
);
}
assert_eq!(agl_order(1), 2);
assert_eq!(agl_order(2), 24);
assert_eq!(agl_order(3), 1344);
}
#[test]
fn affine_maps_compose_and_act_correctly() {
let id = Affine::identity(3);
for phi in all_affine_bijections(3) {
assert!(phi.is_bijection());
assert_eq!(phi.compose(&id), phi);
assert_eq!(id.compose(&phi), phi);
let sq = phi.compose(&phi);
for x in 0..8u64 {
assert_eq!(sq.apply(x), phi.apply(phi.apply(x)), "composition must match double application");
}
}
}
#[test]
fn affine_symmetry_strictly_exceeds_permutation_symmetry_on_parity() {
let p = |v: u32| Lit::pos(v);
let q = |v: u32| Lit::neg(v);
let clauses = vec![
vec![q(0), p(1), p(2)], vec![p(0), q(1), p(2)], vec![p(0), p(1), q(2)], vec![q(0), q(1), q(2)], ];
let affine = affine_symmetries(3, &clauses);
assert_eq!(affine.len(), 96, "the even-parity plane has exactly 96 affine symmetries");
const B3: usize = 8 * 6;
assert!(affine.len() > B3, "AGL symmetry ({}) must exceed |B₃| = {B3}", affine.len());
let shear = Affine { n: 3, matrix: vec![0b001, 0b011, 0b101], translation: 0 };
assert!(shear.is_bijection());
assert!(affine.contains(&shear), "the shear x₁↦x₀⊕x₁, x₂↦x₀⊕x₂ must be an affine symmetry of the plane");
assert!(
shear.matrix.iter().any(|r| r.count_ones() >= 2),
"the shear mixes variables — outside Bₙ, invisible to every clause-level break"
);
}
#[test]
fn affine_symmetry_dwarfs_permutation_symmetry_on_parity_at_every_scale() {
fn factorial(n: u128) -> u128 {
(1..=n).product()
}
let bn_stab = |n: u32| -> u128 { factorial(n as u128) * (1u128 << (n - 1)) };
let agl_stab = |n: u32| -> u128 { gf2::gl_order(n) / ((1u128 << n) - 1) * (1u128 << (n - 1)) };
for n in 3..=4u32 {
let clauses: Vec<Vec<Lit>> = (0u64..(1u64 << n))
.filter(|a| a.count_ones() % 2 == 1)
.map(|a| (0..n).map(|v| Lit::new(v, (a >> v) & 1 == 0)).collect())
.collect();
let syms = affine_symmetries(n as usize, &clauses);
let is_perm = |a: &Affine| -> bool {
let mut cols = 0u64;
for &row in a.matrix.iter().take(n as usize) {
if row.count_ones() != 1 || cols & row != 0 {
return false;
}
cols |= row;
}
cols == (1u64 << n) - 1
};
let bn_count = syms.iter().filter(|a| is_perm(a)).count() as u128;
assert_eq!(syms.len() as u128, agl_stab(n), "n={n}: AGL parity-stabilizer = |GL(n,2)|/(2ⁿ−1)·2^{{n−1}}");
assert_eq!(bn_count, bn_stab(n), "n={n}: Bₙ parity-stabilizer = n!·2^{{n−1}}");
assert!(agl_stab(n) > bn_stab(n), "n={n}: AGL symmetry exceeds Bₙ symmetry");
}
let ratios: Vec<u128> = (3..=8u32).map(|n| agl_stab(n) / bn_stab(n)).collect();
eprintln!("AGL/Bₙ parity-symmetry ratio, n = 3..8: {ratios:?}");
assert!(ratios.windows(2).all(|w| w[1] > w[0]), "the AGL/Bₙ ratio grows with n: {ratios:?}");
assert!(*ratios.last().unwrap() > 1_000_000, "by n=8 the lens misses a >10⁶ factor");
}
#[test]
fn affine_family_symmetry_closed_form_scales_to_all_n() {
for n in 2..=4u32 {
for k in 1..n {
let clauses: Vec<Vec<Lit>> = (k..n).map(|j| vec![Lit::neg(j)]).collect();
assert_eq!(
affine_symmetries(n as usize, &clauses).len() as u128,
affine_subspace_agl_order(n, k),
"n={n} k={k}: closed-form affine-subspace order must match the exhaustive count"
);
}
}
let bn = |n: u128| (1u128 << n) * (1..=n).product::<u128>();
let ratios: Vec<u128> = (6..=12u32).map(|n| affine_subspace_agl_order(n, n / 2) / bn(n as u128)).collect();
eprintln!("affine-family AGL / |Bₙ| ratio, n = 6..12: {ratios:?}");
assert!(ratios.iter().all(|&r| r > 1), "the affine symmetry exceeds |Bₙ| at every n");
assert!(ratios.windows(2).all(|w| w[1] > w[0]), "the gap grows with n: {ratios:?}");
assert!(*ratios.last().unwrap() > 1_000_000_000, "by n=12 the affine symmetry is a >10⁹ factor beyond |Bₙ|");
}
#[test]
fn affine_reduce_refutes_an_inconsistent_linear_core() {
let p = |v: u32| Lit::pos(v);
let q = |v: u32| Lit::neg(v);
let clauses = vec![
vec![q(0), p(1)], vec![p(0), q(1)], vec![q(1), p(2)], vec![p(1), q(2)], vec![p(0), p(2)], vec![q(0), q(2)], ];
match affine_reduce(3, &clauses) {
AffineOutcome::Refuted(Some(drat)) => assert!(
crate::rup::check_refutation(3, &clauses, &drat),
"the affine refutation's xor_drat certificate must RUP-refute the original CNF"
),
other => panic!("expected a certified refutation, got {other:?}"),
}
assert!(models_of(3, &clauses).is_empty(), "the linear core is genuinely unsatisfiable");
}
#[test]
fn affine_refutation_is_certified_via_xor_drat_bridge() {
use crate::solve::{solve_structured, Answer};
let (_, cnf, _) = crate::families::tseitin_expander(6, 1);
let nv = cnf.num_vars;
let drat = affine_refutation_drat(nv, &cnf.clauses).expect("an inconsistent XOR core has a certificate");
assert!(!drat.is_empty(), "the certificate must carry resolvent lemmas");
assert!(
crate::rup::check_refutation(nv, &cnf.clauses, &drat),
"the xor_drat certificate must RUP-refute the original CNF"
);
assert!(matches!(solve_structured(nv, &cnf.clauses).answer, Answer::Unsat), "the parity core is UNSAT");
}
#[test]
fn affine_reduce_derives_a_nonsyntactic_forced_unit() {
let p = |v: u32| Lit::pos(v);
let q = |v: u32| Lit::neg(v);
let clauses = vec![
vec![p(0), p(1)], vec![q(0), q(1)],
vec![q(0), p(1), p(2)], vec![p(0), q(1), p(2)], vec![p(0), p(1), q(2)], vec![q(0), q(1), q(2)],
];
match affine_reduce(3, &clauses) {
AffineOutcome::Forced(extra) => {
assert!(extra.contains(&vec![Lit::pos(2)]), "must derive the forced unit x2 = 1; got {extra:?}");
}
other => panic!("expected forced consequences, got {other:?}"),
}
for m in models_of(3, &clauses) {
assert_eq!((m >> 2) & 1, 1, "x2 must be 1 in every model");
}
}
#[test]
fn the_dispatcher_decides_affine_reducible_formulas() {
use crate::solve::{solve_structured, Answer};
let p = |v: u32| Lit::pos(v);
let q = |v: u32| Lit::neg(v);
let unsat = vec![
vec![q(0), p(1)], vec![p(0), q(1)], vec![q(1), p(2)], vec![p(1), q(2)], vec![p(0), p(2)], vec![q(0), q(2)],
];
assert!(matches!(solve_structured(3, &unsat).answer, Answer::Unsat), "dispatcher must refute the linear core");
let sat = vec![
vec![p(0), p(1)], vec![q(0), q(1)],
vec![q(0), p(1), p(2)], vec![p(0), q(1), p(2)], vec![p(0), p(1), q(2)], vec![q(0), q(1), q(2)],
];
match solve_structured(3, &sat).answer {
Answer::Sat(m) => assert!(
sat.iter().all(|c| c.iter().any(|l| m[l.var() as usize] == l.is_positive())),
"the returned model must satisfy the original formula"
),
Answer::Unsat => panic!("the forced-consequence formula is satisfiable"),
}
}
#[test]
fn affine_reduce_is_sound_and_quiet_when_there_is_nothing_to_do() {
let p = |v: u32| Lit::pos(v);
let plain = vec![vec![p(0), p(1), p(2)]];
assert_eq!(affine_reduce(3, &plain), AffineOutcome::Unchanged);
let q = |v: u32| Lit::neg(v);
let sat_parity = vec![
vec![q(0), p(1), p(2)], vec![p(0), q(1), p(2)], vec![p(0), p(1), q(2)], vec![q(0), q(1), q(2)],
];
assert!(!matches!(affine_reduce(3, &sat_parity), AffineOutcome::Refuted(_)), "a satisfiable plane must not be refuted");
}
#[test]
fn affine_canonicalize_collapses_an_equivalence_chain() {
let p = |v: u32| Lit::pos(v);
let q = |v: u32| Lit::neg(v);
let clauses = vec![
vec![q(0), p(1)], vec![p(0), q(1)], vec![q(1), p(2)], vec![p(1), q(2)], vec![q(2), p(3)], vec![p(2), q(3)], vec![p(0), p(4)], vec![q(4)], ];
match affine_canonicalize(5, &clauses) {
AffineCanon::Canonical(canon) => {
assert!(canon.num_vars < 5, "the chain + unit must shrink 5 vars (got {})", canon.num_vars);
let orig = models_of(5, &clauses);
let red = models_of(canon.num_vars, &canon.clauses);
assert_eq!(red.is_empty(), orig.is_empty(), "reduction must preserve satisfiability");
for &rm_bits in &red {
let rm: Vec<bool> = (0..canon.num_vars).map(|i| (rm_bits >> i) & 1 == 1).collect();
let lifted = canon.lift(&rm);
assert!(
clauses.iter().all(|c| c.iter().any(|l| lifted[l.var() as usize] == l.is_positive())),
"the lifted reduced model must satisfy the original formula"
);
}
}
AffineCanon::Refuted(_) => panic!("the chain is satisfiable, not refuted"),
AffineCanon::Unchanged => panic!("the chain + unit must reduce"),
}
}
#[test]
fn affine_canonicalize_is_sound_against_brute_force() {
let mut state = 0xA5F1_C0DE_1234_5678u64;
let mut rng = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
for _ in 0..400 {
let n = 4 + (rng() % 8) as usize; let mut clauses: Vec<Vec<Lit>> = Vec::new();
for _ in 0..(rng() % 4) {
let k = 2 + (rng() % 2) as usize;
let c: Vec<Lit> = (0..k).map(|_| Lit::new((rng() % n as u64) as u32, rng() & 1 == 0)).collect();
clauses.push(c);
}
for _ in 0..(1 + rng() % 3) {
let a = (rng() % n as u64) as u32;
let b = (rng() % n as u64) as u32;
if rng() & 1 == 0 {
if a == b {
continue;
}
if rng() & 1 == 0 {
clauses.push(vec![Lit::neg(a), Lit::pos(b)]); clauses.push(vec![Lit::pos(a), Lit::neg(b)]);
} else {
clauses.push(vec![Lit::pos(a), Lit::pos(b)]); clauses.push(vec![Lit::neg(a), Lit::neg(b)]);
}
} else {
clauses.push(vec![Lit::new(a, rng() & 1 == 0)]); }
}
let orig = models_of(n, &clauses);
match affine_canonicalize(n, &clauses) {
AffineCanon::Refuted(_) => assert!(orig.is_empty(), "Refuted ⇒ the original must be UNSAT"),
AffineCanon::Canonical(canon) => {
let red = models_of(canon.num_vars, &canon.clauses);
assert_eq!(red.is_empty(), orig.is_empty(), "canonicalization must preserve satisfiability");
for &rm_bits in &red {
let rm: Vec<bool> = (0..canon.num_vars).map(|i| (rm_bits >> i) & 1 == 1).collect();
let lifted = canon.lift(&rm);
assert!(
clauses.iter().all(|c| c.iter().any(|l| lifted[l.var() as usize] == l.is_positive())),
"every lifted reduced model must satisfy the original"
);
}
}
AffineCanon::Unchanged => {}
}
}
}
}