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}