Skip to main content

logicaffeine_proof/
census.rs

1//! **The small-`n` SAT-space census** — the measurement instrument.
2//!
3//! Brute-force over *every* minimal UNSAT formula (= every minimal subcube cover, the irreducible MUS
4//! atoms of the UNSAT universe) for a given `n`, one representative per hyperoctahedral (`Bₙ`) orbit, and
5//! attach to each orbit the full menu of invariants the solver stack can read off it: its symmetry group
6//! (orbit size + stabilizer), its face vector, its minimum resolution width, where it sits on the
7//! certified proof-complexity ladder ([`ProofRung`] / [`Shadow`]), and which engine the full structured
8//! router ([`crate::solve::solve_structured`]) actually decides it with.
9//!
10//! The point is the **audit**: the certified diagnostic ladder ([`weakest_crushing_rung`],
11//! [`diagnose`]) and the full router can *disagree* about a cover — e.g. the router crushes a mod-`p`
12//! germ with the polynomial GF(`p`) engine ([`crate::solve::Route::ModP`]) while the certified ladder,
13//! which has no mod-`p` rung, sees only a high-degree GF(2) Nullstellensatz. Every such disagreement is a
14//! concrete, located gap between *what the solver can do* and *what the certified cascade can prove
15//! cheaply.* The census enumerates them exhaustively at small `n`.
16
17use std::collections::BTreeMap;
18
19use crate::affine::{all_affine_bijections, Affine};
20use crate::cdcl::Lit;
21use crate::hypercube::{
22    canonical_cover, cube_group_closure, diagnose, face_vector, hyperoctahedral_generators,
23    min_resolution_width, minimal_cover_orbits, weakest_crushing_rung,
24    weakest_crushing_rung_with_char, Cover, ProofRung, Shadow,
25};
26use crate::solve::{solve_structured, Answer, Route};
27
28/// One `Bₙ`-orbit of minimal UNSAT formulas over `n` variables, with every invariant the solver can
29/// compute about it. The raw measurements; the audit flags ([`OrbitRecord::router_beats_ladder`],
30/// `OrbitRecord::finder_is_incomplete`) are derived from them.
31#[derive(Clone, Debug)]
32pub struct OrbitRecord {
33    pub n: usize,
34    /// A canonical representative cover of the orbit.
35    pub rep: Cover,
36    pub num_clauses: usize,
37    /// Number of distinct covers in the `Bₙ` orbit.
38    pub orbit_size: usize,
39    /// `|Stab| = |Bₙ| / orbit_size` — how symmetric this formula is.
40    pub stabilizer_order: usize,
41    /// Blockers per face dimension — a geometric invariant of the cover.
42    pub face_vector: BTreeMap<usize, usize>,
43    /// Minimum width of any resolution refutation.
44    pub min_res_width: usize,
45    /// The weakest *certified* cut on the proof-complexity ladder (GF(2) Nullstellensatz at the top).
46    pub rung: ProofRung,
47    /// The single-label certified shadow the diagnoser reads off (`Counting`/`Parity`/`CuttingPlanes`).
48    pub shadow: Option<Shadow>,
49    /// Which engine the full structured router actually decides it with.
50    pub route: Route,
51    /// Self-driving rule-symmetry: how many essentially-distinct clauses the *discovered* automorphisms
52    /// leave (the production symmetry breaker, `symmetry_detect::find_generators`, used in the cascade).
53    pub discovered_rule_orbits: usize,
54    /// The same count under the cover's **full** `Bₙ` stabilizer (every geometric automorphism, computed
55    /// exhaustively from the group). This is the strongest symmetry break achievable. When
56    /// `discovered_rule_orbits > full_rule_orbits`, the production breaker is leaving symmetry unbroken —
57    /// a case the solver could collapse further but currently does not.
58    pub full_rule_orbits: usize,
59    /// Is the UNSAT explained by an inconsistent GF(2) linear system (the parity shadow)?
60    pub affine_explained: bool,
61    /// Did the structured router crush it with the polynomial mod-`p` (GF(`p`)) engine?
62    pub modp_routed: bool,
63}
64
65impl OrbitRecord {
66    /// **Gap signal.** The full router crushed this cover with a *polynomial specialist* the certified
67    /// proof-complexity ladder has no rung for — the router sees structure the certified cascade can only
68    /// reach by an expensive general proof. The clearest case: a mod-`p` germ routed to `ModP` while the
69    /// ladder reports a (GF(2)) `Nullstellensatz` height. This is the unification gap, per orbit.
70    pub fn router_beats_ladder(&self) -> bool {
71        matches!(self.route, Route::ModP | Route::Collapse | Route::HybridXor)
72            && !matches!(self.rung, ProofRung::Counting | ProofRung::Parity)
73    }
74
75    /// **Symmetry left on the table.** The production breaker discovered strictly fewer rule-merges than
76    /// the cover's full geometric stabilizer admits — so the symmetry-breaking predicate it injects is
77    /// weaker than achievable, and a scaled-up member of this family will cost the solver search it could
78    /// have avoided. Every such orbit is a concrete "break more symmetry → solve more cases" target.
79    pub fn symmetry_underbroken(&self) -> bool {
80        self.discovered_rule_orbits > self.full_rule_orbits
81    }
82}
83
84/// The orbit's ladder placement under the **characteristic-extended cascade**
85/// ([`weakest_crushing_rung_with_char`]), recomputed from the representative cover. The audit-gap
86/// closer: an orbit the router crushes via its mod-`p` specialist while the legacy ladder pays an
87/// algebraic price ([`OrbitRecord::router_beats_ladder`]) lands on its `ModCount { p }` rung here;
88/// every cover outside the mod-`p` population is placed exactly as the legacy ladder places it.
89pub fn extended_rung(rec: &OrbitRecord, primes: &[u64]) -> ProofRung {
90    weakest_crushing_rung_with_char(rec.n, &rec.rep.clauses(), rec.n, primes)
91}
92
93/// **The hardness spectrum of the `n`-variable SAT hypercube** — the durable "how much do we cover, and
94/// where is the hard core" map, aggregated from [`census`]. Each minimal-UNSAT family is placed on the
95/// certified proof-complexity ladder by its *weakest crushing rung* (the cheapest proof system that
96/// refutes it), and split by whether it carries `Bₙ` symmetry to break. The `Nullstellensatz` rung is
97/// further resolved by minimum degree — the algebraic-hardness dial — so the spectrum shows not just
98/// *that* the residue is algebraic but *how deep* it sits.
99#[derive(Clone, Debug)]
100pub struct CoverageSummary {
101    pub n: usize,
102    pub orbits: usize,
103    /// Family count per proof rung: `trivial` (unit propagation), `counting`, `parity`,
104    /// `nullstellensatz-d{degree}`, `beyond-budget`. The rung is exactly which proof system covers it.
105    pub by_rung: BTreeMap<String, usize>,
106    /// The deepest minimum-Nullstellensatz degree at this `n` — the algebraic-hardness ceiling.
107    pub max_ns_degree: usize,
108    /// Families with a non-trivial `Bₙ` stabilizer — symmetry there is to break.
109    pub structured: usize,
110    /// Families with trivial stabilizer — the rigid residue, no symmetry shortcut.
111    pub rigid: usize,
112    /// Distribution of minimum resolution width across the families.
113    pub by_resolution_width: BTreeMap<usize, usize>,
114}
115
116/// Aggregate the [`census`] into the [`CoverageSummary`] hardness spectrum.
117pub fn coverage_summary(n: usize) -> CoverageSummary {
118    let mut by_rung: BTreeMap<String, usize> = BTreeMap::new();
119    let mut by_resolution_width: BTreeMap<usize, usize> = BTreeMap::new();
120    let (mut max_ns_degree, mut structured, mut rigid) = (0usize, 0usize, 0usize);
121    let records = census(n);
122    for r in &records {
123        let label = match r.rung {
124            ProofRung::Trivial => "trivial".to_string(),
125            ProofRung::Counting => "counting".to_string(),
126            ProofRung::Parity => "parity".to_string(),
127            ProofRung::ModCount { p } => format!("modcount-p{p}"),
128            ProofRung::Nullstellensatz { min_degree } => {
129                max_ns_degree = max_ns_degree.max(min_degree);
130                format!("nullstellensatz-d{min_degree}")
131            }
132            ProofRung::BeyondBudget => "beyond-budget".to_string(),
133        };
134        *by_rung.entry(label).or_insert(0) += 1;
135        *by_resolution_width.entry(r.min_res_width).or_insert(0) += 1;
136        if r.stabilizer_order > 1 {
137            structured += 1;
138        } else {
139            rigid += 1;
140        }
141    }
142    CoverageSummary { n, orbits: records.len(), by_rung, max_ns_degree, structured, rigid, by_resolution_width }
143}
144
145/// **The residue map** — the honest "here's what we crush, and here's the wall" partition of the census.
146/// Every minimal-UNSAT family lands in exactly one of: *crushed* (a structural specialist route decided it —
147/// parity, mod-`p`, counting, a symmetry route, SoS, …), or the *residue* (routed to `Incompressible`/`Cdcl`
148/// — no lens caught it). Within those, `targetable` are the families still carrying unbroken symmetry (break
149/// more → crush cheaper), and `rigid_core` ⊆ residue are the families with *no* symmetry to break at all —
150/// the incompressible wall the counting bound predicts. This is a search *with structure*: not random probing,
151/// but a ranked pass of every lens over every family, surfacing exactly which ones resist and why.
152#[derive(Clone, Debug)]
153pub struct ResidueMap {
154    pub n: usize,
155    pub total: usize,
156    /// A structural specialist route decided it (route ∉ {Incompressible, Cdcl}).
157    pub crushed: usize,
158    /// No lens caught it — fell through to CDCL / the certified "no shortcut" verdict. The hard core.
159    pub residue: usize,
160    /// Families with symmetry left unbroken (`discovered_rule_orbits > full_rule_orbits`) — the concrete
161    /// "break more symmetry to crush this" targets. `0` means the symmetry breaking is *complete*.
162    pub targetable: usize,
163    /// Residue families that are also rigid (trivial `Bₙ` stabilizer) — no symmetry exists to break, so the
164    /// wall here is intrinsic, not a gap in our breaking.
165    pub rigid_core: usize,
166    /// The deepest minimum-Nullstellensatz degree among the residue — how algebraically deep the wall sits.
167    pub core_max_ns_degree: usize,
168}
169
170/// The set of falsifying assignments of a clause, as a bitmask over the `2ⁿ` points of `𝔽₂ⁿ` (`n ≤ 6`). A
171/// clause `C` is falsified by `x` iff every literal is false: `+v` ⟹ `x_v = 0`, `¬v` ⟹ `x_v = 1`.
172fn falsify_set(clause: &[Lit], num_vars: usize) -> u64 {
173    let mut set = 0u64;
174    for x in 0..(1u64 << num_vars) {
175        if clause.iter().all(|lit| {
176            let bit = (x >> (lit.var() as u64)) & 1;
177            if lit.is_positive() { bit == 0 } else { bit == 1 }
178        }) {
179            set |= 1 << x;
180        }
181    }
182    set
183}
184
185/// Push a point-set through an affine map `φ`: `{φ(p) : p ∈ s}`.
186fn map_point_set(s: u64, phi: &Affine, num_vars: usize) -> u64 {
187    let mut out = 0u64;
188    for x in 0..(1u64 << num_vars) {
189        if (s >> x) & 1 == 1 {
190            out |= 1 << phi.apply(x);
191        }
192    }
193    out
194}
195
196/// Recover the clause whose falsifying-point set is exactly `s`, or `None` if `s` is not an axis-aligned
197/// subcube (a shear can map a clause's cube to a non-cube — precisely the images that are not CNFs). A
198/// coordinate is *fixed* iff every point of `s` agrees on it; `s` is a subcube iff it equals the full
199/// cylinder over the fixed pattern (`|s| = 2^{free}` with every matching point present). The literal on a
200/// fixed coordinate `i` with value `v` is the one falsified there: positive when `v = 0`, negative when
201/// `v = 1` (the inverse of [`falsify_set`]).
202fn pointset_to_clause(s: u64, num_vars: usize) -> Option<Vec<Lit>> {
203    if s == 0 {
204        return None;
205    }
206    let points: Vec<u64> = (0..(1u64 << num_vars)).filter(|&x| (s >> x) & 1 == 1).collect();
207    let mut clause = Vec::new();
208    let mut free = 0usize;
209    for i in 0..num_vars as u64 {
210        let first = (points[0] >> i) & 1;
211        if points.iter().all(|&p| (p >> i) & 1 == first) {
212            clause.push(Lit::new(i as u32, first == 0)); // fixed: positive iff the fixed value is 0
213        } else {
214            free += 1;
215        }
216    }
217    // A genuine subcube: the fixed pattern spans exactly `2^{free}` points and `s` holds all of them.
218    if points.len() != (1usize << free) {
219        return None;
220    }
221    Some(clause)
222}
223
224/// The image of a CNF under an affine map `φ`, or `None` if any clause's cube maps to a non-cube (so the
225/// image is not a CNF). Each clause's falsifying-point set is pushed through `φ` and read back as a clause.
226fn agl_image_formula(clauses: &[Vec<Lit>], phi: &Affine, num_vars: usize) -> Option<Vec<Vec<Lit>>> {
227    clauses
228        .iter()
229        .map(|c| pointset_to_clause(map_point_set(falsify_set(c, num_vars), phi, num_vars), num_vars))
230        .collect()
231}
232
233/// The image of a clause under the transvection `σ : x_i ↦ x_i ⊕ x_j`, computed **symbolically** (`O(clause)`
234/// — no `2ⁿ` point set), or `None` if `σ` maps the clause's blocker subcube to a non-subcube (so `σ` is not a
235/// clause automorphism). Derivation: `σ` is an involution, so the blocker constraint `x_i = c_i` becomes
236/// `y_i ⊕ y_j = c_i`, which stays axis-aligned iff `y_j` is fixed (`j ∈ support`), giving the new value
237/// `c_i ⊕ c_j` at `i`. So the image *is* the clause, except: when `i ∈ support` and `j ∉ support` it is not a
238/// subcube (`None`); and when both `i, j ∈ support` the literal at `i` flips iff `j`'s literal is negative.
239fn transvection_image_clause(clause: &[Lit], i: u32, j: u32) -> Option<Vec<Lit>> {
240    let lit_i = clause.iter().find(|l| l.var() == i);
241    let lit_j = clause.iter().find(|l| l.var() == j);
242    match (lit_i, lit_j) {
243        (Some(_), None) => None,
244        (Some(_), Some(lj)) if !lj.is_positive() => {
245            Some(clause.iter().map(|l| if l.var() == i { l.negated() } else { *l }).collect())
246        }
247        _ => Some(clause.to_vec()),
248    }
249}
250
251/// A canonical, order-/duplicate-independent key for a clause set.
252fn clause_set_key(clauses: &[Vec<Lit>]) -> std::collections::BTreeSet<Vec<(u32, bool)>> {
253    clauses
254        .iter()
255        .map(|c| {
256            let mut k: Vec<(u32, bool)> = c.iter().map(|l| (l.var(), l.is_positive())).collect();
257            k.sort_unstable();
258            k
259        })
260        .collect()
261}
262
263/// **A scalable, `∀n` affine-symmetry finder — the transvection generators, in polynomial time.** Exhaustive
264/// `AGL(n,2)` enumeration is stuck at `n ≤ 4`; this finds every *transvection* `x_i ↦ x_i ⊕ x_j` that is an
265/// affine automorphism of the clause set, in `O(n² · |clauses|)` time via [`transvection_image_clause`] — no
266/// `2ⁿ` anywhere, so it runs at any `n`. Sound: every returned pair is a genuine automorphism (the produced
267/// clause set is checked equal to the original). *Incomplete* by design: an affine symmetry realized only by a
268/// *composite* of transvections (e.g. parity's shears, products of two transvections) is not a single
269/// generator and is not returned — completeness for general formulas is a hard affine-equivalence problem, and
270/// the closed form ([`crate::affine::affine_subspace_agl_order`]) is the route for structured families.
271pub fn affine_transvection_generators(num_vars: usize, clauses: &[Vec<Lit>]) -> Vec<(u32, u32)> {
272    let original = clause_set_key(clauses);
273    let mut gens = Vec::new();
274    for i in 0..num_vars as u32 {
275        for j in 0..num_vars as u32 {
276            if i == j {
277                continue;
278            }
279            let image: Option<Vec<Vec<Lit>>> =
280                clauses.iter().map(|c| transvection_image_clause(c, i, j)).collect();
281            if let Some(img) = image {
282                if clause_set_key(&img) == original {
283                    gens.push((i, j));
284                }
285            }
286        }
287    }
288    gens
289}
290
291/// The image of a clause under a **composite shear** `x_i ↦ x_i ⊕ x_j for every i ∈ targets` (with `j ∉
292/// targets`) — a product of transvections that all share the source `j`, hence commute (disjoint
293/// destinations), so their action is the sequential composition of [`transvection_image_clause`]. `None` if
294/// any stage leaves the subcube world.
295fn composite_shear_image_clause(clause: &[Lit], targets: &[u32], j: u32) -> Option<Vec<Lit>> {
296    let mut c = clause.to_vec();
297    for &i in targets {
298        c = transvection_image_clause(&c, i, j)?;
299    }
300    Some(c)
301}
302
303/// All size-`k` subsets of `pool` (small `k`; `pool` small).
304fn combinations(pool: &[u32], k: usize) -> Vec<Vec<u32>> {
305    if k == 0 {
306        return vec![Vec::new()];
307    }
308    let mut out = Vec::new();
309    for (idx, &x) in pool.iter().enumerate() {
310        for mut rest in combinations(&pool[idx + 1..], k - 1) {
311            rest.insert(0, x);
312            out.push(rest);
313        }
314    }
315    out
316}
317
318/// **A bounded-composite affine-symmetry finder — climbing the "single-transvection wall" instead of
319/// declaring it.** [`affine_transvection_generators`] only sees *depth-1* shears (a single `x_i ↦ x_i ⊕ x_j`),
320/// so it is blind to symmetries realized only as a *product* of transvections — the parity/linear shears are
321/// the canonical example ("add `x_j` to `x_i` **and** `x_k`" preserves `⊕ x = b` because it adds `x_j` to the
322/// form twice `= 0`; neither half does). This extends the finder to shears of composite depth `≤
323/// max_targets`: for every source `j` and every target set `S ⊆ vars∖{j}` with `1 ≤ |S| ≤ max_targets`, the
324/// commuting product "add `x_j` to each `x_i ∈ S`". At `max_targets = 1` it *is* the transvection finder; at
325/// `max_targets = 2` it catches the parity shears I earlier called out of reach. Polynomial for fixed depth
326/// (`O(n^{max_targets+1}·|clauses|)`), sound (every returned generator is verified to permute the clause set),
327/// and still incomplete only at *unbounded* depth — but the depth is now a tunable window, and the composite
328/// depth a formula's symmetries require is itself a complexity measure. Returns each generator as `(S, j)`.
329pub fn affine_composite_shear_generators(
330    num_vars: usize,
331    clauses: &[Vec<Lit>],
332    max_targets: usize,
333) -> Vec<(Vec<u32>, u32)> {
334    let original = clause_set_key(clauses);
335    let mut gens = Vec::new();
336    for j in 0..num_vars as u32 {
337        let others: Vec<u32> = (0..num_vars as u32).filter(|&v| v != j).collect();
338        for size in 1..=max_targets.min(others.len()) {
339            for combo in combinations(&others, size) {
340                let image: Option<Vec<Vec<Lit>>> =
341                    clauses.iter().map(|c| composite_shear_image_clause(c, &combo, j)).collect();
342                if let Some(img) = image {
343                    if clause_set_key(&img) == original {
344                        gens.push((combo, j));
345                    }
346                }
347            }
348        }
349    }
350    gens
351}
352
353/// The image of a clause under the **general rank-1 GF(2) involution** `M_{u,v} : x ↦ x ⊕ (v·x)·u` (masks `u,
354/// v`; a bijection iff `u·v = 0`), or `None` if `M` sends the clause's blocker subcube to a non-subcube. This
355/// is the affine shear generalized: the shear `x_i ↦ x_i⊕x_j for i∈S` is `M_{u=1_S, v=e_j}`, and the
356/// **symplectic transvection** `T_w = I⊕w wᵀ` is `M_{u=w, v=w}`.
357///
358/// A clause's blocker is the affine subcube `{y : y_i = c_i, i∈T}` (`c_i = 0` for a positive literal at `i`,
359/// `1` for a negative; `T = supp`). Since `M` is an involution, `M(blocker) = {y : (M y)_i = c_i, i∈T}`, i.e.
360/// the linear system with rows `(e_i ⊕ u_i·v)` and right-hand sides `c_i` — because row `i` of `M = I⊕uvᵀ` is
361/// `e_i ⊕ u_i v`. Row-reduce over `GF(2)`: the image is a clause iff the reduced system is *axis-aligned*
362/// (every pivot row a single coordinate), and then each reduced row `y_k = b` is the literal on `k`. A rank-1
363/// map can re-align a clause onto a *different* support (that is exactly why the naive support-preserving rule
364/// is wrong); the row reduction handles it. Cost `O(|T|²)`, no `2ⁿ`. Cross-checked against the exhaustive
365/// point-set image in `rank1_symbolic_image_matches_the_pointset_computation`.
366fn rank1_image_clause(clause: &[Lit], u: u64, v: u64) -> Option<Vec<Lit>> {
367    // RREF of the constraint rows (mask, rhs) over GF(2), pivoting on the highest set bit.
368    let mut pivots: Vec<(u64, u8)> = Vec::new();
369    for l in clause {
370        let i = l.var();
371        let mut row = (1u64 << i) ^ (if (u >> i) & 1 == 1 { v } else { 0 });
372        let mut rhs: u8 = if l.is_positive() { 0 } else { 1 };
373        for &(prow, prhs) in &pivots {
374            let pbit = 1u64 << (63 - prow.leading_zeros());
375            if row & pbit != 0 {
376                row ^= prow;
377                rhs ^= prhs;
378            }
379        }
380        if row == 0 {
381            if rhs != 0 {
382                return None; // inconsistent (empty image — cannot happen for a bijection, guarded anyway)
383            }
384            continue;
385        }
386        let nbit = 1u64 << (63 - row.leading_zeros());
387        for p in pivots.iter_mut() {
388            if p.0 & nbit != 0 {
389                p.0 ^= row;
390                p.1 ^= rhs;
391            }
392        }
393        pivots.push((row, rhs));
394    }
395    let mut lits = Vec::with_capacity(pivots.len());
396    for (row, rhs) in pivots {
397        if row.count_ones() != 1 {
398            return None; // reduced constraint couples two coordinates — image is not axis-aligned
399        }
400        lits.push(Lit::new(row.trailing_zeros(), rhs == 0));
401    }
402    Some(lits)
403}
404
405/// Every even-weight mask over `num_vars` bits with weight in `2..=max_weight` (the symplectic-transvection
406/// candidates: `w·w = weight(w) mod 2 = 0`).
407fn even_weight_masks(num_vars: usize, max_weight: usize) -> Vec<u64> {
408    let pool: Vec<u32> = (0..num_vars as u32).collect();
409    let mut out = Vec::new();
410    let mut k = 2;
411    while k <= max_weight.min(num_vars) {
412        for combo in combinations(&pool, k) {
413            out.push(combo.iter().fold(0u64, |m, &b| m | (1u64 << b)));
414        }
415        k += 2;
416    }
417    out
418}
419
420/// **The symplectic-transvection symmetry finder — one rung above affine shears.** An affine shear is a rank-1
421/// map whose right factor is a *unit* vector (`v = e_j`); dropping that restriction to a general `v` gives the
422/// full rank-1 group, whose bijective involutions are the **symplectic transvections** `T_w = I ⊕ w wᵀ` (even
423/// weight `w`). Where the affine dichotomy of §4 found permutation-symmetric families (PHP, mod-counting) to be
424/// affine-shear-*rigid*, this finder can still see their higher symmetry: it enumerates every even-weight `w`
425/// with `2 ≤ |w| ≤ max_weight` and returns those whose `T_w` permutes the clause set (verified via
426/// [`rank1_image_clause`]). Cost `O(Σ_{k≤W even} C(n,k) · |clauses|)` — polynomial for fixed weight. Sound
427/// (every returned `w` is a checked automorphism); complete only up to the chosen weight. The *weight* of the
428/// smallest such symmetry is the graded invariant we track against NS degree.
429pub fn symplectic_transvection_generators(
430    num_vars: usize,
431    clauses: &[Vec<Lit>],
432    max_weight: usize,
433) -> Vec<u64> {
434    let original = clause_set_key(clauses);
435    let mut gens = Vec::new();
436    for w in even_weight_masks(num_vars, max_weight) {
437        let image: Option<Vec<Vec<Lit>>> =
438            clauses.iter().map(|c| rank1_image_clause(c, w, w)).collect();
439        if let Some(img) = image {
440            if clause_set_key(&img) == original {
441                gens.push(w);
442            }
443        }
444    }
445    gens
446}
447
448/// **The clause-level AGL symmetry detector** — the affine analog of the `Bₙ` stabilizer, and the one
449/// symmetry lens that works on UNSAT formulas (the model-based detector is vacuous — no models). An affine
450/// map `φ : x ↦ Ax ⊕ b` (`φ ∈ AGL(n,2)`) is a clause-set automorphism iff, acting on `𝔽₂ⁿ`, it *permutes the
451/// falsifying-point sets of the clauses*. Since `Bₙ ⊆ AGL(n,2)`, `|Aut_AGL| ≥ |Bₙ-stabilizer|`; a `Bₙ`-rigid
452/// core with `|Aut_AGL| > 1` has **hidden affine symmetry** (breakable by an affine SBP); `|Aut_AGL| = 1`
453/// means it is affine-rigid — structure-minimal at the affine level too. Brute over `AGL(n,2)`, feasible
454/// `n ≤ 4`.
455pub fn clause_agl_symmetries(num_vars: usize, clauses: &[Vec<Lit>]) -> usize {
456    let blockers: std::collections::HashSet<u64> =
457        clauses.iter().map(|c| falsify_set(c, num_vars)).collect();
458    all_affine_bijections(num_vars)
459        .into_iter()
460        .filter(|phi| blockers.iter().all(|&s| blockers.contains(&map_point_set(s, phi, num_vars))))
461        .count()
462}
463
464/// The sorted falsifying-point-set masks of a cover's clauses — the cover as a set of subcubes in point
465/// space, the representation the affine group acts on directly.
466fn blocker_masks(clauses: &[Vec<Lit>], num_vars: usize) -> Vec<u64> {
467    let mut m: Vec<u64> = clauses.iter().map(|c| falsify_set(c, num_vars)).collect();
468    m.sort_unstable();
469    m.dedup();
470    m
471}
472
473/// The point-permutation table of an affine map: `table[x] = φ(x)` over the `2ⁿ` corners. Precomputed once
474/// so the group action on point-set masks is fast bit-arithmetic rather than a fresh `apply` per point.
475fn perm_table(phi: &Affine, num_vars: usize) -> Vec<u32> {
476    (0..(1u64 << num_vars)).map(|x| phi.apply(x) as u32).collect()
477}
478
479/// Push a point-set mask through a precomputed permutation table: `{table[x] : x ∈ s}`.
480fn map_mask(s: u64, table: &[u32]) -> u64 {
481    let mut out = 0u64;
482    let mut bits = s;
483    while bits != 0 {
484        let x = bits.trailing_zeros() as usize;
485        out |= 1u64 << table[x];
486        bits &= bits - 1;
487    }
488    out
489}
490
491/// The canonical form of a point-set collection under a group of affine maps (given as permutation tables):
492/// the lexicographically minimal sorted image. Over the full `AGL(n,2)` this is the affine-orbit invariant —
493/// two CNFs are AGL-equivalent iff their blocker collections share it (the minimal image need not itself be a
494/// CNF; it is only a label) — and over the `Bₙ` subgroup it is the signed-permutation canonical form.
495fn canonical_over_tables(masks: &[u64], tables: &[Vec<u32>]) -> Vec<u64> {
496    let mut best: Option<Vec<u64>> = None;
497    for t in tables {
498        let mut img: Vec<u64> = masks.iter().map(|&s| map_mask(s, t)).collect();
499        img.sort_unstable();
500        match &best {
501            Some(b) if *b <= img => {}
502            _ => best = Some(img),
503        }
504    }
505    best.unwrap_or_default()
506}
507
508/// Whether an affine map's linear part is a permutation matrix (exactly one set bit per row, columns a
509/// permutation) — i.e. the map is a signed permutation, an element of `Bₙ`.
510fn is_permutation_matrix(matrix: &[u64], num_vars: usize) -> bool {
511    let mut cols = 0u64;
512    for row in matrix.iter().take(num_vars) {
513        if row.count_ones() != 1 || cols & row != 0 {
514            return false;
515        }
516        cols |= row;
517    }
518    cols == (1u64 << num_vars) - 1
519}
520
521/// The `Bₙ` subgroup of `AGL(n,2)`: the affine maps whose linear part is a permutation matrix (signed
522/// permutations = variable permutations × literal negations). `|Bₙ| = n!·2ⁿ`.
523fn bn_affines(num_vars: usize) -> Vec<Affine> {
524    all_affine_bijections(num_vars).into_iter().filter(|a| is_permutation_matrix(&a.matrix, num_vars)).collect()
525}
526
527/// The elementary transvections `xᵢ ↦ xᵢ ⊕ xⱼ` (`i ≠ j`): the generators of `GL(n,2)` beyond the
528/// permutation matrices. Together with `Bₙ` they generate all of `AGL(n,2)`, so a single transvection is
529/// the smallest affine move outside the signed-permutation lens.
530fn transvections(num_vars: usize) -> Vec<Affine> {
531    let mut out = Vec::new();
532    for i in 0..num_vars {
533        for j in 0..num_vars {
534            if i == j {
535                continue;
536            }
537            let mut matrix: Vec<u64> = (0..num_vars).map(|k| 1u64 << k).collect();
538            matrix[i] |= 1u64 << j;
539            out.push(Affine { n: num_vars, matrix, translation: 0 });
540        }
541    }
542    out
543}
544
545/// **The AGL collapse of the census** — how far the affine group `AGL(n,2) ⊋ Bₙ` merges the `Bₙ`-orbit
546/// count. Because the certified family is AGL-invariant (proved), the AGL census carries the *same* family
547/// tower — just fewer classes: more symmetry, less to cover.
548#[derive(Clone, Debug)]
549pub struct AglCollapse {
550    pub n: usize,
551    /// The `Bₙ`-orbit count (the census's covering-class count).
552    pub bn_orbits: usize,
553    /// The `AGL(n,2)`-class count after the affine lens merges affine-equivalent orbits.
554    pub agl_classes: usize,
555}
556
557impl AglCollapse {
558    /// The collapse factor `bn_orbits / agl_classes` — how many `Bₙ` orbits the affine lens fuses per class.
559    pub fn factor(&self) -> f64 {
560        self.bn_orbits as f64 / self.agl_classes.max(1) as f64
561    }
562}
563
564/// **The exact AGL collapse** (`n ≤ 3`). Canonicalize every `Bₙ`-orbit representative under the full affine
565/// group in point-set space (`affine_canonical_form`); the number of distinct canonical forms is the exact
566/// AGL-class count. Brute over `|AGL(n,2)|` per orbit, so bounded to `n ≤ 3`.
567pub fn agl_collapse_exact(n: usize) -> AglCollapse {
568    let tables: Vec<Vec<u32>> = all_affine_bijections(n).iter().map(|p| perm_table(p, n)).collect();
569    let mut classes: std::collections::BTreeSet<Vec<u64>> = std::collections::BTreeSet::new();
570    let mut bn_orbits = 0usize;
571    for cover in minimal_cover_orbits(n) {
572        bn_orbits += 1;
573        classes.insert(canonical_over_tables(&blocker_masks(&cover.clauses(), n), &tables));
574    }
575    AglCollapse { n, bn_orbits, agl_classes: classes.len() }
576}
577
578/// **The AGL collapse via CNF-preserving affine moves** — scales to `n = 4`. Since `AGL = ⟨Bₙ,
579/// transvections⟩`, uniting the `Bₙ` orbits connected by a single CNF-preserving transvection (its image
580/// stays a valid clause set, then `Bₙ`-canonicalized) merges genuine affine equivalences. The component
581/// count is an *upper bound* on the AGL-class count — every union is a real affine equivalence; only merges
582/// that must route through a non-CNF affine intermediate are missed — hence a *rigorous lower bound* on the
583/// collapse, exact when the walk is tight (checked against [`agl_collapse_exact`] at `n = 3`).
584pub fn agl_collapse_via_transvections(n: usize) -> AglCollapse {
585    let reps: Vec<Vec<u64>> =
586        minimal_cover_orbits(n).into_iter().map(|c| blocker_masks(&c.clauses(), n)).collect();
587    let bn_tables: Vec<Vec<u32>> = bn_affines(n).iter().map(|p| perm_table(p, n)).collect();
588    let tau_tables: Vec<Vec<u32>> = transvections(n).iter().map(|p| perm_table(p, n)).collect();
589    let mut key_to_idx: std::collections::HashMap<Vec<u64>, usize> = std::collections::HashMap::new();
590    for (i, masks) in reps.iter().enumerate() {
591        key_to_idx.insert(canonical_over_tables(masks, &bn_tables), i);
592    }
593    let mut parent: Vec<usize> = (0..reps.len()).collect();
594    fn find(parent: &mut [usize], x: usize) -> usize {
595        let mut root = x;
596        while parent[root] != root {
597            root = parent[root];
598        }
599        let mut cur = x;
600        while parent[cur] != cur {
601            let next = parent[cur];
602            parent[cur] = root;
603            cur = next;
604        }
605        root
606    }
607    for (i, masks) in reps.iter().enumerate() {
608        for tau in &tau_tables {
609            let mut img: Vec<u64> = Vec::with_capacity(masks.len());
610            let mut ok = true;
611            for &s in masks {
612                let t = map_mask(s, tau);
613                if pointset_to_clause(t, n).is_none() {
614                    ok = false;
615                    break;
616                }
617                img.push(t);
618            }
619            if !ok {
620                continue;
621            }
622            img.sort_unstable();
623            img.dedup();
624            if let Some(&j) = key_to_idx.get(&canonical_over_tables(&img, &bn_tables)) {
625                let (ri, rj) = (find(&mut parent, i), find(&mut parent, j));
626                if ri != rj {
627                    parent[ri] = rj;
628                }
629            }
630        }
631    }
632    let agl_classes = (0..reps.len()).filter(|&i| find(&mut parent, i) == i).count();
633    AglCollapse { n, bn_orbits: reps.len(), agl_classes }
634}
635
636/// An **auto-discovered affine equivalence** between two `Bₙ`-orbit representatives: a concrete
637/// `φ ∈ AGL(n,2)` carrying one cover's blocker set onto the other's. The `Bₙ` census reports the two orbits
638/// as distinct classes; this witness proves they are the SAME unsatisfiable problem up to an affine
639/// coordinate change — the structure the signed-permutation lens could not see, made explicit and
640/// re-checkable. Codifying the collapse this way turns a merge *count* into concrete affine symmetries.
641#[derive(Clone, Debug)]
642pub struct AglWitness {
643    pub map: Affine,
644}
645
646impl AglWitness {
647    /// Re-check (zero trust in the producer): pushing `from`'s blocker set through `map` yields exactly
648    /// `to`'s blocker set.
649    pub fn verify(&self, from: &[u64], to: &[u64], num_vars: usize) -> bool {
650        let table = perm_table(&self.map, num_vars);
651        let img: std::collections::BTreeSet<u64> = from.iter().map(|&s| map_mask(s, &table)).collect();
652        img == to.iter().copied().collect()
653    }
654}
655
656/// Search `AGL(n,2)` for an affine map carrying `from`'s blocker set onto `to`'s — the witness that two `Bₙ`
657/// orbits are affine-equivalent, or `None` if they are genuinely affine-distinct. Exhaustive, so `n ≤ 4`.
658pub fn find_agl_witness(from: &[u64], to: &[u64], num_vars: usize) -> Option<AglWitness> {
659    if from.len() != to.len() {
660        return None;
661    }
662    let target: std::collections::BTreeSet<u64> = to.iter().copied().collect();
663    for phi in all_affine_bijections(num_vars) {
664        let table = perm_table(&phi, num_vars);
665        let img: std::collections::BTreeSet<u64> = from.iter().map(|&s| map_mask(s, &table)).collect();
666        if img == target {
667            return Some(AglWitness { map: phi });
668        }
669    }
670    None
671}
672
673/// **Peek inside the generic full-degree cores.** They're `Bₙ`-rigid and full-degree — but are they one
674/// undifferentiated blob, or do finer invariants split them into sub-families? Sub-classify the *distinct*
675/// generic full-degree types by their coarser invariants `(shadow, min-res-width)`: returns
676/// `(shadow, width) → number of distinct full-degree types`. If a few buckets hold most of the types, the
677/// "generic" bucket has real sub-structure (more families); if they're spread thin, it's genuinely varied.
678pub fn generic_subfamilies(n: usize) -> BTreeMap<(String, usize), usize> {
679    let full = format!("nullstellensatz-d{n}");
680    let mut seen: std::collections::BTreeSet<(String, usize, Vec<(usize, usize)>)> = std::collections::BTreeSet::new();
681    let mut sub: BTreeMap<(String, usize), usize> = BTreeMap::new();
682    for cover in minimal_cover_orbits(n) {
683        let clauses = cover.clauses();
684        if rung_label(&weakest_crushing_rung(n, &clauses, n)) != full {
685            continue; // only the generic full-degree cores
686        }
687        let shadow = format!("{:?}", diagnose(n, &clauses).cut);
688        let width = min_resolution_width(&cover).unwrap_or(usize::MAX);
689        let fv: Vec<(usize, usize)> = face_vector(&cover).into_iter().collect();
690        if seen.insert((shadow.clone(), width, fv)) {
691            *sub.entry((shadow, width)).or_insert(0) += 1; // a NEW distinct generic type
692        }
693    }
694    sub
695}
696
697/// Name the family a structural signature belongs to. The named combinatorial families are the *low-degree*
698/// ones (Tseitin=parity, PHP=counting); a `degree-d` algebraic core with `d < n` is a *bounded-algebraic*
699/// family; and a **full-degree** core (`d = n`) is the **generic** type — no low-degree shortcut, the opposite
700/// of a nice family. So the matcher's verdict on a giant is: is it a low-degree named family, a bounded
701/// algebraic one, or the generic full-degree core?
702fn family_label(rung_label: &str, n: usize) -> String {
703    match rung_label {
704        "trivial" => "unit-propagation".into(),
705        "counting" => "PHP / cardinality (counting)".into(),
706        "parity" => "Tseitin / XOR (parity, poly via GF(2))".into(),
707        s if s == format!("nullstellensatz-d{n}") => "GENERIC full-degree core (no low-degree shortcut)".into(),
708        s => format!("bounded-algebraic ({s})"),
709    }
710}
711
712/// **Name the giants.** The top-`k` morph-classes at `n` (by orbit count), each labeled with the family it
713/// matches. Confirms which of the huge morph-classes are recognizable named families vs. the generic
714/// full-degree core.
715pub fn named_giants(n: usize, k: usize) -> Vec<(usize, String)> {
716    let mut sigs: BTreeMap<(String, String, usize, Vec<(usize, usize)>), usize> = BTreeMap::new();
717    for cover in minimal_cover_orbits(n) {
718        let clauses = cover.clauses();
719        let rl = rung_label(&weakest_crushing_rung(n, &clauses, n));
720        let shadow = format!("{:?}", diagnose(n, &clauses).cut);
721        let width = min_resolution_width(&cover).unwrap_or(usize::MAX);
722        let fv: Vec<(usize, usize)> = face_vector(&cover).into_iter().collect();
723        *sigs.entry((rl, shadow, width, fv)).or_insert(0) += 1;
724    }
725    let mut entries: Vec<_> = sigs.into_iter().collect();
726    entries.sort_by(|a, b| b.1.cmp(&a.1));
727    entries.into_iter().take(k).map(|((rl, _, _, _), count)| (count, family_label(&rl, n))).collect()
728}
729
730/// **Label ALL structural types.** Every distinct signature (morph-class) at `n`, labeled with its family;
731/// returns `(num_types, family → type-count)` — so we see how the *types* (not orbits) distribute across the
732/// families. This is the full census of "what are the 403 types and where do they come from": each is a
733/// low-degree named family, a bounded-algebraic one, or the generic full-degree core.
734pub fn family_of_types(n: usize) -> (usize, BTreeMap<String, usize>) {
735    let mut sig_label: BTreeMap<(String, String, usize, Vec<(usize, usize)>), String> = BTreeMap::new();
736    for cover in minimal_cover_orbits(n) {
737        let clauses = cover.clauses();
738        let rl = rung_label(&weakest_crushing_rung(n, &clauses, n));
739        let shadow = format!("{:?}", diagnose(n, &clauses).cut);
740        let width = min_resolution_width(&cover).unwrap_or(usize::MAX);
741        let fv: Vec<(usize, usize)> = face_vector(&cover).into_iter().collect();
742        sig_label.entry((rl.clone(), shadow, width, fv)).or_insert_with(|| family_label(&rl, n));
743    }
744    let mut by_family: BTreeMap<String, usize> = BTreeMap::new();
745    for label in sig_label.values() {
746        *by_family.entry(label.clone()).or_insert(0) += 1;
747    }
748    (sig_label.len(), by_family)
749}
750
751/// The certified **family** an orbit belongs to — the proof-system that refutes it, named. The algebraic
752/// family is degree-graded (`algebraic-d{k}`), so the family tower shows the degree-`n` wall directly.
753fn family_name(rung: &ProofRung) -> String {
754    match rung {
755        ProofRung::Trivial => "unit-propagation".into(),
756        ProofRung::Counting => "counting (pigeonhole/cardinality)".into(),
757        ProofRung::Parity => "parity (XOR/Tseitin)".into(),
758        ProofRung::ModCount { p } => format!("mod-{p} counting (one-hot GF({p}))"),
759        ProofRung::Nullstellensatz { min_degree } => format!("algebraic-d{min_degree}"),
760        ProofRung::BeyondBudget => "unclassified".into(),
761    }
762}
763
764/// **The family growth across `n`** — for each `n ≤ max_n`: the number of distinct certified families, and
765/// which families are *new* (absent at `n−1`). The finding: the cheap families (unit-propagation, parity,
766/// counting) form a *fixed* menu that saturates early; from then on **each increase in `n` forces exactly one
767/// new family — the degree-`n` algebraic family (`algebraic-d{n}`)** — because `max_ns_degree = n`, so a
768/// degree-`n` certificate becomes *necessary* at `n` and was not at `n−1`. That is the forcing mechanism: the
769/// degree wall. The family count is therefore `Θ(n)` (fixed cheap menu + one algebraic rung per `n`), climbing
770/// forever — no finite family set covers all `n` (bounded-degree incompleteness).
771pub fn family_growth(max_n: usize) -> Vec<(usize, usize, Vec<String>)> {
772    let mut prev: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
773    (1..=max_n)
774        .map(|n| {
775            let fam: std::collections::BTreeSet<String> = family_census(n).into_keys().collect();
776            let new: Vec<String> = fam.difference(&prev).cloned().collect();
777            prev.clone_from(&fam);
778            (n, fam.len(), new)
779        })
780        .collect()
781}
782
783/// **The family census across `n`** — classify every minimal-UNSAT orbit into its certified family (lean:
784/// reaches `n=4`). This is the parametric handle on the infinite tower: the families are defined for *all*
785/// `n`, so recognizing membership (poly for the cheap families; `n^d` for the degree-`d` algebraic ones)
786/// covers the structured tower without enumerating it — the only way past the super-exponential orbit count.
787pub fn family_census(n: usize) -> BTreeMap<String, usize> {
788    let mut fam: BTreeMap<String, usize> = BTreeMap::new();
789    for cover in minimal_cover_orbits(n) {
790        *fam.entry(family_name(&weakest_crushing_rung(n, &cover.clauses(), n))).or_insert(0) += 1;
791    }
792    fam
793}
794
795/// **The family tower — proven complete and finite at every `n`, without enumeration.** The constructive
796/// Nullstellensatz completeness of [`crate::polycalc::build_ns_certificate`] (every UNSAT formula over `n`
797/// variables has a degree-`≤ n` GF(2) refutation, so nothing is *beyond budget*) pins the *weakest crushing
798/// rung* of every minimal-UNSAT formula to a FIXED, parametric set: the three cheap families (unit
799/// propagation, counting, parity) and the algebraic degrees `d = 2..=n`. No family lies outside this tower.
800/// So the complete list of families at scale `n` is produced in `O(n)` here — not read off the
801/// super-exponential orbit census. This is the "we do not iterate — we PROVED the families" statement, and
802/// [`family_census`] (which does enumerate) is always a subset of it. The size is `Θ(n)`, climbing forever:
803/// no finite family set covers all `n`, because the degree wall forces a fresh `algebraic-d{n}` at each `n`.
804pub fn family_tower(n: usize) -> Vec<String> {
805    let mut fams = vec![
806        "unit-propagation".to_string(),
807        "counting (pigeonhole/cardinality)".to_string(),
808        "parity (XOR/Tseitin)".to_string(),
809    ];
810    for d in 2..=n {
811        fams.push(format!("algebraic-d{d}"));
812    }
813    fams
814}
815
816/// A parametric UNSAT witness realizing the **unit-propagation** rung at any `n ≥ 1`: a variable and its
817/// negation. Carving alone closes it, so its weakest crushing rung is [`ProofRung::Trivial`].
818pub fn witness_unit_propagation(n: usize) -> (usize, Vec<Vec<Lit>>) {
819    (n.max(1), vec![vec![Lit::pos(0)], vec![Lit::neg(0)]])
820}
821
822/// A parametric UNSAT witness realizing the **parity** rung at any `n ≥ 3`: the odd XOR triangle
823/// `x₀⊕x₁ = 1, x₁⊕x₂ = 1, x₂⊕x₀ = 1` (their sum is `1 ≠ 0`), encoded in CNF. No unit propagates and no
824/// counting cut sees it — only a GF(2) parity refutation closes it, so the weakest rung is
825/// [`ProofRung::Parity`].
826pub fn witness_parity(n: usize) -> (usize, Vec<Vec<Lit>>) {
827    let mut clauses = Vec::new();
828    for (a, b) in [(0u32, 1u32), (1, 2), (2, 0)] {
829        clauses.push(vec![Lit::pos(a), Lit::pos(b)]); // x_a ⊕ x_b = 1
830        clauses.push(vec![Lit::neg(a), Lit::neg(b)]);
831    }
832    (n.max(3), clauses)
833}
834
835/// A parametric UNSAT witness realizing the **counting** rung: the pigeonhole principle with `pigeons`
836/// pigeons and `pigeons − 1` holes ([`crate::families::php`]). Resolution-exponential, invisible to GF(2)
837/// parity, closed only by a counting / Hall cut — so the weakest rung is [`ProofRung::Counting`].
838pub fn witness_counting(pigeons: usize) -> (usize, Vec<Vec<Lit>>) {
839    let (cnf, _) = crate::families::php(pigeons);
840    (cnf.num_vars, cnf.clauses)
841}
842
843/// **The realization frontier at `n`** — the lower-bound companion to [`family_tower`]. For each certified
844/// family that actually OCCURS among the minimal-UNSAT covers over `n` variables, one witness cover's
845/// clauses. Where [`family_tower`] is the proven *upper* envelope (`families ⊆ tower`), this shows *which*
846/// rungs are realized at scale `n`, with a witness. A tower family absent here is simply not yet realized at
847/// this `n` — e.g. counting needs enough variables to host a pigeonhole, so it is empty at small `n`.
848pub fn realized_tower_families(n: usize) -> BTreeMap<String, Vec<Vec<Lit>>> {
849    let mut out: BTreeMap<String, Vec<Vec<Lit>>> = BTreeMap::new();
850    for cover in minimal_cover_orbits(n) {
851        let clauses = cover.clauses();
852        out.entry(family_name(&weakest_crushing_rung(n, &clauses, n))).or_insert(clauses);
853    }
854    out
855}
856
857fn rung_label(rung: &ProofRung) -> String {
858    match rung {
859        ProofRung::Trivial => "trivial".into(),
860        ProofRung::Counting => "counting".into(),
861        ProofRung::Parity => "parity".into(),
862        ProofRung::ModCount { p } => format!("modcount-p{p}"),
863        ProofRung::Nullstellensatz { min_degree } => format!("nullstellensatz-d{min_degree}"),
864        ProofRung::BeyondBudget => "beyond-budget".into(),
865    }
866}
867
868/// The certificate degree of a rung label: trivial/counting = 0, parity = 1, `modcount-p{p}` = 1 (a
869/// linear cut, just over another characteristic), `nullstellensatz-d{k}` = k, `beyond-budget` = `None`
870/// (no bounded certificate — the "structureless at this budget").
871fn degree_of_label(label: &str) -> Option<usize> {
872    match label {
873        "trivial" | "counting" => Some(0),
874        "parity" => Some(1),
875        "beyond-budget" => None,
876        l if l.starts_with("modcount-p") => Some(1),
877        l => l.strip_prefix("nullstellensatz-d").and_then(|d| d.parse().ok()),
878    }
879}
880
881/// **Structure accounting** — the answer to "how many minimal-UNSAT families have *no* structure, and how
882/// does cheap structure run out?" For each `n`: the cumulative number of orbits covered by a certified proof
883/// of degree `≤ d` (`covered_by_degree[d]`), and the `structureless` count = orbits with no bounded
884/// certificate at all (rung = `beyond-budget`). The honest finding at `n ≤ 4`: **`structureless = 0`** — every
885/// minimal-UNSAT cover is algebraically structured (degree `≤ n`). Nothing at finite `n` is truly random; but
886/// the *degree* the structure requires grows with `n` (`= n`), so any *fixed*-degree ("cheap") lens covers a
887/// shrinking fraction. That reconciles "no finite randomness" with the counting bound: structure always
888/// exists, but cheap structure runs out.
889#[derive(Clone, Debug)]
890pub struct StructureAccounting {
891    pub n: usize,
892    pub orbits: usize,
893    /// `covered_by_degree[d]` = orbits refutable by a certified proof of degree `≤ d`, `d = 0..=n`.
894    pub covered_by_degree: Vec<usize>,
895    /// Orbits with no bounded certificate (rung = `beyond-budget`) — the truly-structureless count.
896    pub structureless: usize,
897}
898
899/// Compute [`StructureAccounting`] for `n` (lean: certified rung per orbit, reaches `n=4`).
900pub fn structure_accounting(n: usize) -> StructureAccounting {
901    let mut by_rung: BTreeMap<String, usize> = BTreeMap::new();
902    let mut orbits = 0usize;
903    for cover in minimal_cover_orbits(n) {
904        orbits += 1;
905        *by_rung.entry(rung_label(&weakest_crushing_rung(n, &cover.clauses(), n))).or_insert(0) += 1;
906    }
907    let mut covered_by_degree = vec![0usize; n + 1];
908    let mut structureless = 0usize;
909    for (label, &count) in &by_rung {
910        match degree_of_label(label) {
911            Some(d) => {
912                for slot in covered_by_degree.iter_mut().skip(d.min(n)) {
913                    *slot += count;
914                }
915            }
916            None => structureless += count,
917        }
918    }
919    StructureAccounting { n, orbits, covered_by_degree, structureless }
920}
921
922/// **The lens-menu split + morph clustering.** A *lean* pass (per-orbit certified invariants only — no full
923/// router, no group closure — so it reaches `n=4`): which certified lens-class covers each orbit, and how
924/// many *distinct structural signatures* the orbits collapse to. A signature is `(rung, shadow, min-res-width,
925/// face-vector)` — the structural fingerprint. When `distinct_signatures ≪ orbits`, the vast orbit count is
926/// mostly **morphs of a few structural types** (the same family, mutated), exactly as one expects: symmetry
927/// already quotiented the exact symmetries, and the residual multiplicity is near-symmetric variation.
928#[derive(Clone, Debug)]
929pub struct MenuSplit {
930    pub n: usize,
931    pub orbits: usize,
932    /// Orbit count per certified proof-rung — the fixed lens menu (trivial/counting/parity) vs the growing
933    /// `nullstellensatz-d{degree}` dial.
934    pub by_rung: BTreeMap<String, usize>,
935    /// Number of distinct `(rung, shadow, min-res-width, face-vector)` signatures — the structural-type count.
936    pub distinct_signatures: usize,
937    /// Orbits in the single most common signature — the largest morph-class.
938    pub largest_morph_class: usize,
939}
940
941/// Compute the [`MenuSplit`] for `n` (lean: reaches `n=4`).
942pub fn menu_split(n: usize) -> MenuSplit {
943    let mut by_rung: BTreeMap<String, usize> = BTreeMap::new();
944    let mut sigs: BTreeMap<(String, String, usize, Vec<(usize, usize)>), usize> = BTreeMap::new();
945    let mut orbits = 0usize;
946    for cover in minimal_cover_orbits(n) {
947        orbits += 1;
948        let clauses = cover.clauses();
949        let label = rung_label(&weakest_crushing_rung(n, &clauses, n));
950        *by_rung.entry(label.clone()).or_insert(0) += 1;
951        let shadow = format!("{:?}", diagnose(n, &clauses).cut);
952        let width = min_resolution_width(&cover).unwrap_or(usize::MAX);
953        let fv: Vec<(usize, usize)> = face_vector(&cover).into_iter().collect();
954        *sigs.entry((label, shadow, width, fv)).or_insert(0) += 1;
955    }
956    MenuSplit {
957        n,
958        orbits,
959        by_rung,
960        distinct_signatures: sigs.len(),
961        largest_morph_class: sigs.values().copied().max().unwrap_or(0),
962    }
963}
964
965/// **The degree-growth wall, measured.** The deepest minimum-Nullstellensatz degree among the minimal-UNSAT
966/// families at `n` whose *weakest* certified refutation is algebraic (families caught by a cheaper lens —
967/// counting, parity — contribute nothing, correctly). This is a *lean* pass: only the certified
968/// proof-complexity ladder ([`weakest_crushing_rung`]) per orbit, none of the full router's symmetry/algebraic
969/// machinery — so it reaches `n=4` where the full [`census`] times out. As `n` grows this climbs (bounded-degree
970/// Nullstellensatz is incomplete — Tseitin/expander families force degree `Ω(n)`), so any *fixed*-degree lens
971/// is eventually outrun. That climb is the honest wall, not a fixed residue.
972pub fn max_ns_degree_at(n: usize) -> usize {
973    minimal_cover_orbits(n)
974        .into_iter()
975        .filter_map(|cover| match weakest_crushing_rung(n, &cover.clauses(), n) {
976            ProofRung::Nullstellensatz { min_degree } => Some(min_degree),
977            _ => None,
978        })
979        .max()
980        .unwrap_or(0)
981}
982
983/// Compute the [`ResidueMap`]: rank every lens over every family and partition by which resists.
984pub fn residue_map(n: usize) -> ResidueMap {
985    let records = census(n);
986    let mut m = ResidueMap {
987        n,
988        total: records.len(),
989        crushed: 0,
990        residue: 0,
991        targetable: 0,
992        rigid_core: 0,
993        core_max_ns_degree: 0,
994    };
995    for r in &records {
996        let fell_through = matches!(r.route, Route::Incompressible | Route::Cdcl);
997        if fell_through {
998            m.residue += 1;
999            if r.stabilizer_order == 1 {
1000                m.rigid_core += 1;
1001            }
1002            if let ProofRung::Nullstellensatz { min_degree } = r.rung {
1003                m.core_max_ns_degree = m.core_max_ns_degree.max(min_degree);
1004            }
1005        } else {
1006            m.crushed += 1;
1007        }
1008        if r.symmetry_underbroken() {
1009            m.targetable += 1;
1010        }
1011    }
1012    m
1013}
1014
1015/// The `Bₙ`-orbit census of all minimal UNSAT formulas over `n` variables.
1016pub fn census(n: usize) -> Vec<OrbitRecord> {
1017    let gens = hyperoctahedral_generators(n);
1018    let group = cube_group_closure(&gens, n);
1019    let group_order = group.len();
1020    minimal_cover_orbits(n)
1021        .into_iter()
1022        .map(|cover| {
1023            let clauses = cover.clauses();
1024            let (_, orbit_size) = canonical_cover(&cover, &gens);
1025            // The cover's full Bₙ stabilizer — every geometric automorphism — and the rule-orbit count
1026            // it induces: the strongest symmetry break this formula admits.
1027            let stabilizer: Vec<_> =
1028                group.iter().filter(|g| g.is_automorphism(&cover)).cloned().collect();
1029            let full_rule_orbits =
1030                cover.blocker_orbits(&stabilizer).map(|o| o.len()).unwrap_or(clauses.len());
1031            let rung = weakest_crushing_rung(n, &clauses, n);
1032            let shadow = diagnose(n, &clauses).cut;
1033            let solved = solve_structured(n, &clauses);
1034            let affine_explained =
1035                cover.to_expr().map(|e| crate::xorsat::refute_via_parity(&e)).unwrap_or(false);
1036            OrbitRecord {
1037                n,
1038                num_clauses: clauses.len(),
1039                orbit_size,
1040                stabilizer_order: group_order / orbit_size,
1041                face_vector: face_vector(&cover),
1042                min_res_width: min_resolution_width(&cover).unwrap_or(usize::MAX),
1043                rung,
1044                shadow,
1045                route: solved.via,
1046                discovered_rule_orbits: cover.discovered_rule_symmetry().rule_orbits,
1047                full_rule_orbits,
1048                affine_explained,
1049                modp_routed: matches!(solved.via, Route::ModP)
1050                    && matches!(solved.answer, Answer::Unsat),
1051                rep: cover,
1052            }
1053        })
1054        .collect()
1055}
1056
1057#[cfg(test)]
1058mod tests {
1059    use super::*;
1060
1061    #[test]
1062    fn residue_map_partitions_the_census_and_locates_the_wall() {
1063        for n in 2..=3 {
1064            let m = residue_map(n);
1065            // The partition is exhaustive: every minimal-UNSAT family is either crushed by a lens or residue.
1066            assert_eq!(m.crushed + m.residue, m.total, "crushed ⊎ residue = all orbits at n={n}");
1067            assert!(m.crushed > 0, "structural specialists crush families at n={n}");
1068            // The rigid core is a subset of the residue (residue that also has no symmetry to break).
1069            assert!(m.rigid_core <= m.residue, "rigid core ⊆ residue");
1070            // The measurement — the honest map. (Reported; the numbers are the finding, not an assertion.)
1071            eprintln!(
1072                "n={n}: total={} crushed={} residue={} targetable(symmetry-left)={} rigid_core={} core_ns_deg={}",
1073                m.total, m.crushed, m.residue, m.targetable, m.rigid_core, m.core_max_ns_degree
1074            );
1075        }
1076    }
1077
1078    /// **The characteristic rung closes the router-beats-ladder audit gap, conservatively.** For every
1079    /// census orbit at `n ≤ 3`, the extended ladder ([`extended_rung`], primes 3/5/7) differs from the
1080    /// legacy placement ONLY by verified `ModCount` placements: whenever it reports `ModCount { p }`,
1081    /// re-running the one-hot recovery and the `GF(p)` Gaussian independently confirms a re-checkable
1082    /// refutation at that exact modulus; every other orbit gets the identical rung. And the gap
1083    /// direction: every orbit the router crushed with its mod-`p` specialist while the ladder had no
1084    /// narrow rung (`modp_routed && router_beats_ladder`) is caught by `ModCount`. The gap population is
1085    /// counted and reported honestly — at these small `n` it may be empty (one-hot mod-`p` germs are
1086    /// wide), which is exactly why the synthetic mod-3 Tseitin instance in the hypercube test pins the
1087    /// non-vacuous case; here the claim is census-wide conservativity plus gap coverage.
1088    #[test]
1089    fn the_characteristic_rung_closes_the_router_ladder_audit_gap() {
1090        let primes = [3u64, 5, 7];
1091        let (mut gap_closed, mut modcount_seen) = (0usize, 0usize);
1092        for n in 1..=3usize {
1093            for rec in census(n) {
1094                let ext = extended_rung(&rec, &primes);
1095                match ext {
1096                    ProofRung::ModCount { p } => {
1097                        modcount_seen += 1;
1098                        let clauses = rec.rep.clauses();
1099                        let r = crate::modp::recover_from_cnf(n, &clauses)
1100                            .expect("a ModCount placement implies a recognized one-hot encoding");
1101                        assert_eq!(r.modulus, p, "the rung names the recovered modulus");
1102                        match crate::modp::solve(&r.equations, r.num_vars, p) {
1103                            crate::modp::ModpOutcome::Unsat(combo) => assert!(
1104                                crate::modp::is_refutation(&r.equations, r.num_vars, p, &combo),
1105                                "n={n}: the ModCount placement re-checks independently"
1106                            ),
1107                            crate::modp::ModpOutcome::Sat(_) => {
1108                                panic!("n={n}: a ModCount placement must be refutable")
1109                            }
1110                        }
1111                    }
1112                    _ => assert_eq!(
1113                        ext, rec.rung,
1114                        "n={n}: off the ModCount population the ladders place identically"
1115                    ),
1116                }
1117                if rec.modp_routed && rec.router_beats_ladder() {
1118                    assert!(
1119                        matches!(ext, ProofRung::ModCount { .. }),
1120                        "n={n}: the audit-gap orbit lands on the characteristic rung"
1121                    );
1122                    gap_closed += 1;
1123                }
1124            }
1125        }
1126        eprintln!(
1127            "census n ≤ 3: {gap_closed} audit-gap orbits closed, {modcount_seen} ModCount placements"
1128        );
1129    }
1130
1131    #[test]
1132    fn degree_growth_curve_and_covering_class_counts() {
1133        // Two curves vs n, both lean (orbit enumeration + certified rung, no full router):
1134        //   (1) covering-class count = number of Bₙ-orbits of minimal-UNSAT covers — the "covering ring"
1135        //       size at n: how many ESSENTIALLY-DISTINCT covering classes exist once symmetry collapses the
1136        //       raw 2ⁿ·n! covers to one representative per orbit (this is exactly "try a TON less").
1137        //   (2) max_ns_degree = the algebraic depth the residue forces — the degree-growth wall.
1138        for n in 1..=3 {
1139            let classes = crate::hypercube::minimal_cover_orbits(n).len();
1140            eprintln!("n={n}: covering_classes(orbits)={classes} max_ns_degree={}", max_ns_degree_at(n));
1141        }
1142        // The covering ring grows fast (1, 4, 43, … 42263 at n=4) — the count of classes to cover climbs.
1143        let (c2, c3) = (crate::hypercube::minimal_cover_orbits(2).len(), crate::hypercube::minimal_cover_orbits(3).len());
1144        assert!(c3 > c2, "the number of covering classes grows with n: {c2} → {c3}");
1145        // The degree-growth wall: n=3 forces strictly deeper algebra than n=2.
1146        assert!(max_ns_degree_at(3) > max_ns_degree_at(2), "the NS-degree wall climbs n=2 → n=3");
1147    }
1148
1149    #[test]
1150    fn lens_menu_split_and_morph_clustering() {
1151        for n in 2..=3 {
1152            let m = menu_split(n);
1153            eprintln!(
1154                "n={n}: orbits={} distinct_signatures={} largest_morph_class={} by_rung={:?}",
1155                m.orbits, m.distinct_signatures, m.largest_morph_class, m.by_rung
1156            );
1157            assert_eq!(m.by_rung.values().sum::<usize>(), m.orbits, "the lens menu covers every orbit");
1158            assert!(m.distinct_signatures <= m.orbits, "signatures ≤ orbits");
1159        }
1160        // At n=3 the 43 orbits collapse to strictly fewer structural signatures — morphs of fewer types.
1161        assert!(menu_split(3).distinct_signatures < menu_split(3).orbits, "orbits are morphs of fewer types");
1162    }
1163
1164    #[test]
1165    fn clause_agl_detector_probes_the_generic_cores_for_hidden_affine_symmetry() {
1166        let full = "nullstellensatz-d3".to_string();
1167        let (mut parity_checked, mut hidden, mut affine_rigid) = (false, 0usize, 0usize);
1168        for cover in minimal_cover_orbits(3) {
1169            let clauses = cover.clauses();
1170            let label = rung_label(&weakest_crushing_rung(3, &clauses, 3));
1171            let agl = clause_agl_symmetries(3, &clauses);
1172            assert!(agl >= 1, "the identity is always an AGL automorphism");
1173            if label == "parity" {
1174                // Validation: the parity family is highly symmetric ⟹ the detector finds real AGL symmetry.
1175                assert!(agl > 1, "the detector finds the parity family's affine symmetry");
1176                parity_checked = true;
1177            }
1178            if label == full {
1179                if agl > 1 {
1180                    hidden += 1; // Bₙ-rigid but affine-symmetric — a NEW breakable structure
1181                } else {
1182                    affine_rigid += 1; // structure-minimal at the affine level too
1183                }
1184            }
1185        }
1186        assert!(parity_checked, "validated the detector on the symmetric parity family");
1187        eprintln!("n=3 generic cores: hidden_affine_symmetry={hidden} affine_rigid={affine_rigid}");
1188    }
1189
1190    #[test]
1191    #[ignore = "n=4 sampled clause-AGL: AGL(4,2)=322560 per core, minutes"]
1192    fn clause_agl_at_n4_sampled_does_hidden_affine_symmetry_persist() {
1193        let full = "nullstellensatz-d4".to_string();
1194        let (mut hidden, mut rigid, mut sampled) = (0usize, 0usize, 0usize);
1195        for cover in minimal_cover_orbits(4) {
1196            if sampled >= 40 {
1197                break;
1198            }
1199            let clauses = cover.clauses();
1200            if rung_label(&weakest_crushing_rung(4, &clauses, 4)) != full {
1201                continue;
1202            }
1203            sampled += 1;
1204            if clause_agl_symmetries(4, &clauses) > 1 {
1205                hidden += 1;
1206            } else {
1207                rigid += 1;
1208            }
1209        }
1210        eprintln!("n=4 generic cores (sampled {sampled}): hidden_affine={hidden} affine_rigid={rigid}");
1211    }
1212
1213    #[test]
1214    fn peek_inside_the_generic_full_degree_cores() {
1215        let sub = generic_subfamilies(3);
1216        eprintln!("n=3 generic sub-families (shadow, width) → distinct types: {sub:?}");
1217        let generic_types: usize = sub.values().sum();
1218        // The generic full-degree cores are NOT one undifferentiated blob — finer invariants split them.
1219        assert!(generic_types > 0, "there are generic full-degree types at n=3");
1220        assert!(!sub.is_empty(), "they sub-classify by (shadow, width) — sub-structure exists");
1221        // "Zero structure" is impossible: structure_accounting proves structureless = 0 (degree ≤ n always
1222        // refutes). So even the generic cores HAVE structure (a full-degree certificate); "generic" means
1223        // no CHEAPER structure, not none.
1224        assert_eq!(structure_accounting(3).structureless, 0, "no core has literally zero structure");
1225    }
1226
1227    #[test]
1228    fn label_every_structural_type_by_family() {
1229        // n=3: label all 27 types; report the family distribution + name the giants.
1230        let (num_types, by_family) = family_of_types(3);
1231        eprintln!("n=3: {num_types} types → {by_family:?}");
1232        eprintln!("n=3 giants: {:?}", named_giants(3, 5));
1233        assert_eq!(num_types, by_family.values().sum::<usize>(), "every type is labeled by exactly one family");
1234        assert!(num_types > 1, "multiple structural types at n=3");
1235        // The giants are labeled with a concrete family (no unlabeled type).
1236        assert!(named_giants(3, 3).iter().all(|(_, label)| !label.is_empty()), "every giant named");
1237    }
1238
1239    #[test]
1240    #[ignore = "n=4: label all 403 types, ~minutes"]
1241    fn label_all_403_types_at_n4() {
1242        let (num_types, by_family) = family_of_types(4);
1243        eprintln!("n=4: {num_types} types → {by_family:?}");
1244        eprintln!("n=4 giants: {:?}", named_giants(4, 8));
1245    }
1246
1247    #[test]
1248    fn family_growth_is_forced_by_the_degree_wall() {
1249        let growth = family_growth(3);
1250        for (n, count, new) in &growth {
1251            eprintln!("n={n}: family_count={count} new_families={new:?}");
1252        }
1253        // The family count climbs with n (a fixed cheap menu + a new algebraic degree per n).
1254        assert!(growth[2].1 > growth[0].1, "more families at n=3 than n=1");
1255        // THE forcing mechanism: at n=3 the new family opened is the degree-3 algebraic one — the degree wall
1256        // (max_ns_degree = n) necessitates a degree-n certificate that did not exist at n−1.
1257        assert!(growth[2].2.iter().any(|f| f == "algebraic-d3"), "n=3 forces the algebraic-d3 family");
1258    }
1259
1260    #[test]
1261    fn family_census_classifies_every_orbit_across_n() {
1262        for n in 1..=3 {
1263            let fam = family_census(n);
1264            let total: usize = fam.values().sum();
1265            eprintln!("n={n}: {fam:?}");
1266            // Every orbit is classified into a certified family — none "unclassified".
1267            assert!(!fam.contains_key("unclassified"), "no unclassified orbit at n={n}");
1268            assert_eq!(total, crate::hypercube::minimal_cover_orbits(n).len(), "the family census covers all orbits");
1269        }
1270        // Cross-n trend: the algebraic families appear and deepen with n (the degree tower); n=3 carries an
1271        // algebraic-d3 family absent at n=2 — the parametric handle on the tower, one degree per n.
1272        assert!(family_census(3).keys().any(|k| k == "algebraic-d3"), "n=3 opens the algebraic-d3 family");
1273        assert!(!family_census(2).keys().any(|k| k == "algebraic-d3"), "which n=2 does not have");
1274    }
1275
1276    #[test]
1277    fn structure_accounting_no_finite_randomness_but_cheap_structure_runs_out() {
1278        for n in 1..=3 {
1279            let a = structure_accounting(n);
1280            eprintln!(
1281                "n={n}: orbits={} structureless={} covered_by_degree={:?}",
1282                a.orbits, a.structureless, a.covered_by_degree
1283            );
1284            // THE answer to "how many are random": zero — every minimal-UNSAT cover has degree-≤n structure.
1285            assert_eq!(a.structureless, 0, "no truly-structureless family at n={n} (degree ≤ n always suffices)");
1286            // Full coverage is reached by degree n (the last slot covers everything non-structureless).
1287            assert_eq!(*a.covered_by_degree.last().unwrap(), a.orbits, "degree ≤ n covers ALL orbits");
1288            // Cheap structure runs out: coverage is monotone nondecreasing in the degree budget.
1289            assert!(a.covered_by_degree.windows(2).all(|w| w[0] <= w[1]), "coverage grows with the degree dial");
1290        }
1291        // Cheap (bounded) degree covers a SHRINKING fraction as n grows: at n=3, degree ≤ 2 covers < all.
1292        let a3 = structure_accounting(3);
1293        assert!(a3.covered_by_degree[2] < a3.orbits, "at n=3 a degree-2 lens already misses families (need d=3)");
1294    }
1295
1296    #[test]
1297    fn parametric_witnesses_realize_the_cheap_rungs_for_all_n() {
1298        // Each cheap tower rung has an explicit PARAMETRIC witness whose WEAKEST crushing rung is certified to
1299        // be exactly that rung (not a cheaper one) — realization for all n above the rung's threshold.
1300        for n in 1..=6 {
1301            let (nv, cl) = witness_unit_propagation(n);
1302            assert!(matches!(weakest_crushing_rung(nv, &cl, nv), ProofRung::Trivial), "unit-prop realized @ n={n}");
1303        }
1304        for n in 3..=6 {
1305            let (nv, cl) = witness_parity(n);
1306            assert!(matches!(weakest_crushing_rung(nv, &cl, nv), ProofRung::Parity), "parity realized @ n={n}");
1307        }
1308        for pigeons in 3..=4 {
1309            let (nv, cl) = witness_counting(pigeons);
1310            assert!(
1311                matches!(weakest_crushing_rung(nv, &cl, nv), ProofRung::Counting),
1312                "counting realized (PHP {pigeons} pigeons, {nv} vars)"
1313            );
1314        }
1315    }
1316
1317    #[test]
1318    fn the_realization_frontier_certifies_which_tower_rungs_occur() {
1319        // The lower-bound companion to the tower: which rungs are REALIZED among the n-variable covers, with a
1320        // witness. Every realized family is in the tower (containment) and every witness is genuinely UNSAT.
1321        for n in 2..=3 {
1322            let realized = realized_tower_families(n);
1323            let tower: std::collections::BTreeSet<String> = family_tower(n).into_iter().collect();
1324            eprintln!("n={n}: realized families = {:?}", realized.keys().collect::<Vec<_>>());
1325            for (fam, clauses) in &realized {
1326                assert!(tower.contains(fam), "n={n}: realized family {fam} lies in the tower");
1327                assert!(crate::polycalc::build_ns_certificate(n, clauses).is_ok(), "n={n}: witness for {fam} is UNSAT");
1328            }
1329        }
1330        // The algebraic ladder is populated with EXACT minimum degree: d=2 (@n=2) and d=3 (@n=3) each have a
1331        // witness refuted at degree d but NOT at d−1 — the degree wall's rungs, occupied and certified.
1332        for d in 2..=3usize {
1333            let witness = realized_tower_families(d)
1334                .remove(&format!("algebraic-d{d}"))
1335                .unwrap_or_else(|| panic!("algebraic-d{d} must be realized at n={d}"));
1336            assert!(crate::polycalc::nullstellensatz_refutes(d, &witness, d), "algebraic-d{d}: refuted at degree {d}");
1337            assert!(
1338                !crate::polycalc::nullstellensatz_refutes(d, &witness, d - 1),
1339                "algebraic-d{d}: NOT refuted at degree {} — minimum degree is exactly {d}",
1340                d - 1
1341            );
1342        }
1343    }
1344
1345    #[test]
1346    #[ignore = "n=4 census scan for the algebraic-d4 witness, minutes"]
1347    fn algebraic_d4_rung_is_realized_with_exact_minimum_degree() {
1348        let witness = realized_tower_families(4)
1349            .remove("algebraic-d4")
1350            .expect("the algebraic-d4 rung must be realized at n=4");
1351        assert!(crate::polycalc::nullstellensatz_refutes(4, &witness, 4), "algebraic-d4: refuted at degree 4");
1352        assert!(
1353            !crate::polycalc::nullstellensatz_refutes(4, &witness, 3),
1354            "algebraic-d4: NOT refuted at degree 3 — minimum degree is exactly 4"
1355        );
1356    }
1357
1358    #[test]
1359    fn agl_collapse_factor_at_small_n_and_the_transvection_walk_is_tight() {
1360        // The affine lens can only MERGE orbits (never split): AGL-classes ≤ Bₙ-orbits at every n.
1361        for n in 1..=3 {
1362            let ex = agl_collapse_exact(n);
1363            eprintln!(
1364                "n={n}: Bₙ-orbits={} → AGL-classes={} (collapse ×{:.2})",
1365                ex.bn_orbits, ex.agl_classes, ex.factor()
1366            );
1367            assert!(ex.agl_classes <= ex.bn_orbits, "n={n}: AGL can only merge classes, never split");
1368            assert!(ex.agl_classes >= 1, "n={n}: at least one class");
1369        }
1370        // MORE SYMMETRY ⟹ strictly fewer classes at n=3: the affine lens sees equivalences Bₙ cannot.
1371        let e3 = agl_collapse_exact(3);
1372        assert!(e3.agl_classes < e3.bn_orbits, "AGL strictly collapses the n=3 census (43 → fewer)");
1373        // The transvection walk (which SCALES to n=4) is an upper bound on classes — and TIGHT at n=3,
1374        // evidence the CNF-preserving affine moves capture the full affine equivalence (no non-CNF detour
1375        // is needed at this scale).
1376        let w3 = agl_collapse_via_transvections(3);
1377        assert!(w3.agl_classes >= e3.agl_classes, "the walk over-counts classes (a lower bound on collapse)");
1378        assert_eq!(w3.agl_classes, e3.agl_classes, "the transvection walk is TIGHT at n=3 (matches exact AGL)");
1379    }
1380
1381    #[test]
1382    fn agl_merges_are_auto_discovered_and_codified_as_verified_affine_witnesses() {
1383        // Group the n=3 Bₙ orbits into AGL classes, then codify the 43→38 collapse: every SAME-class pair
1384        // must carry a VERIFIED affine witness (the map Bₙ could not see), and every CROSS-class pair must
1385        // have NONE (genuinely affine-distinct). This proves the AGL partition is exactly right AND turns
1386        // each merge into a concrete, re-checkable affine equivalence — the auto-discovered structure.
1387        let reps: Vec<Vec<u64>> =
1388            minimal_cover_orbits(3).into_iter().map(|c| blocker_masks(&c.clauses(), 3)).collect();
1389        let tables: Vec<Vec<u32>> = all_affine_bijections(3).iter().map(|p| perm_table(p, 3)).collect();
1390        let canon: Vec<Vec<u64>> = reps.iter().map(|m| canonical_over_tables(m, &tables)).collect();
1391        let mut merges = 0usize;
1392        for i in 0..reps.len() {
1393            for j in (i + 1)..reps.len() {
1394                let same_class = canon[i] == canon[j];
1395                match find_agl_witness(&reps[i], &reps[j], 3) {
1396                    Some(w) => {
1397                        assert!(same_class, "an affine witness exists only between affine-equivalent orbits");
1398                        assert!(w.verify(&reps[i], &reps[j], 3), "the auto-discovered affine witness must re-check");
1399                        // The witness genuinely uses affine structure: a shear is invisible to Bₙ.
1400                        merges += 1;
1401                    }
1402                    None => assert!(!same_class, "affine-distinct orbits must have NO affine witness"),
1403                }
1404            }
1405        }
1406        assert!(merges > 0, "the 43→38 collapse yields real affine equivalences to codify");
1407    }
1408
1409    #[test]
1410    #[ignore = "n=4 AGL collapse: 42263 orbits × 12 transvections × Bₙ-canon(384), minutes"]
1411    fn agl_collapse_factor_at_n4() {
1412        let w = agl_collapse_via_transvections(4);
1413        eprintln!(
1414            "n=4: Bₙ-orbits={} → AGL-classes={} (collapse ×{:.2})",
1415            w.bn_orbits, w.agl_classes, w.factor()
1416        );
1417    }
1418
1419    #[test]
1420    fn transvection_symbolic_image_matches_the_pointset_computation() {
1421        // The O(clause) symbolic transvection rule must agree with the exhaustive 2ⁿ point-set computation, on
1422        // hundreds of random clauses — the correctness anchor for the scalable finder.
1423        let mut s = 0x9E37_79B9_7F4A_7C15u64;
1424        let mut rng = || {
1425            s ^= s << 13;
1426            s ^= s >> 7;
1427            s ^= s << 17;
1428            s
1429        };
1430        let key = |o: &Option<Vec<Lit>>| {
1431            o.as_ref().map(|cl| {
1432                let mut k: Vec<(u32, bool)> = cl.iter().map(|l| (l.var(), l.is_positive())).collect();
1433                k.sort_unstable();
1434                k
1435            })
1436        };
1437        for _ in 0..500 {
1438            let n = 3 + (rng() % 4) as usize; // 3..=6
1439            let width = 1 + (rng() % n as u64) as usize;
1440            let mut seen = std::collections::HashSet::new();
1441            let mut c: Vec<Lit> = Vec::new();
1442            while c.len() < width {
1443                let v = (rng() % n as u64) as u32;
1444                if seen.insert(v) {
1445                    c.push(Lit::new(v, rng() & 1 == 0));
1446                }
1447            }
1448            let a = (rng() % n as u64) as u32;
1449            let mut b = (rng() % n as u64) as u32;
1450            while b == a {
1451                b = (rng() % n as u64) as u32;
1452            }
1453            let sym = transvection_image_clause(&c, a, b);
1454            let tau = Affine {
1455                n,
1456                matrix: {
1457                    let mut m: Vec<u64> = (0..n).map(|k| 1u64 << k).collect();
1458                    m[a as usize] |= 1u64 << b;
1459                    m
1460                },
1461                translation: 0,
1462            };
1463            let point_based = pointset_to_clause(map_point_set(falsify_set(&c, n), &tau, n), n);
1464            assert_eq!(key(&sym), key(&point_based), "symbolic transvection image ≠ point-based for {c:?}, σ({a}↦{a}⊕{b})");
1465        }
1466    }
1467
1468    #[test]
1469    fn the_affine_transvection_finder_scales_to_large_n() {
1470        // Polynomial, no 2ⁿ: run at n = 60 — impossible for any AGL enumeration. A family where x0 and x1
1471        // always co-occur positively is invariant under σ: x0 ↦ x0 ⊕ x1 (both in support, x1 positive ⟹ no
1472        // flip); the finder recovers it in O(n²·|clauses|) time.
1473        let n = 60usize;
1474        let clauses: Vec<Vec<Lit>> =
1475            (2..n as u32).map(|v| vec![Lit::pos(0), Lit::pos(1), Lit::pos(v)]).collect();
1476        let gens = affine_transvection_generators(n, &clauses);
1477        assert!(gens.contains(&(0, 1)), "the finder recovers σ(0↦0⊕1) at n={n}");
1478        assert!(gens.contains(&(1, 0)), "and its partner σ(1↦1⊕0)");
1479        // Sanity: a shear mixing a variable that does not co-occur is correctly rejected.
1480        assert!(!gens.contains(&(0, 2)), "σ(0↦0⊕2) is not a symmetry (x2 is absent from most clauses)");
1481    }
1482
1483    #[test]
1484    fn rank1_symbolic_image_matches_the_pointset_computation() {
1485        // The O(clause) rank-1 rule M_{u,v}: x ↦ x ⊕ (v·x)u must agree with the exhaustive 2ⁿ point-set image,
1486        // on hundreds of random bijective (u·v = 0) instances — the correctness anchor for the symplectic finder.
1487        let mut s = 0xD1B5_4A32_D192_ED03u64;
1488        let mut rng = || {
1489            s ^= s << 13;
1490            s ^= s >> 7;
1491            s ^= s << 17;
1492            s
1493        };
1494        let key = |o: &Option<Vec<Lit>>| {
1495            o.as_ref().map(|cl| {
1496                let mut k: Vec<(u32, bool)> = cl.iter().map(|l| (l.var(), l.is_positive())).collect();
1497                k.sort_unstable();
1498                k
1499            })
1500        };
1501        let mut bijective_seen = 0;
1502        for _ in 0..800 {
1503            let n = 3 + (rng() % 4) as usize; // 3..=6
1504            let mask = (1u64 << n) - 1;
1505            let u = rng() & mask;
1506            let v = rng() & mask;
1507            if (u & v).count_ones() % 2 != 0 {
1508                continue; // require u·v = 0 (M bijective)
1509            }
1510            bijective_seen += 1;
1511            // a random nonempty clause
1512            let width = 1 + (rng() % n as u64) as usize;
1513            let mut seen = std::collections::HashSet::new();
1514            let mut c: Vec<Lit> = Vec::new();
1515            while c.len() < width {
1516                let var = (rng() % n as u64) as u32;
1517                if seen.insert(var) {
1518                    c.push(Lit::new(var, rng() & 1 == 0));
1519                }
1520            }
1521            let sym = rank1_image_clause(&c, u, v);
1522            // point-based oracle: matrix row a = e_a ⊕ (u_a ? v : 0), since (Mx)_a = x_a ⊕ u_a(v·x).
1523            let matrix: Vec<u64> =
1524                (0..n).map(|a| (1u64 << a) ^ (if (u >> a) & 1 == 1 { v } else { 0 })).collect();
1525            let tau = Affine { n, matrix, translation: 0 };
1526            let point_based = pointset_to_clause(map_point_set(falsify_set(&c, n), &tau, n), n);
1527            assert_eq!(key(&sym), key(&point_based), "rank-1 image ≠ point-based for {c:?}, u={u:#b}, v={v:#b}");
1528        }
1529        assert!(bijective_seen > 200, "exercised enough bijective (u·v=0) cases: {bijective_seen}");
1530    }
1531
1532    #[test]
1533    fn the_parity_wall_is_a_depth_not_a_wall_composite_finder_catches_it() {
1534        // The full parity constraint x0⊕x1⊕x2⊕x3 = 0, as CNF: one width-4 clause forbidding each odd-parity
1535        // point. NO single transvection preserves it (each adds x_j to the parity form once, flipping it), but
1536        // the depth-2 shear "add x_j to two coordinates" preserves it (adds x_j twice = 0). So the "wall" I
1537        // named was a depth-1 horizon, not a barrier — the composite finder climbs straight over it.
1538        let n = 4usize;
1539        let clauses: Vec<Vec<Lit>> = (0u32..(1 << n))
1540            .filter(|p| p.count_ones() % 2 == 1)
1541            .map(|p| {
1542                (0..n as u32)
1543                    .map(|i| if (p >> i) & 1 == 1 { Lit::neg(i) } else { Lit::pos(i) })
1544                    .collect()
1545            })
1546            .collect();
1547        assert_eq!(clauses.len(), 8, "half of the 16 points (the odd-parity ones) are forbidden");
1548
1549        // Depth 1: blind. No single transvection is a symmetry of parity.
1550        let depth1 = affine_transvection_generators(n, &clauses);
1551        assert!(depth1.is_empty(), "single transvections cannot preserve parity — the depth-1 horizon");
1552
1553        // Depth 2: the parity shears appear. Each is genuinely verified to permute the clause set.
1554        let depth2 = affine_composite_shear_generators(n, &clauses, 2);
1555        assert!(!depth2.is_empty(), "depth-2 composite shears preserve parity — the wall was a horizon");
1556        assert!(
1557            depth2.iter().all(|(s, _)| s.len() == 2),
1558            "every parity generator is genuinely composite (needs two targets, never one)"
1559        );
1560        // Concretely: "add x2 to x0 and x1" is one such symmetry.
1561        assert!(
1562            depth2.iter().any(|(s, j)| *j == 2 && s.contains(&0) && s.contains(&1)),
1563            "the shear (add x2 to {{x0,x1}}) is found"
1564        );
1565        eprintln!("parity(n={n}): depth-1 finds {} shears, depth-2 finds {}", depth1.len(), depth2.len());
1566    }
1567
1568    #[test]
1569    fn symplectic_transvection_weight_cannot_grade_ns_degree() {
1570        // Chasing a graded tracking of symmetry weight against NS degree — and finding, honestly, that the
1571        // naive axis cannot work. Going one rung above affine shears to the full rank-1 involutions (symplectic
1572        // transvections T_w = I⊕wwᵀ) does NOT expose growing-weight symmetries: PHP, whose NS degree grows, is
1573        // rigid even here. And a single parity block's minimal symplectic weight is a constant 2 — because the
1574        // classical groups (Sₙ, Sp, O) are all generated by weight-2 transvections, so the *minimal* weight is
1575        // structurally pinned at 2 (or absent) and can never grade. The graded invariant that DOES track NS
1576        // degree is not a symmetry-group weight but the certificate depth (polycalc's bridge theorem).
1577        use crate::polycalc::nullstellensatz_refutes;
1578
1579        // PHP: NS degree grows (4 → 6) yet no symplectic transvection exists up to the variable count — its
1580        // hardness is protected by *permutation* symmetry, expressible as no single low-rank linear involution.
1581        for (m, deg) in [(3usize, 4usize), (4, 6)] {
1582            let (php, _) = crate::families::php(m);
1583            let nv = php.num_vars;
1584            let gens = symplectic_transvection_generators(nv, &php.clauses, nv.min(6));
1585            assert!(gens.is_empty(), "PHP({m}) is symplectic-rigid — no rank-1 linear involution symmetry");
1586            assert!(nullstellensatz_refutes(nv, &php.clauses, deg), "PHP({m}) NS degree reaches {deg}");
1587            assert!(!nullstellensatz_refutes(nv, &php.clauses, deg - 1), "…and is exactly {deg}, growing with m");
1588        }
1589
1590        // Parity blocks: a symplectic symmetry exists but its minimal weight is a constant 2 for every width —
1591        // the weight-2-generation of the symmetry group, made visible.
1592        for w in [4usize, 6] {
1593            let vars: Vec<u32> = (0..w as u32).collect();
1594            let clauses: Vec<Vec<Lit>> = (0u32..(1 << w))
1595                .filter(|p| p.count_ones() % 2 == 1)
1596                .map(|p| (0..w).map(|i| Lit::new(vars[i], (p >> i) & 1 == 0)).collect())
1597                .collect();
1598            let min_weight = symplectic_transvection_generators(w, &clauses, w)
1599                .iter()
1600                .map(|g| g.count_ones() as usize)
1601                .min();
1602            assert_eq!(min_weight, Some(2), "parity_block({w}): minimal symplectic weight is a constant 2");
1603        }
1604    }
1605
1606    #[test]
1607    fn family_is_agl_invariant_so_the_affine_lens_preserves_structure() {
1608        // THEOREM: an affine coordinate change x ↦ Ax⊕b is a degree-PRESERVING automorphism of the multilinear
1609        // GF(2) ring, so it carries a degree-d Nullstellensatz certificate to a degree-d one — the minimum NS
1610        // degree, hence the certified FAMILY, is AGL-invariant. Validated exhaustively at n=3: for every census
1611        // orbit and every affine map that keeps the cover a CNF, the image has the SAME weakest rung and family.
1612        // This is why AGL (⊋ Bₙ — "more symmetry") is a structure-PRESERVING census lens: quotienting by it
1613        // loses no family information, it only MERGES orbits — exactly the coarser lens that collapses toward n=5.
1614        let mut checked = 0usize;
1615        let mut beyond_bn = 0usize; // images produced by a genuine shear (linear part not a permutation matrix)
1616        for cover in minimal_cover_orbits(3) {
1617            let f = cover.clauses();
1618            let fam_f = family_name(&weakest_crushing_rung(3, &f, 3));
1619            for phi in all_affine_bijections(3) {
1620                if let Some(g) = agl_image_formula(&f, &phi, 3) {
1621                    let fam_g = family_name(&weakest_crushing_rung(3, &g, 3));
1622                    assert_eq!(fam_f, fam_g, "the family must be invariant under an affine coordinate change");
1623                    checked += 1;
1624                    // A row with ≥2 set bits is a shear — a map no signed permutation (Bₙ) can express.
1625                    if phi.matrix.iter().any(|r| r.count_ones() >= 2) {
1626                        beyond_bn += 1;
1627                    }
1628                }
1629            }
1630        }
1631        assert!(checked > 43, "invariance exercised across every orbit and its affine images");
1632        // The lens genuinely reaches past Bₙ: some structure-preserving images come from true shears.
1633        assert!(beyond_bn > 0, "AGL reaches valid CNF images via shears the permutation lens cannot");
1634    }
1635
1636    #[test]
1637    fn the_family_tower_is_provably_complete_and_finite_without_enumeration() {
1638        // The tower is produced in O(n) parametrically — no orbit enumeration at all.
1639        for n in 1..=6 {
1640            let tower = family_tower(n);
1641            // Finite, size Θ(n): the 3 cheap families + the algebraic degrees 2..=n.
1642            assert_eq!(tower.len(), 3 + n.saturating_sub(1), "n={n}: tower = 3 cheap families + algebraic d=2..=n");
1643        }
1644        // SOUNDNESS of the parametric tower against the ACTUAL census (n=2,3, cheap): every orbit's weakest
1645        // family lies in the tower, none is beyond-budget (the completeness the constructive certificate
1646        // guarantees), and that membership is WITNESSED per orbit by a re-checking certificate.
1647        for n in 2..=3 {
1648            let tower: std::collections::BTreeSet<String> = family_tower(n).into_iter().collect();
1649            for cover in minimal_cover_orbits(n) {
1650                let clauses = cover.clauses();
1651                let rung = weakest_crushing_rung(n, &clauses, n);
1652                assert!(!matches!(rung, ProofRung::BeyondBudget), "n={n}: completeness ⟹ no beyond-budget family");
1653                assert!(tower.contains(&family_name(&rung)), "n={n}: every orbit's family lies in the parametric tower");
1654                let cert = crate::polycalc::build_ns_certificate(n, &clauses).expect("UNSAT ⟹ a certificate");
1655                assert!(cert.verify(&clauses), "the constructive certificate witnesses the orbit's tower membership");
1656            }
1657        }
1658        // The enumerated family set is always a SUBSET of the parametric tower — the tower is the complete
1659        // envelope, computed without iterating the orbits.
1660        for n in 1..=3 {
1661            let tower: std::collections::BTreeSet<String> = family_tower(n).into_iter().collect();
1662            assert!(family_census(n).keys().all(|k| tower.contains(k)), "n={n}: enumerated families ⊆ the tower");
1663        }
1664    }
1665
1666    #[test]
1667    fn the_coverage_verdict_no_random_family_theta_n_families_but_no_fixed_cost_lens() {
1668        // The honest answer to "is there a random family / how many families / can we cover them all", proven:
1669        //
1670        // (Q2) Is there ONE genuinely-random family with no checkable structure? NO — `structureless = 0` at
1671        // every n: every UNSAT formula has a degree-≤n GF(2) certificate. Randomness needs unbounded
1672        // complexity; a finite cube caps it at n. Nothing is structureless.
1673        for n in 1..=3 {
1674            assert_eq!(structure_accounting(n).structureless, 0, "no genuinely-random family at n={n}");
1675        }
1676        // (Q3a) Are there ~2ⁿ families we can't cover? NO at the FAMILY level: the family count is Θ(n)
1677        // (3 cheap families + one algebraic degree per n), so the families are coverable PARAMETRICALLY — the
1678        // super-exponential blow-up is in INSTANCES/orbits (1,4,43,42263,…), which collapse to Θ(n) families.
1679        for n in 1..=6 {
1680            assert_eq!(family_tower(n).len(), 3 + n.saturating_sub(1), "n={n}: Θ(n) families, not 2ⁿ");
1681        }
1682        // (Q3b) BUT no FIXED-cost (bounded-degree) lens is complete: the minimum certificate degree climbs
1683        // without bound (max_ns_degree = n), so for each degree budget d there is a family — at n = d+1 — whose
1684        // weakest certificate needs degree d+1 > d, escaping every degree-d lens. THIS is the real wall: not an
1685        // unbounded number of families, and not randomness, but unbounded certificate COST.
1686        for d in 1..=2usize {
1687            let n = d + 1;
1688            let witness = realized_tower_families(n)
1689                .remove(&format!("algebraic-d{n}"))
1690                .unwrap_or_else(|| panic!("a full-degree family exists at n={n}"));
1691            assert!(
1692                !crate::polycalc::nullstellensatz_refutes(n, &witness, d),
1693                "a degree-{d} lens MISSES a family at n={n} — no fixed-cost lens is complete"
1694            );
1695        }
1696        // (Q1) How fast to CHECK once the degree is known? The certificate basis is Σ_{k≤d} C(n,k) = O(nᵈ) —
1697        // polynomial for FIXED d, only 2ⁿ at the full degree d = n. So structure, once its degree is known, is
1698        // cheap to check; the 2ⁿ is the worst-case for the generic full-degree core (where the hardness lives).
1699        assert!(crate::polycalc::nullstellensatz_basis_size(10, 2) < 10u128.pow(3), "fixed-degree check is polynomial");
1700        assert_eq!(crate::polycalc::nullstellensatz_basis_size(10, 10), 1u128 << 10, "full degree is 2ⁿ");
1701    }
1702
1703    #[test]
1704    fn every_census_cover_carries_a_constructive_certificate_that_generalizes_past_the_wall() {
1705        // `structure_accounting` only *counts* `structureless = 0` at `n ≤ 4`. This EXHIBITS the witness:
1706        // every minimal-UNSAT orbit at n=3 has a constructive degree-≤n Nullstellensatz certificate that
1707        // re-checks against its own clauses — structureless = 0 proven *per orbit*, not merely tallied.
1708        for cover in minimal_cover_orbits(3) {
1709            let clauses = cover.clauses();
1710            let cert = crate::polycalc::build_ns_certificate(3, &clauses)
1711                .expect("a minimal-UNSAT cover is unsatisfiable, so the construction yields a certificate");
1712            assert!(cert.verify(&clauses), "the certificate re-checks against the cover's own clauses");
1713            assert!(cert.degree() <= 3, "the certificate degree is ≤ n");
1714        }
1715        // The SAME uniform construction reaches n=5 — where `minimal_cover_orbits` is infeasible (~10⁷
1716        // orbits) — on a constructed n=5 UNSAT cover (all 2⁵ corners forbidden by full-width clauses). So
1717        // `structureless = 0` is settled at n=5 by *proof*, not enumeration: we do not iterate, we construct.
1718        let n = 5;
1719        let full: Vec<Vec<Lit>> = (0u64..(1u64 << n))
1720            .map(|a| (0..n as u32).map(|v| Lit::new(v, (a >> v) & 1 == 0)).collect())
1721            .collect();
1722        let cert = crate::polycalc::build_ns_certificate(n, &full).expect("the all-corners-forbidden cover is UNSAT");
1723        assert!(cert.verify(&full), "the n=5 certificate re-checks — structureless=0 proven past the census wall");
1724        assert!(cert.degree() <= n, "the n=5 certificate degree is ≤ n");
1725    }
1726
1727    #[test]
1728    #[ignore = "n=4 lean menu split: 42263 orbits × (rung+shadow+width+face-vector), minutes"]
1729    fn lens_menu_split_at_n4() {
1730        let m = menu_split(4);
1731        eprintln!(
1732            "n=4: orbits={} distinct_signatures={} largest_morph_class={} by_rung={:?}",
1733            m.orbits, m.distinct_signatures, m.largest_morph_class, m.by_rung
1734        );
1735    }
1736
1737    #[test]
1738    #[ignore = "n=4 lean degree pass: 42263-orbit enumeration + certified rungs, minutes"]
1739    fn degree_growth_at_n4() {
1740        eprintln!(
1741            "n=4: covering_classes={} max_ns_degree={}",
1742            crate::hypercube::minimal_cover_orbits(4).len(),
1743            max_ns_degree_at(4)
1744        );
1745    }
1746
1747    #[test]
1748    #[ignore = "census-scale: 42263 orbits, minutes"]
1749    fn residue_map_at_census_scale_n4() {
1750        let m = residue_map(4);
1751        assert_eq!(m.crushed + m.residue, m.total, "the partition is exhaustive at census scale");
1752        eprintln!(
1753            "n=4: total={} crushed={} residue={} targetable(symmetry-left)={} rigid_core={} core_ns_deg={}",
1754            m.total, m.crushed, m.residue, m.targetable, m.rigid_core, m.core_max_ns_degree
1755        );
1756    }
1757}