logicaffeine_proof/polycalc.rs
1//! Polynomial Calculus / Nullstellensatz over `GF(2)` — the algebraic proof system that **subsumes the
2//! linear cuts** (parity is its degree-1 fragment) and, at higher degree, refutes strictly more.
3//!
4//! A CNF is unsatisfiable iff the constant polynomial `1` lies in the ideal generated by the clause
5//! polynomials over the *multilinear* `GF(2)` ring (where `x² = x`, so every monomial is squarefree).
6//! At a **fixed degree `d`** this is a finite linear-algebra question — is `1` in the `GF(2)`-span of
7//! `{ m · p_C : clause C, monomial m, deg(m·p_C) ≤ d }`? — answered by Gaussian elimination. So degree-`d`
8//! Nullstellensatz is polynomial, and the **degree is the power dial**: `d = 1` recovers parity, larger
9//! `d` reaches counting-style and beyond. The genuinely hard residue (random) needs degree `Θ(n)`, which
10//! is exactly why it stays hard — there is no low-degree algebraic certificate, just as there is no small
11//! symmetry quotient. The two are the same wall seen from two sides.
12//!
13//! Symmetry meets this engine at the monomial basis: an automorphism of the formula permutes monomials,
14//! so the Nullstellensatz system has the family's symmetry and its solution can be sought on the orbit
15//! quotient of the basis — the algebraic form of "decide on the quotient."
16
17use crate::cdcl::Lit;
18use std::collections::{BTreeSet, HashMap, HashSet};
19
20/// A multilinear monomial: the bitmask of the variables it contains (`x² = x` ⟹ squarefree). The `u64`
21/// mask carries up to 63 variables; the *clause* engine's explicit cube enumeration stops at 20, the
22/// degree-bounded polynomial engine ([`monomials_up_to_degree`]) uses the full range.
23pub type Mono = u64;
24/// A multilinear polynomial over `GF(2)`: the set of monomials with coefficient 1 (XOR/symmetric-
25/// difference semantics).
26pub type Poly = BTreeSet<Mono>;
27
28fn toggle(p: &mut Poly, m: Mono) {
29 if !p.remove(&m) {
30 p.insert(m);
31 }
32}
33
34pub(crate) fn poly_mul_mono(p: &Poly, m: Mono) -> Poly {
35 let mut r = Poly::new();
36 for &t in p {
37 toggle(&mut r, t | m); // multilinear product: x·x = x ⟹ OR the masks
38 }
39 r
40}
41
42fn poly_mul(a: &Poly, b: &Poly) -> Poly {
43 let mut r = Poly::new();
44 for &s in a {
45 for &t in b {
46 toggle(&mut r, s | t);
47 }
48 }
49 r
50}
51
52/// The clause polynomial: `1` exactly on the clause's falsifying assignment. The false-indicator of a
53/// positive literal `x` is `1 + x` (`{∅, {x}}`), of a negative literal `¬x` is `x` (`{{x}}`); the clause
54/// polynomial is their product. Degree = clause width.
55pub fn clause_polynomial(clause: &[Lit]) -> Poly {
56 let mut p: Poly = [0u64].into_iter().collect(); // the polynomial "1" (the empty monomial)
57 for l in clause {
58 let bit = 1u64 << l.var();
59 let indicator: Poly = if l.is_positive() {
60 [0u64, bit].into_iter().collect() // 1 + x
61 } else {
62 [bit].into_iter().collect() // x
63 };
64 p = poly_mul(&p, &indicator);
65 }
66 p
67}
68
69/// Is `target` in the `GF(2)`-span of `rows`? Gaussian elimination over packed bit-rows: reduce the rows
70/// to an echelon basis (one pivot bit each), then reduce `target` by it and check it vanishes.
71fn in_gf2_span(mut rows: Vec<Vec<u64>>, target: &[u64]) -> bool {
72 let words = target.len();
73 let high_bit = |r: &[u64]| -> Option<usize> {
74 for w in (0..words).rev() {
75 if r[w] != 0 {
76 return Some(w * 64 + (63 - r[w].leading_zeros() as usize));
77 }
78 }
79 None
80 };
81 let xor_into = |dst: &mut [u64], src: &[u64]| {
82 for w in 0..words {
83 dst[w] ^= src[w];
84 }
85 };
86 // Build an echelon basis keyed by pivot bit.
87 let mut basis: HashMap<usize, Vec<u64>> = HashMap::new();
88 for mut r in rows.drain(..) {
89 while let Some(p) = high_bit(&r) {
90 match basis.get(&p) {
91 Some(b) => xor_into(&mut r, b),
92 None => break,
93 }
94 }
95 if let Some(p) = high_bit(&r) {
96 basis.insert(p, r);
97 }
98 }
99 // Reduce the target.
100 let mut t = target.to_vec();
101 while let Some(p) = high_bit(&t) {
102 match basis.get(&p) {
103 Some(b) => xor_into(&mut t, b),
104 None => break,
105 }
106 }
107 t.iter().all(|&w| w == 0)
108}
109
110/// Does a **degree-`d` Nullstellensatz refutation** exist over `GF(2)`? Sound: such a certificate exists
111/// only when the formula is unsatisfiable. Complete at `d = num_vars` (full degree decides any instance).
112/// Bounded to `num_vars ≤ 20` (the explicit monomial basis).
113pub fn nullstellensatz_refutes(num_vars: usize, clauses: &[Vec<Lit>], degree: usize) -> bool {
114 if num_vars > 20 {
115 return false;
116 }
117 // Monomial basis: every squarefree monomial of degree ≤ `degree`.
118 let mut index: HashMap<Mono, usize> = HashMap::new();
119 for m in 0u64..(1u64 << num_vars) {
120 if m.count_ones() as usize <= degree {
121 let n = index.len();
122 index.insert(m, n);
123 }
124 }
125 let nb = index.len();
126 let words = nb.div_ceil(64).max(1);
127 let to_bits = |p: &Poly| -> Vec<u64> {
128 let mut b = vec![0u64; words];
129 for &m in p {
130 if let Some(&i) = index.get(&m) {
131 b[i / 64] |= 1 << (i % 64);
132 }
133 }
134 b
135 };
136 let monos: Vec<Mono> = index.keys().copied().collect();
137
138 // Generators: m · p_C for every clause C and monomial m with deg(m · p_C) ≤ degree.
139 let mut rows: Vec<Vec<u64>> = Vec::new();
140 for c in clauses {
141 if c.is_empty() {
142 return true; // an empty clause is `1 = 0` outright
143 }
144 let width = c.len();
145 if width > degree {
146 continue;
147 }
148 let pc = clause_polynomial(c);
149 for &m in &monos {
150 if m.count_ones() as usize <= degree - width {
151 rows.push(to_bits(&poly_mul_mono(&pc, m)));
152 }
153 }
154 }
155 // Target: the constant polynomial `1` — the empty monomial.
156 let mut target = vec![0u64; words];
157 let t0 = index[&0u64];
158 target[t0 / 64] |= 1 << (t0 % 64);
159 in_gf2_span(rows, &target)
160}
161
162/// Reduce a polynomial against an echelon basis keyed by leading (largest) monomial: while its leading
163/// monomial is a pivot, XOR (symmetric-difference) that basis row in. The leading monomial strictly
164/// decreases each step, so this terminates; the result is `p` modulo the span.
165fn pc_reduce(basis: &HashMap<Mono, Poly>, mut p: Poly) -> Poly {
166 while let Some(&lm) = p.iter().next_back() {
167 match basis.get(&lm) {
168 Some(b) => {
169 for &m in b {
170 toggle(&mut p, m);
171 }
172 }
173 None => break,
174 }
175 }
176 p
177}
178
179/// Does a **degree-`d` Polynomial Calculus refutation** exist over `GF(2)`? PC is the *dynamic*
180/// strengthening of Nullstellensatz: start from the clause polynomials and close under (i) `GF(2)` linear
181/// combination and (ii) multiplication by a single variable, keeping every *derived* polynomial
182/// multilinear of degree ≤ `d`; the system is refuted iff the constant `1` is derived. Because an
183/// intermediate linear combination can cancel high-degree terms *before* the next multiply, degree-`d` PC
184/// certifies a superset of what degree-`d` Nullstellensatz can (which must hit each axiom with a single
185/// monomial in one shot). Sound — `1` is derivable only from an unsatisfiable system — and `PC ⊇ NS`, so
186/// it never refutes fewer. Complete at `d = num_vars`. Bounded to `num_vars ≤ 20`.
187pub fn polynomial_calculus_refutes(num_vars: usize, clauses: &[Vec<Lit>], degree: usize) -> bool {
188 if num_vars > 20 {
189 return false;
190 }
191 let poly_deg = |p: &Poly| p.iter().map(|&m| m.count_ones() as usize).max().unwrap_or(0);
192 let mut basis: HashMap<Mono, Poly> = HashMap::new();
193 let one: Poly = [0u64].into_iter().collect();
194
195 // Seed the worklist with the clause polynomials usable at this degree (width ≤ d).
196 let mut worklist: Vec<Poly> = Vec::new();
197 for c in clauses {
198 if c.is_empty() {
199 return true; // an empty clause is `1 = 0` outright
200 }
201 if c.len() <= degree {
202 worklist.push(clause_polynomial(c));
203 }
204 }
205 // Saturate: each newly-independent polynomial joins the basis and is multiplied by every variable
206 // (staying within degree), feeding the closure back into the worklist.
207 while let Some(p) = worklist.pop() {
208 let r = pc_reduce(&basis, p);
209 let Some(&lm) = r.iter().next_back() else { continue }; // reduced to 0 — no new information
210 for i in 0..num_vars as u64 {
211 let q = poly_mul_mono(&r, 1u64 << i);
212 if !q.is_empty() && poly_deg(&q) <= degree {
213 worklist.push(q);
214 }
215 }
216 basis.insert(lm, r);
217 if pc_reduce(&basis, one.clone()).is_empty() {
218 return true; // the constant 1 is in the span — refuted
219 }
220 }
221 false
222}
223
224/// A **constructive Nullstellensatz certificate** over the multilinear `GF(2)` ring: one coefficient
225/// polynomial `g_C` per input clause such that `Σ_C p_C · g_C = 1`, where `p_C = clause_polynomial(C)` is the
226/// clause's false-indicator. The identity is a re-checkable proof of UNSAT — evaluate it at any assignment
227/// `a`: the right side is `1`, so some `p_C(a) = 1`, i.e. *every* assignment falsifies some clause. Where
228/// [`nullstellensatz_refutes`] only *decides* whether such a certificate exists, this one carries the witness.
229#[derive(Clone, Debug)]
230pub struct NsCertificate {
231 num_vars: usize,
232 /// `coeffs[i]` is `g_{C_i}` for the `i`-th input clause (parallel indexing to the clause list).
233 coeffs: Vec<Poly>,
234}
235
236impl NsCertificate {
237 /// The variable count the certificate lives over.
238 pub fn num_vars(&self) -> usize {
239 self.num_vars
240 }
241
242 /// The maximum monomial degree among the coefficient polynomials. Multilinear over `num_vars`
243 /// variables, so `≤ num_vars` unconditionally — the content is not this (trivial for a multilinear
244 /// certificate) but that the certificate *exists and is built by one uniform construction at every `n`*.
245 pub fn degree(&self) -> usize {
246 self.coeffs.iter().flatten().map(|m| m.count_ones() as usize).max().unwrap_or(0)
247 }
248
249 /// **Re-check against the original clauses** (zero trust in the producer): recompute
250 /// `Σ_C clause_polynomial(C) · g_C` and confirm it is the constant `1` (the empty monomial). A `true`
251 /// verdict is an independent proof that `clauses` is unsatisfiable; it fails closed if the certificate's
252 /// clause count does not match the formula it is checked against.
253 pub fn verify(&self, clauses: &[Vec<Lit>]) -> bool {
254 if self.coeffs.len() != clauses.len() {
255 return false;
256 }
257 let mut sum = Poly::new();
258 for (c, g) in clauses.iter().zip(&self.coeffs) {
259 if g.is_empty() {
260 continue;
261 }
262 for m in poly_mul(&clause_polynomial(c), g) {
263 toggle(&mut sum, m);
264 }
265 }
266 sum.len() == 1 && sum.contains(&0u64)
267 }
268}
269
270/// The single-point indicator `δ_a` — the multilinear function that is `1` at assignment `a` and `0` at every
271/// other corner: `Π_{a_i=1} x_i · Π_{a_i=0} (1 + x_i)`. Its monomials are `ones(a) ∪ T` over every subset `T`
272/// of the zero-coordinates (top degree `n`); the `Σ_a δ_a = 1` identity over all corners is the partition of
273/// unity the certificate construction rests on.
274fn point_indicator(a: u64, num_vars: usize) -> Poly {
275 let mask = (1u64 << num_vars).wrapping_sub(1);
276 let ones = a & mask;
277 let zeros = !a & mask;
278 let mut p = Poly::new();
279 let mut sub = zeros;
280 loop {
281 p.insert(ones | sub); // masks are distinct (T ⊆ zeros, disjoint from ones) — no cancellation
282 if sub == 0 {
283 break;
284 }
285 sub = (sub - 1) & zeros;
286 }
287 p
288}
289
290/// **The uniform Nullstellensatz completeness construction.** For any CNF over `num_vars ≤ 20` variables,
291/// return either a constructive [`NsCertificate`] proving UNSAT, or a satisfying assignment proving SAT — a
292/// *total, certifying* decision. The construction is the partition of unity `Σ_a δ_a = 1`: every corner `a`
293/// is charged to one clause it falsifies (a corner that falsifies none *is* a model — SAT), and the
294/// coefficient of clause `C` is `g_C = Σ_{a charged to C} δ_a`. Then `Σ_C p_C · g_C = Σ_a p_{sel(a)} · δ_a =
295/// Σ_a δ_a = 1`, because `p_{sel(a)}(a) = 1` collapses `p·δ_a` to `δ_a` on the cube (multilinear
296/// representations are unique). Because this succeeds *identically at every `n`*, it does not merely measure
297/// but **proves** that every unsatisfiable formula has a degree-`≤ n` `GF(2)` Nullstellensatz refutation — no
298/// minimal-UNSAT family is structureless, at any `n`. This is the census's `max_ns_degree = n` ceiling as a
299/// construction, settling `n = 5, 6, …` where the orbit census is infeasible.
300pub fn build_ns_certificate(num_vars: usize, clauses: &[Vec<Lit>]) -> Result<NsCertificate, Vec<bool>> {
301 assert!(num_vars <= 20, "the explicit-corner construction is bounded to num_vars ≤ 20");
302 let mut coeffs: Vec<Poly> = vec![Poly::new(); clauses.len()];
303 for a in 0u64..(1u64 << num_vars) {
304 let sel = clauses
305 .iter()
306 .position(|c| !c.iter().any(|l| ((a >> l.var()) & 1 == 1) == l.is_positive()));
307 match sel {
308 None => return Err((0..num_vars).map(|i| (a >> i) & 1 == 1).collect()),
309 Some(ci) => {
310 for m in point_indicator(a, num_vars) {
311 toggle(&mut coeffs[ci], m);
312 }
313 }
314 }
315 }
316 Ok(NsCertificate { num_vars, coeffs })
317}
318
319/// The closure of a permutation group under composition (BFS), for small groups. Elements keyed by their
320/// image vector so the group is deduplicated.
321pub(crate) fn close_perm_group(gens: &[crate::proof::Perm], num_vars: usize) -> Vec<crate::proof::Perm> {
322 use crate::proof::Perm;
323 let key = |p: &Perm| -> Vec<u32> { (0..num_vars).map(|v| p.apply(Lit::pos(v as u32)).var()).collect() };
324 let id = Perm::identity(num_vars);
325 let mut seen: std::collections::BTreeSet<Vec<u32>> = [key(&id)].into_iter().collect();
326 let mut group = vec![id.clone()];
327 let mut frontier = vec![id];
328 while let Some(p) = frontier.pop() {
329 for g in gens {
330 let q = p.compose(g);
331 if seen.insert(key(&q)) {
332 group.push(q.clone());
333 frontier.push(q);
334 }
335 }
336 }
337 group
338}
339
340/// The **symmetrization** of a `GF(2)` functional `L` (given as the monomials where it is `1`) over a group:
341/// `Σ_{g∈G} g·L` (mod 2). The Reynolds/averaging operator of characteristic 0 — but *without the `1/|G|`*,
342/// because that division is unavailable over `GF(2)`.
343fn symmetrize(l: &[Mono], group: &[crate::proof::Perm]) -> BTreeSet<Mono> {
344 let mut sym: BTreeSet<Mono> = BTreeSet::new();
345 for &m in l {
346 for g in group {
347 let img = apply_perm_to_mono(g, m);
348 if !sym.remove(&img) {
349 sym.insert(img);
350 }
351 }
352 }
353 sym
354}
355
356/// Exact binomial coefficient `C(n, k)` in `u128` (small `n`; the running-product form stays integral).
357fn binom(n: usize, k: usize) -> u128 {
358 if k > n {
359 return 0;
360 }
361 let k = k.min(n - k);
362 let mut c = 1u128;
363 for i in 0..k {
364 c = c * (n - i) as u128 / (i + 1) as u128;
365 }
366 c
367}
368
369/// The width of the degree-`d` Nullstellensatz system over `n` variables: the count of multilinear monomials
370/// of degree `≤ d`, `Σ_{k≤d} C(n,k)`. At `d = n` this is exactly `2ⁿ` — so the degree-`n` certificate that
371/// [`build_ns_certificate`] always produces (completeness: no unsatisfiable formula over `n` variables is
372/// "structureless") lives in an **exponentially large** space. The certificate's *existence* is an
373/// information-theoretic fact about the finite cube; it is not an efficient algorithm, and it says nothing
374/// about P vs NP — which is a statement about the asymptotic growth of a *family* of instances, not any fixed
375/// finite `n` (a fixed finite problem is decidable by table lookup, vacuously).
376pub fn nullstellensatz_basis_size(n: usize, d: usize) -> u128 {
377 (0..=d.min(n)).map(|k| binom(n, k)).sum()
378}
379
380/// All squarefree monomials of degree `≤ degree` over `num_vars ≤ 63` variables, ascending as `u64`s —
381/// enumerated directly (Gosper's next-`k`-subset walk per degree class), never touching the `2ⁿ` cube.
382/// This is the basis walk that lifts fixed-degree Nullstellensatz work past the clause engine's
383/// 20-variable cap: the count is `Σ_{k≤d} C(n,k)` ([`nullstellensatz_basis_size`]), not `2ⁿ`.
384pub fn monomials_up_to_degree(num_vars: usize, degree: usize) -> Vec<Mono> {
385 assert!(num_vars <= 63, "the u64 monomial mask carries ≤ 63 variables");
386 let mut out: Vec<Mono> = vec![0];
387 for k in 1..=degree.min(num_vars) {
388 let limit: Mono = 1u64 << num_vars;
389 let mut m: Mono = (1u64 << k) - 1; // the least k-subset
390 while m < limit {
391 out.push(m);
392 let c = m & m.wrapping_neg(); // Gosper's hack: next integer with the same popcount
393 let r = m + c;
394 m = (((r ^ m) >> 2) / c) | r;
395 }
396 }
397 out.sort_unstable();
398 out
399}
400
401/// The degree of a multilinear `GF(2)` polynomial: its largest monomial's popcount (`0` for the zero
402/// polynomial and for the constant `1`).
403pub fn poly_degree(p: &Poly) -> usize {
404 p.iter().map(|&m| m.count_ones() as usize).max().unwrap_or(0)
405}
406
407/// Does a **degree-`d` Nullstellensatz refutation** exist over `GF(2)` for an arbitrary polynomial
408/// generator system — is `1` in the `GF(2)`-span of `{ m·g : deg(m·g) ≤ d }`? The clause engine's
409/// question asked of *any* generators, not just clause polynomials: the substrate for the linear
410/// encoding ([`exactly_one_linear_generators`]) and the symmetric-family machinery. The multiplier rule
411/// is exact — a product is admitted by its degree *after* multilinear collapse. (For clause polynomials
412/// this span equals the clause engine's: a multiplier overlapping a positive literal kills the product,
413/// one overlapping a negative literal absorbs into a smaller multiplier — pinned by the differential
414/// test.) Degree-bounded enumeration, so it scales to `num_vars ≤ 63`.
415pub fn ns_refutes_polys(num_vars: usize, gens: &[Poly], degree: usize) -> bool {
416 let basis = monomials_up_to_degree(num_vars, degree);
417 let index: HashMap<Mono, usize> = basis.iter().enumerate().map(|(i, &m)| (m, i)).collect();
418 let words = basis.len().div_ceil(64).max(1);
419 let to_bits = |p: &Poly| -> Vec<u64> {
420 let mut b = vec![0u64; words];
421 for &m in p {
422 if let Some(&i) = index.get(&m) {
423 b[i / 64] |= 1 << (i % 64);
424 }
425 }
426 b
427 };
428 let mut rows: Vec<Vec<u64>> = Vec::new();
429 for g in gens {
430 if g.is_empty() {
431 continue; // the zero polynomial generates nothing
432 }
433 for &m in &basis {
434 let prod = poly_mul_mono(g, m);
435 if !prod.is_empty() && poly_degree(&prod) <= degree {
436 rows.push(to_bits(&prod));
437 }
438 }
439 }
440 let t0 = index[&0u64];
441 let mut target = vec![0u64; words];
442 target[t0 / 64] |= 1 << (t0 % 64);
443 in_gf2_span(rows, &target)
444}
445
446/// [`ns_lower_bound_witness`] for an arbitrary polynomial generator system: a degree-`d`
447/// pseudo-expectation `L` with `L(1) = 1` and `L(m·g) = 0` for every admitted generator, returned as the
448/// monomials where `L = 1`. `Some(L)` certifies `NS-degree > d` for the system (re-checkable by
449/// [`check_ns_lower_bound_polys`], zero trust in the solver); `None` means a degree-`d` refutation
450/// exists. Degree-bounded enumeration — `num_vars ≤ 63`.
451pub fn ns_lower_bound_witness_polys(num_vars: usize, gens: &[Poly], degree: usize) -> Option<Vec<Mono>> {
452 let basis = monomials_up_to_degree(num_vars, degree);
453 let index: HashMap<Mono, usize> = basis.iter().enumerate().map(|(i, &m)| (m, i)).collect();
454 let nb = basis.len();
455 let words = nb.div_ceil(64).max(1);
456 let mask_of = |p: &Poly| -> Vec<u64> {
457 let mut mask = vec![0u64; words];
458 for &m in p {
459 if let Some(&i) = index.get(&m) {
460 mask[i / 64] |= 1u64 << (i % 64);
461 }
462 }
463 mask
464 };
465 let mut eqs: Vec<(Vec<u64>, bool)> = Vec::new();
466 for g in gens {
467 if g.is_empty() {
468 continue;
469 }
470 for &m in &basis {
471 let prod = poly_mul_mono(g, m);
472 if !prod.is_empty() && poly_degree(&prod) <= degree {
473 eqs.push((mask_of(&prod), false)); // ⟨L, m·g⟩ = 0
474 }
475 }
476 }
477 let t0 = index[&0u64];
478 let mut target = vec![0u64; words];
479 target[t0 / 64] |= 1u64 << (t0 % 64);
480 eqs.push((target, true)); // L(1) = 1
481 let l = gf2_solve(&eqs, nb)?;
482 Some((0..nb).filter(|&i| (l[i / 64] >> (i % 64)) & 1 == 1).map(|i| basis[i]).collect())
483}
484
485/// [`ns_lower_bound_witness_polys`] restricted to a **sub-basis**: the functional `L` is sought only on
486/// monomials passing `in_basis` (`L = 0` elsewhere), while the constraints `⟨L, m·g⟩ = 0` still range
487/// over *all* admitted generators — so any `Some` is a fully valid,
488/// [`check_ns_lower_bound_polys`]-verifiable witness, and `None` means only "no witness on this
489/// sub-basis". The structure probe: which candidate supports carry a family's lower bound (for
490/// pigeonhole this is how the hole-injective support was found and the classical partial-matching
491/// support was ruled out over `GF(2)`). Degree-bounded enumeration — `num_vars ≤ 63`.
492pub fn ns_lower_bound_witness_polys_on_basis(
493 num_vars: usize,
494 gens: &[Poly],
495 degree: usize,
496 in_basis: &dyn Fn(Mono) -> bool,
497) -> Option<Vec<Mono>> {
498 let all = monomials_up_to_degree(num_vars, degree);
499 let basis: Vec<Mono> = all.iter().copied().filter(|&m| in_basis(m)).collect();
500 let index: HashMap<Mono, usize> = basis.iter().enumerate().map(|(i, &m)| (m, i)).collect();
501 index.get(&0u64)?; // the empty monomial must be in the sub-basis for L(1) = 1
502 let nb = basis.len();
503 let words = nb.div_ceil(64).max(1);
504 let mask_of = |p: &Poly| -> Vec<u64> {
505 let mut mask = vec![0u64; words];
506 for &m in p {
507 if let Some(&i) = index.get(&m) {
508 mask[i / 64] |= 1u64 << (i % 64);
509 }
510 }
511 mask
512 };
513 let mut eqs: Vec<(Vec<u64>, bool)> = Vec::new();
514 for g in gens {
515 if g.is_empty() {
516 continue;
517 }
518 for &m in &all {
519 let prod = poly_mul_mono(g, m);
520 if !prod.is_empty() && poly_degree(&prod) <= degree {
521 eqs.push((mask_of(&prod), false));
522 }
523 }
524 }
525 let t0 = index[&0u64];
526 let mut target = vec![0u64; words];
527 target[t0 / 64] |= 1u64 << (t0 % 64);
528 eqs.push((target, true));
529 let l = gf2_solve(&eqs, nb)?;
530 Some((0..nb).filter(|&i| (l[i / 64] >> (i % 64)) & 1 == 1).map(|i| basis[i]).collect())
531}
532
533/// Re-check a [`ns_lower_bound_witness_polys`] certificate (zero trust in the producer): `L(1) = 1` and
534/// `L(m·g) = 0` for every admitted generator of the system. `true` ⟹ the generator system genuinely has
535/// no degree-`d` `GF(2)` Nullstellensatz refutation. Degree-bounded enumeration — `num_vars ≤ 63`.
536pub fn check_ns_lower_bound_polys(num_vars: usize, gens: &[Poly], degree: usize, witness: &[Mono]) -> bool {
537 let l: BTreeSet<Mono> = witness.iter().copied().collect();
538 if !l.contains(&0u64) {
539 return false; // L(1) must be 1
540 }
541 let basis = monomials_up_to_degree(num_vars, degree);
542 for g in gens {
543 if g.is_empty() {
544 continue;
545 }
546 for &m in &basis {
547 let prod = poly_mul_mono(g, m);
548 if !prod.is_empty()
549 && poly_degree(&prod) <= degree
550 && prod.iter().filter(|t| l.contains(t)).count() % 2 == 1
551 {
552 return false; // ⟨L, m·g⟩ must be 0
553 }
554 }
555 }
556 true
557}
558
559/// The **linear encoding** of exactly-one constraints: for each group `G` the degree-1 generator
560/// `1 + Σ_{v∈G} x_v` (the `GF(2)` form of `Σ x = 1`) plus the pairwise products `x_u·x_v` over each
561/// group, deduplicated. This is the encoding under which the modular-counting and pigeonhole degree
562/// lower bounds are stated in the literature — the wide at-least-one clause is recovered from it modulo
563/// the pairs (the interreduction test pins the identity, and the clause→linear direction is
564/// degree-preserving, so bounds against this encoding are the stronger statements). The linear
565/// generators come first (one per group, in group order), then the pairs.
566pub fn exactly_one_linear_generators(groups: &[Vec<u32>]) -> Vec<Poly> {
567 let mut gens: Vec<Poly> = Vec::new();
568 for g in groups {
569 let mut lin: Poly = [0u64].into_iter().collect();
570 for &v in g {
571 assert!(v < 63, "the u64 monomial mask carries ≤ 63 variables");
572 toggle(&mut lin, 1u64 << v);
573 }
574 gens.push(lin);
575 }
576 let mut pairs: BTreeSet<Mono> = BTreeSet::new();
577 for g in groups {
578 for (i, &u) in g.iter().enumerate() {
579 for &v in &g[i + 1..] {
580 pairs.insert((1u64 << u) | (1u64 << v));
581 }
582 }
583 }
584 gens.extend(pairs.into_iter().map(|m| [m].into_iter().collect::<Poly>()));
585 gens
586}
587
588/// Solve a `GF(2)` linear system `⟨x, coeffᵢ⟩ = rhsᵢ` (each `coeff` a multi-word bit-vector over `nvars`
589/// variables) by Gaussian elimination to reduced row echelon form. Returns any solution `x` (bit-packed into
590/// `⌈nvars/64⌉` words) or `None` if the system is inconsistent. Multi-word, so it scales past 63 variables.
591pub(crate) fn gf2_solve(equations: &[(Vec<u64>, bool)], nvars: usize) -> Option<Vec<u64>> {
592 let words = nvars.div_ceil(64).max(1);
593 let bit = |v: &[u64], i: usize| (v[i / 64] >> (i % 64)) & 1 == 1;
594 let mut rows: Vec<(Vec<u64>, bool)> = equations.to_vec();
595 let mut pivots: Vec<usize> = Vec::new();
596 let mut r = 0usize;
597 for col in 0..nvars {
598 let Some(sel) = (r..rows.len()).find(|&i| bit(&rows[i].0, col)) else {
599 continue;
600 };
601 rows.swap(r, sel);
602 let (pmask, prhs) = (rows[r].0.clone(), rows[r].1);
603 for i in 0..rows.len() {
604 if i != r && bit(&rows[i].0, col) {
605 for w in 0..words {
606 rows[i].0[w] ^= pmask[w];
607 }
608 rows[i].1 ^= prhs;
609 }
610 }
611 pivots.push(col);
612 r += 1;
613 }
614 if rows.iter().any(|(m, b)| *b && m.iter().all(|&w| w == 0)) {
615 return None; // 0 = 1 — inconsistent
616 }
617 // RREF: each pivot row's only set bits are its pivot column plus free columns; free vars = 0 ⟹ x_col = rhs.
618 let mut x = vec![0u64; words];
619 for (i, &col) in pivots.iter().enumerate() {
620 if rows[i].1 {
621 x[col / 64] |= 1u64 << (col % 64);
622 }
623 }
624 Some(x)
625}
626
627/// **A re-checkable degree-`d` Nullstellensatz LOWER-bound certificate.** Where [`nullstellensatz_refutes`]
628/// *decides* whether a degree-`d` refutation exists, this witnesses its NON-existence: a `GF(2)` linear
629/// functional `L` on the degree-`≤ d` monomials (a *degree-`d` pseudo-expectation`) with `L(1) = 1` and
630/// `L(m · p_C) = 0` for every generator. Such an `L` exists iff `1` is **not** in the `GF(2)`-span of the
631/// generators, i.e. iff there is no degree-`d` refutation — so `Some(L)` certifies the lower bound
632/// `NS-degree(F) > d`, independently re-checkable by [`check_ns_lower_bound`] (no trust in the solver). The
633/// certificate is returned as the list of monomials on which `L` is `1`. `None` means a degree-`d` refutation
634/// exists (no lower bound at `d`). Multi-word linear algebra, so the monomial basis is not size-capped —
635/// only `num_vars ≤ 20` for the explicit basis enumeration.
636pub fn ns_lower_bound_witness(num_vars: usize, clauses: &[Vec<Lit>], degree: usize) -> Option<Vec<u64>> {
637 if num_vars > 20 {
638 return None;
639 }
640 let mut index: HashMap<Mono, usize> = HashMap::new();
641 let mut monos: Vec<Mono> = Vec::new();
642 for m in 0u64..(1u64 << num_vars) {
643 if m.count_ones() as usize <= degree {
644 index.insert(m, monos.len());
645 monos.push(m);
646 }
647 }
648 let nb = monos.len();
649 let words = nb.div_ceil(64).max(1);
650 let mask_of = |p: &Poly| -> Vec<u64> {
651 let mut mask = vec![0u64; words];
652 for &m in p {
653 if let Some(&i) = index.get(&m) {
654 mask[i / 64] |= 1u64 << (i % 64);
655 }
656 }
657 mask
658 };
659 let mut eqs: Vec<(Vec<u64>, bool)> = Vec::new();
660 for c in clauses {
661 let width = c.len();
662 if width == 0 {
663 return None; // an empty clause is an immediate refutation — no lower bound
664 }
665 if width > degree {
666 continue;
667 }
668 let pc = clause_polynomial(c);
669 for &m in &monos {
670 if m.count_ones() as usize <= degree - width {
671 eqs.push((mask_of(&poly_mul_mono(&pc, m)), false)); // ⟨L, m·p_C⟩ = 0
672 }
673 }
674 }
675 let t0 = index[&0u64];
676 let mut target = vec![0u64; words];
677 target[t0 / 64] |= 1u64 << (t0 % 64);
678 eqs.push((target, true)); // L(1) = 1
679 let l = gf2_solve(&eqs, nb)?;
680 Some((0..nb).filter(|&i| (l[i / 64] >> (i % 64)) & 1 == 1).map(|i| monos[i]).collect())
681}
682
683/// [`ns_lower_bound_witness`] restricted to a **sub-basis** of monomials: the functional `L` is sought only
684/// on monomials `m` with `in_basis(m)` (and `L = 0` elsewhere), while the constraints `⟨L, m·p_C⟩ = 0` still
685/// range over *all* generators. `Some(L)` is a fully valid, `check_ns_lower_bound`-verifiable witness (the
686/// restriction only limits the *search*, not the check); `None` here means "no witness on this sub-basis" —
687/// which, unlike the full-basis version, does **not** imply a refutation exists. This exposes the *structure*
688/// of a lower bound: e.g. for pigeonhole, the partial-matching sub-basis already carries the certificate.
689pub fn ns_lower_bound_witness_on_basis(
690 num_vars: usize,
691 clauses: &[Vec<Lit>],
692 degree: usize,
693 in_basis: &dyn Fn(Mono) -> bool,
694) -> Option<Vec<u64>> {
695 if num_vars > 20 {
696 return None;
697 }
698 let mut index: HashMap<Mono, usize> = HashMap::new();
699 let mut basis: Vec<Mono> = Vec::new();
700 for m in 0u64..(1u64 << num_vars) {
701 if m.count_ones() as usize <= degree && in_basis(m) {
702 index.insert(m, basis.len());
703 basis.push(m);
704 }
705 }
706 index.get(&0u64)?; // the empty monomial must be in the basis for L(1) = 1
707 let nb = basis.len();
708 let words = nb.div_ceil(64).max(1);
709 let mask_of = |p: &Poly| -> Vec<u64> {
710 let mut mask = vec![0u64; words];
711 for &m in p {
712 if let Some(&i) = index.get(&m) {
713 mask[i / 64] |= 1u64 << (i % 64);
714 }
715 }
716 mask
717 };
718 let mults: Vec<Mono> =
719 (0u64..(1u64 << num_vars)).filter(|m| m.count_ones() as usize <= degree).collect();
720 let mut eqs: Vec<(Vec<u64>, bool)> = Vec::new();
721 for c in clauses {
722 let width = c.len();
723 if width == 0 {
724 return None;
725 }
726 if width > degree {
727 continue;
728 }
729 let pc = clause_polynomial(c);
730 for &m in &mults {
731 if m.count_ones() as usize <= degree - width {
732 eqs.push((mask_of(&poly_mul_mono(&pc, m)), false));
733 }
734 }
735 }
736 let t0 = index[&0u64];
737 let mut target = vec![0u64; words];
738 target[t0 / 64] |= 1u64 << (t0 % 64);
739 eqs.push((target, true));
740 let l = gf2_solve(&eqs, nb)?;
741 Some((0..nb).filter(|&i| (l[i / 64] >> (i % 64)) & 1 == 1).map(|i| basis[i]).collect())
742}
743
744/// Is a `PHP` monomial a **partial matching** — an injective partial pigeon→hole map? PHP's variable
745/// `x_{p,h}` sits at index `p·holes + h` ([`crate::families::php`]), so a monomial (a set of variables) is a
746/// partial matching iff no two of its variables share a pigeon or a hole. This is the support of the
747/// Razborov degree lower bound's pseudo-expectation.
748pub fn php_is_partial_matching(mono: Mono, holes: usize) -> bool {
749 let (mut pigeons, mut used_holes) = (0u64, 0u64);
750 let mut bits = mono;
751 while bits != 0 {
752 let v = bits.trailing_zeros() as usize;
753 let (p, h) = (v / holes, v % holes);
754 if (pigeons >> p) & 1 == 1 || (used_holes >> h) & 1 == 1 {
755 return false;
756 }
757 pigeons |= 1u64 << p;
758 used_holes |= 1u64 << h;
759 bits &= bits - 1;
760 }
761 true
762}
763
764/// Is a `PHP` monomial **hole-injective** — no two of its edges share a hole? Standard PHP forbids two
765/// pigeons in one hole but allows a pigeon in several holes, so the at-most-one clauses force the `GF(2)`
766/// pseudo-expectation to vanish exactly on hole-*collision* monomials — i.e. its support lies in the
767/// hole-injective monomials (partial functions hole→pigeon). This is looser than a partial matching (which
768/// also forbids pigeon repeats) and is the correct `GF(2)` support.
769pub fn php_is_hole_injective(mono: Mono, holes: usize) -> bool {
770 let mut used_holes = 0u64;
771 let mut bits = mono;
772 while bits != 0 {
773 let h = (bits.trailing_zeros() as usize) % holes;
774 if (used_holes >> h) & 1 == 1 {
775 return false;
776 }
777 used_holes |= 1u64 << h;
778 bits &= bits - 1;
779 }
780 true
781}
782
783/// Re-check a [`ns_lower_bound_witness`] certificate (zero trust in the producer): the functional `L` (given
784/// as the monomials on which it is `1`) must satisfy `L(1) = 1` and `L(m · p_C) = 0` for every degree-`≤ d`
785/// generator. `true` ⟹ `F` genuinely has no degree-`d` `GF(2)` Nullstellensatz refutation.
786pub fn check_ns_lower_bound(num_vars: usize, clauses: &[Vec<Lit>], degree: usize, witness: &[u64]) -> bool {
787 let l: BTreeSet<Mono> = witness.iter().copied().collect();
788 let pairs = |p: &Poly| -> bool { p.iter().filter(|m| l.contains(m)).count() % 2 == 1 }; // ⟨L, p⟩
789 if !l.contains(&0u64) {
790 return false; // L(1) must be 1
791 }
792 for c in clauses {
793 let width = c.len();
794 if width == 0 {
795 return false;
796 }
797 if width > degree {
798 continue;
799 }
800 let pc = clause_polynomial(c);
801 for m in 0u64..(1u64 << num_vars) {
802 if m.count_ones() as usize <= degree.saturating_sub(width) {
803 if pairs(&poly_mul_mono(&pc, m)) {
804 return false; // ⟨L, m·p_C⟩ must be 0
805 }
806 }
807 }
808 }
809 true
810}
811
812/// The **atom** of the partition-of-unity recurrence on variable `v`: `(1 + x_v) + x_v`, which reduces to
813/// the constant `1` in the multilinear `GF(2)` ring (`x_v + x_v = 0`). This single identity is the engine of
814/// the whole `n = ∞` ratchet: it is *independent of `n`*, so a product of `n` copies of it is `1` at every
815/// scale.
816pub fn pou_atom(v: usize) -> Poly {
817 let x: Poly = [1u64 << v].into_iter().collect();
818 let one_plus_x: Poly = [0u64, 1u64 << v].into_iter().collect();
819 let mut atom = one_plus_x;
820 for m in x {
821 toggle(&mut atom, m);
822 }
823 atom
824}
825
826/// The **partition of unity** over the `n`-cube: `Σ_{a ∈ {0,1}ⁿ} δ_a`, the sum of every corner's
827/// point-indicator. It is the constant `1` for all `n` — the identity the constructive Nullstellensatz
828/// certificate ([`build_ns_certificate`]) rests on. Computed here by direct summation (for finite checks);
829/// [`pou_as_product`] gives the closed form that proves it `∀n`.
830pub fn partition_of_unity(n: usize) -> Poly {
831 let mut sum = Poly::new();
832 for a in 0..(1u64 << n) {
833 for m in point_indicator(a, n) {
834 toggle(&mut sum, m);
835 }
836 }
837 sum
838}
839
840/// The **closed form** of the partition of unity: the product `Π_{v<n} ((1+x_v) + x_v)` of the per-coordinate
841/// atoms. By distributivity `Σ_a Π_i f_{i,a_i} = Π_i (f_{i,0} + f_{i,1})`, this equals
842/// [`partition_of_unity`] — a *sum of `2ⁿ` products* rewritten as a *product of `n` sums*. Since every atom
843/// is `1` ([`pou_atom`]), the product is `1` for **every** `n`. This is the ratchet to `n = ∞`: not `2ⁿ`
844/// terms checked one cube at a time, but `n` identical unit factors.
845pub fn pou_as_product(n: usize) -> Poly {
846 let mut product: Poly = [0u64].into_iter().collect(); // the polynomial 1
847 for v in 0..n {
848 product = poly_mul(&product, &pou_atom(v));
849 }
850 product
851}
852
853/// Apply a formula automorphism to a monomial: a monomial is a *set of variables*, so the permutation
854/// part of `σ` relabels them (phase flips do not act on monomials). The bridge from the symmetry group
855/// to the polynomial basis.
856pub(crate) fn apply_perm_to_mono(perm: &crate::proof::Perm, m: Mono) -> Mono {
857 let mut out = 0u64;
858 for v in 0..perm.num_vars() {
859 if m & (1 << v) != 0 {
860 out |= 1 << perm.apply(Lit::pos(v as u32)).var();
861 }
862 }
863 out
864}
865
866/// **Symmetry-break Polynomial Calculus at the basis.** Partition the degree-≤`degree` monomials into
867/// orbits under the formula's automorphisms. The orbit count is the width of the *symmetry-reduced*
868/// Nullstellensatz system — a symmetric certificate is constant on each orbit, so the Gaussian runs over
869/// the quotient instead of all `C(n,d)` monomials. The same collapse that made the field cuts O(1),
870/// inherited by the algebraic engine. (Bounded to `num_vars ≤ 20`.)
871pub fn monomial_orbits(num_vars: usize, degree: usize, generators: &[crate::proof::Perm]) -> Vec<Vec<Mono>> {
872 let basis: BTreeSet<Mono> =
873 (0u64..(1u64 << num_vars)).filter(|m| m.count_ones() as usize <= degree).collect();
874 let mut seen: BTreeSet<Mono> = BTreeSet::new();
875 let mut orbits = Vec::new();
876 for &m in &basis {
877 if seen.contains(&m) {
878 continue;
879 }
880 let mut orbit = BTreeSet::new();
881 orbit.insert(m);
882 let mut stack = vec![m];
883 while let Some(x) = stack.pop() {
884 for g in generators {
885 let y = apply_perm_to_mono(g, x);
886 if basis.contains(&y) && orbit.insert(y) {
887 stack.push(y);
888 }
889 }
890 }
891 for &x in &orbit {
892 seen.insert(x);
893 }
894 orbits.push(orbit.into_iter().collect());
895 }
896 orbits
897}
898
899/// The generators of the full symmetric group `Sₙ` on `n` variables: the adjacent transpositions
900/// `(i, i+1)`. `Sₙ` is the symmetry of a *fully-symmetric* formula (every variable interchangeable), and
901/// under it the monomial basis collapses to one orbit per degree — the sharpest instance of
902/// [`monomial_orbits`]' compression.
903pub fn symmetric_group_generators(n: usize) -> Vec<crate::proof::Perm> {
904 (0..n.saturating_sub(1))
905 .map(|i| {
906 let images: Vec<Lit> = (0..n)
907 .map(|v| {
908 if v == i {
909 Lit::pos((i + 1) as u32)
910 } else if v == i + 1 {
911 Lit::pos(i as u32)
912 } else {
913 Lit::pos(v as u32)
914 }
915 })
916 .collect();
917 crate::proof::Perm::from_images(images)
918 })
919 .collect()
920}
921
922/// **Symmetry-reduced Nullstellensatz** — the algebraic refutation collapsed by the formula's symmetry.
923/// Full degree-`d` NS asks whether `1` lies in the `GF(2)`-span of the generators `m·p_C`, a Gaussian over
924/// up to `C(n,≤d)` monomial columns. When the formula has a symmetry group `G`, summing each generator's
925/// `G`-orbit gives an *invariant* generator, and every invariant polynomial is constant on the monomial
926/// orbits ([`monomial_orbits`]) — so the same span check runs over just `#orbits` columns and
927/// `#generator-orbits` rows. For a symmetric family that is the difference between `2^Θ(n)` and `O(1)`.
928///
929/// **Sound**: each row is an honest `GF(2)`-sum of NS generators, so `1` in the reduced span is `1` in the
930/// full span — a real refutation, hence UNSAT. Incomplete (a refutation need not be `G`-invariant), and
931/// fail-closed: if a passed generator is not a genuine symmetry (an orbit step leaves the generator set)
932/// it declines rather than reduce unsoundly. `generators` must be automorphisms of `clauses`.
933pub fn nullstellensatz_refutes_symmetric(
934 num_vars: usize,
935 clauses: &[Vec<Lit>],
936 degree: usize,
937 generators: &[crate::proof::Perm],
938) -> bool {
939 if num_vars > 20 {
940 return false;
941 }
942 // Monomial orbits give the reduced column basis; an invariant polynomial is all-or-nothing per orbit.
943 let mono_orbits = monomial_orbits(num_vars, degree, generators);
944 let n_orbits = mono_orbits.len();
945 let words = n_orbits.div_ceil(64).max(1);
946 let orbit_index: HashMap<Mono, usize> = (0u64..(1u64 << num_vars))
947 .filter(|m| m.count_ones() as usize <= degree)
948 .map(|m| (m, mono_orbits.iter().position(|o| o.contains(&m)).unwrap()))
949 .collect();
950 let to_orbit_bits = |p: &Poly| -> Vec<u64> {
951 let mut b = vec![0u64; words];
952 for (oi, orbit) in mono_orbits.iter().enumerate() {
953 if p.contains(&orbit[0]) {
954 b[oi / 64] |= 1 << (oi % 64);
955 }
956 }
957 b
958 };
959 let apply_sigma = |sigma: &crate::proof::Perm, p: &Poly| -> Poly {
960 let mut out = Poly::new();
961 for &m in p {
962 toggle(&mut out, apply_perm_to_mono(sigma, m));
963 }
964 out
965 };
966 let canon = |p: &Poly| -> Vec<Mono> { p.iter().copied().collect() };
967
968 // The NS generators m·p_C with deg(m·p_C) ≤ degree.
969 let monos: Vec<Mono> = orbit_index.keys().copied().collect();
970 let mut gens: Vec<Poly> = Vec::new();
971 for c in clauses {
972 if c.is_empty() {
973 return true; // an empty clause is `1 = 0`
974 }
975 let width = c.len();
976 if width > degree {
977 continue;
978 }
979 let pc = clause_polynomial(c);
980 for &m in &monos {
981 if m.count_ones() as usize <= degree - width {
982 gens.push(poly_mul_mono(&pc, m));
983 }
984 }
985 }
986 let gen_set: HashSet<Vec<Mono>> = gens.iter().map(|p| canon(p)).collect();
987
988 // Sum each generator-orbit into an invariant generator; express it in the orbit basis.
989 let mut seen: HashSet<Vec<Mono>> = HashSet::new();
990 let mut rows: Vec<Vec<u64>> = Vec::new();
991 for g in &gens {
992 if seen.contains(&canon(g)) {
993 continue;
994 }
995 let mut orbit_sum = Poly::new();
996 let mut local: HashSet<Vec<Mono>> = HashSet::from([canon(g)]);
997 let mut stack = vec![g.clone()];
998 while let Some(x) = stack.pop() {
999 seen.insert(canon(&x));
1000 for &m in &x {
1001 toggle(&mut orbit_sum, m);
1002 }
1003 for sigma in generators {
1004 let y = apply_sigma(sigma, &x);
1005 let yk = canon(&y);
1006 if !gen_set.contains(&yk) {
1007 return false; // not a genuine symmetry of the NS system — fail closed
1008 }
1009 if local.insert(yk) {
1010 stack.push(y);
1011 }
1012 }
1013 }
1014 rows.push(to_orbit_bits(&orbit_sum));
1015 }
1016
1017 // Target: the constant `1` (the empty monomial, its own orbit).
1018 let mut target = vec![0u64; words];
1019 if let Some(&oi) = orbit_index.get(&0u64) {
1020 target[oi / 64] |= 1 << (oi % 64);
1021 }
1022 in_gf2_span(rows, &target)
1023}
1024
1025#[cfg(test)]
1026mod tests {
1027 use super::*;
1028
1029 fn sat(num_vars: usize, clauses: &[Vec<Lit>]) -> bool {
1030 (0u64..(1u64 << num_vars)).any(|x| {
1031 clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
1032 })
1033 }
1034
1035 /// **The constructive completeness certificate carries a re-checkable witness.** `nullstellensatz_refutes`
1036 /// only *decides* that a degree-`n` certificate exists; [`build_ns_certificate`] *produces* it — the
1037 /// explicit `g_C` with `Σ_C p_C·g_C = 1` — and it re-checks against the original clauses. The construction
1038 /// rests on the partition of unity `Σ_a δ_a = 1`; a SAT formula yields a re-checked model, never a false
1039 /// refutation; and the certificate fails closed against a clause set it was not built for.
1040 #[test]
1041 fn ns_certificate_is_a_constructive_completeness_proof() {
1042 // Partition of unity `Σ_a δ_a = 1` — the identity the whole construction rests on.
1043 let mut unity = Poly::new();
1044 for a in 0u64..8 {
1045 for m in point_indicator(a, 3) {
1046 toggle(&mut unity, m);
1047 }
1048 }
1049 assert!(unity.len() == 1 && unity.contains(&0u64), "Σ_a δ_a must be the constant 1");
1050
1051 // A transitive-XOR contradiction: x0=x1, x1=x2, x0≠x2 — UNSAT with no two clauses in direct conflict.
1052 let p = |v: u32| Lit::pos(v);
1053 let q = |v: u32| Lit::neg(v);
1054 let core = vec![
1055 vec![q(0), p(1)], vec![p(0), q(1)],
1056 vec![q(1), p(2)], vec![p(1), q(2)],
1057 vec![p(0), p(2)], vec![q(0), q(2)],
1058 ];
1059 let cert = build_ns_certificate(3, &core).expect("the UNSAT core has a constructive certificate");
1060 assert!(cert.verify(&core), "the constructive certificate re-checks against the original clauses");
1061 assert!(cert.degree() <= cert.num_vars(), "the certificate degree is ≤ n by construction");
1062 // Fail-closed: the certificate must not verify against a clause set it was not built for.
1063 assert!(!cert.verify(&core[..core.len() - 1]), "a certificate must not verify a different clause set");
1064
1065 // SAT ⇒ a re-checked satisfying assignment, never a spurious refutation.
1066 let satisfiable = vec![vec![p(0), p(1)], vec![q(0), p(2)]];
1067 match build_ns_certificate(3, &satisfiable) {
1068 Err(model) => assert!(
1069 satisfiable.iter().all(|c| c.iter().any(|l| model[l.var() as usize] == l.is_positive())),
1070 "the returned SAT witness must satisfy every clause"
1071 ),
1072 Ok(_) => panic!("a satisfiable formula must not yield a refutation certificate"),
1073 }
1074 }
1075
1076 /// **The pattern that ratchets to `n = ∞` (discrete-math induction).** The partition of unity — the
1077 /// identity `Σ_a δ_a = 1` behind constructive Nullstellensatz completeness — is not checked cube-by-cube
1078 /// (that dies at `2ⁿ`). It FACTORS: `Σ_a Π_i f_{i,a_i} = Π_i ((1+x_i) + x_i)`, a product of `n`
1079 /// per-coordinate atoms, each equal to the constant `1`. Base `PoU(0) = 1`; step `PoU(n+1) = PoU(n)·atom
1080 /// = PoU(n)·1 = PoU(n)`. The step is `n`-INDEPENDENT (one fixed identity `(1+x)+x = 1`), so induction
1081 /// closes it for ALL `n`. Here: the atom is `1`, the base holds, the recurrence holds, and `PoU(n)`
1082 /// equals both the direct sum and the product — the finite checks that pin the `∀n` factorization.
1083 #[test]
1084 fn partition_of_unity_is_one_for_all_n_by_the_atom_factorization() {
1085 let one: Poly = [0u64].into_iter().collect();
1086 // The atom is the constant 1 — the n-independent engine of the ratchet.
1087 for v in 0..8 {
1088 assert_eq!(pou_atom(v), one, "the atom (1+x{v})+x{v} reduces to 1");
1089 }
1090 // Base case: the 0-cube's partition of unity is 1.
1091 assert_eq!(partition_of_unity(0), one, "PoU(0) = 1 (base case)");
1092 for n in 0..=12 {
1093 // The factorization (distributivity): the 2ⁿ-term sum equals the n-factor product.
1094 assert_eq!(partition_of_unity(n), pou_as_product(n), "PoU(n) = Π atoms (sum-of-products = product-of-sums)");
1095 // And the product of n ones is one — the conclusion, at every n.
1096 assert_eq!(partition_of_unity(n), one, "PoU(n) = 1");
1097 }
1098 // The inductive STEP, explicit: PoU(n+1) = PoU(n) · atom_n (the recurrence the induction climbs).
1099 for n in 0..12 {
1100 assert_eq!(
1101 partition_of_unity(n + 1),
1102 poly_mul(&partition_of_unity(n), &pou_atom(n)),
1103 "PoU(n+1) = PoU(n)·atom — the n-uniform inductive step"
1104 );
1105 }
1106 }
1107
1108 /// **A parametric family with machine-checked degree growth.** The all-corners cube `F_n` (every one of
1109 /// the `2ⁿ` assignments forbidden by a full-width clause) has minimum `GF(2)` Nullstellensatz degree
1110 /// *exactly* `n` — certified at each `n` by a re-checkable dual witness that no degree-`(n-1)` refutation
1111 /// exists, plus a degree-`n` refutation that does. That is machine-checked, re-verifiable **linear degree
1112 /// growth**. Honest caveat: this particular bound is *width-driven* — a width-`n` family admits no
1113 /// generator below degree `n` — so it is a clean but "easy" lower bound. A *bounded-width* family with
1114 /// super-constant degree is the deeper object; PHP(3) is measured as an honest data point.
1115 #[test]
1116 /// **The genuine Ω(n) degree lower bound: pigeonhole is NS-hard, and its degree GROWS.** PHP(m) (m
1117 /// pigeons, m−1 holes) has clause width `≤ m−1` but its `GF(2)` Nullstellensatz degree is `2(m−1) =
1118 /// Θ(√vars)` — the classical pigeonhole degree bound. We certify the EXACT degree for m = 3 (degree 4) and
1119 /// m = 4 (degree 6): a re-checkable dual witness that there is no degree-`(2m−3)` refutation, and a
1120 /// refutation at `2(m−1)`. The certified degree strictly *exceeds the clause width* (so it is not the
1121 /// trivial width bound) and strictly *grows* with `m` — a genuine, non-width, parametric degree lower
1122 /// bound with certified growth. (The uniform `∀m` proof that it is exactly `2(m−1)` is the classical
1123 /// Razborov-style theorem; here it is certified per-`m` with re-checkable witnesses.)
1124 #[test]
1125 fn pigeonhole_has_certified_growing_non_width_ns_degree() {
1126 let measured = [(3usize, 4usize), (4, 6)]; // (pigeons, exact GF(2) NS degree)
1127 let mut degrees = Vec::new();
1128 for (m, deg) in measured {
1129 let (php, _) = crate::families::php(m);
1130 // Re-checkable dual witness: no degree-(deg−1) refutation ⟹ NS-degree > deg−1.
1131 let w = ns_lower_bound_witness(php.num_vars, &php.clauses, deg - 1)
1132 .unwrap_or_else(|| panic!("PHP({m}): a degree-{} lower-bound witness must exist", deg - 1));
1133 assert!(check_ns_lower_bound(php.num_vars, &php.clauses, deg - 1, &w), "PHP({m}): NS-degree > {} re-checks", deg - 1);
1134 // Refuted at `deg` ⟹ NS-degree(PHP(m)) = deg exactly.
1135 assert!(nullstellensatz_refutes(php.num_vars, &php.clauses, deg), "PHP({m}): a degree-{deg} refutation exists");
1136 assert!(!nullstellensatz_refutes(php.num_vars, &php.clauses, deg - 1), "PHP({m}): NS-degree = {deg} exactly");
1137 // Genuine (non-width): the degree exceeds the max clause width `m−1`.
1138 assert!(deg > m - 1, "PHP({m}): NS-degree {deg} > max clause width {} — not a width bound", m - 1);
1139 degrees.push(deg);
1140 }
1141 // Certified GROWTH: the NS degree strictly climbs with the family size (Θ(√vars)).
1142 assert!(degrees.windows(2).all(|w| w[1] > w[0]), "the certified NS degree grows with n: {degrees:?}");
1143 }
1144
1145 /// **The symmetric NS width is CONSTANT in `m` at each fixed degree.** An orbit-type of a degree-`≤d` PHP
1146 /// monomial under `Sₘ × Sₘ₋₁` is the isomorphism type of its bipartite pigeon/hole graph; there are only
1147 /// finitely many such types with `≤ d` edges, and every one is realizable once `m ≥ d+1`. So for each fixed
1148 /// `d` the orbit-type count is **independent of `m`** — the symmetric Nullstellensatz basis at degree `d` is
1149 /// `O(1)`-wide at every scale. (The *witness* of §5.3 has degree `2m−3` that grows with `m`, so its own
1150 /// orbit count grows as `Σ_{k≤m−1} p(k)`; this is the complementary fixed-degree statement.) We verify the
1151 /// count stabilizes across `m` for `d = 1, 2, 3`.
1152 #[test]
1153 fn php_symmetric_ns_width_is_constant_in_m_at_fixed_degree() {
1154 // `monomial_orbits` enumerates the 2^vars monomial cube, so keep PHP small (m ≤ 5 ⟹ ≤ 20 vars). The
1155 // orbit-type count stabilizes once m ≥ d+1 (enough pigeons/holes to realize every ≤d-edge graph type).
1156 for (d, ms) in [(1usize, vec![2, 3, 4, 5]), (2, vec![3, 4, 5]), (3, vec![4, 5])] {
1157 let counts: Vec<usize> = ms
1158 .iter()
1159 .map(|&m| {
1160 let (php, _) = crate::families::php(m);
1161 monomial_orbits(php.num_vars, d, &crate::hypercube::php_perm_symmetries(m)).len()
1162 })
1163 .collect();
1164 eprintln!("degree {d}: symmetric-NS orbit-type counts across m = {counts:?}");
1165 assert!(counts.windows(2).all(|w| w[0] == w[1]), "degree {d}: orbit-type count constant in m: {counts:?}");
1166 }
1167 }
1168
1169 /// **The uniform lower bound is FORCED BY SYMMETRY.** The witness `L = [hole-injective]` is invariant
1170 /// under PHP's automorphism group (permute pigeons × permute holes) — it is the *symmetric*
1171 /// pseudo-expectation. So the whole degree lower bound is a symmetry fact: the invariant witness collapses
1172 /// the exponential monomial basis to a handful of orbit-types (symmetry = compression = the certificate).
1173 /// We verify invariance and report the orbit compression.
1174 #[test]
1175 fn the_uniform_php_witness_is_the_symmetric_pseudo_expectation() {
1176 for m in [3usize, 4] {
1177 let (php, _) = crate::families::php(m);
1178 let holes = m - 1;
1179 let d = 2 * holes - 1;
1180 let l: BTreeSet<Mono> = (0u64..(1u64 << php.num_vars))
1181 .filter(|&mo| mo.count_ones() as usize <= d && php_is_hole_injective(mo, holes))
1182 .collect();
1183 let gens = crate::hypercube::php_perm_symmetries(m);
1184 // Invariance: every pigeon/hole permutation maps the witness onto itself.
1185 for g in &gens {
1186 for &mo in &l {
1187 assert!(l.contains(&apply_perm_to_mono(g, mo)), "PHP({m}): the uniform witness is symmetry-invariant");
1188 }
1189 }
1190 // Compression: the witness (|L| monomials) is a union of few symmetry orbit-types.
1191 let orbits = monomial_orbits(php.num_vars, d, &gens);
1192 let witness_orbits = orbits.iter().filter(|o| l.contains(&o[0])).count();
1193 eprintln!(
1194 "PHP({m}): symmetric witness = {} monomials → {} orbit-types (of {} total), compression ×{:.1}",
1195 l.len(), witness_orbits, orbits.len(), l.len() as f64 / witness_orbits as f64
1196 );
1197 assert!(witness_orbits < l.len(), "PHP({m}): symmetry compresses the witness to fewer orbit-types");
1198 }
1199 }
1200
1201 /// **A UNIFORM `∀m` degree lower bound, via a closed-form parity-aware witness.** The explicit functional
1202 /// `L(M) = [M is hole-injective]` (1 on every hole-injective monomial, 0 elsewhere) is a valid
1203 /// degree-`(2m−3)` `GF(2)` pseudo-expectation for PHP(m) — proving `NS-degree(PHP(m)) ≥ 2(m−1)` for **all
1204 /// m**, by a single argument, not a per-`m` Gaussian search:
1205 /// - the at-most-one clauses vanish because their generators are hole collisions (`L = 0`);
1206 /// - each pigeon clause gives `⟨L, m·p_C⟩ = Σ_{S⊆U} 1 = 2^{|U|} ≡ 0 (mod 2)`, and the multiplier `m` has
1207 /// degree `≤ (2m−3) − (m−1) = m−2 < holes`, so it misses ≥1 hole (`|U| ≥ 1`) — the parity that holds
1208 /// over `GF(2)` and **fails** over characteristic 0.
1209 /// Here the *explicit* `L` is re-checked at `m = 3, 4` (the argument gives every `m`). This is a genuine,
1210 /// uniform proof-complexity lower bound — a hardness result (the P ≠ NP direction), not an algorithm.
1211 #[test]
1212 fn uniform_parity_aware_witness_proves_php_degree_bound_for_all_m() {
1213 for m in [3usize, 4] {
1214 let (php, _) = crate::families::php(m);
1215 let holes = m - 1;
1216 let d = 2 * holes - 1; // 2m − 3
1217 let l: Vec<u64> = (0u64..(1u64 << php.num_vars))
1218 .filter(|&mo| mo.count_ones() as usize <= d && php_is_hole_injective(mo, holes))
1219 .collect();
1220 assert!(
1221 check_ns_lower_bound(php.num_vars, &php.clauses, d, &l),
1222 "PHP({m}): the hole-injective indicator is a valid degree-{d} pseudo-expectation ⟹ NS-degree ≥ {}",
1223 2 * holes
1224 );
1225 // Tight: it matches the measured exact degree 2(m−1).
1226 assert!(nullstellensatz_refutes(php.num_vars, &php.clauses, 2 * holes), "PHP({m}): refuted at 2(m−1)");
1227 }
1228 }
1229
1230 /// **THE BRIDGE: the symmetric certificate's *depth* equals the NS degree, exactly, `∀m`.** The naive
1231 /// guess — that the affine-symmetry *group* grows with hardness — is false and the tools prove it: PHP is
1232 /// affine-shear-*rigid* (no shears at any depth), and parity carries only a constant depth-2 shear. So the
1233 /// symmetry that governs degree is not read off the group's *width* but off the *depth of the invariant
1234 /// certificate it supports*. Define the **symmetric depth** of a family as the greatest degree `d` at which
1235 /// its symmetry-invariant pseudo-expectation is still valid (the deepest the symmetric compression reaches
1236 /// before a refutation forces it to zero). For PHP(m) that certificate is the hole-injective indicator
1237 /// (§5.3), invariant under `Sₘ × Sₘ₋₁`, and its symmetric depth is `2m−3`, while the NS degree is `2m−2` —
1238 /// so **`NS-degree(PHP(m)) = symmetric-depth(PHP(m)) + 1` for every `m`**. The `+1` is exactly the
1239 /// witness↔refutation duality unit: a degree-`d` pseudo-expectation certifies "no degree-`d` refutation," so
1240 /// the deepest surviving symmetric certificate sits precisely one below the refutation degree. This is the
1241 /// honest form of "depth tracks NS degree" — not a loose correlation but an exact identity, certified at
1242 /// `m = 3, 4` here and proven for all `m` by the uniform parity-aware witness. Symmetry-depth and
1243 /// proof-degree are one number, read twice.
1244 #[test]
1245 fn the_symmetric_certificate_depth_is_exactly_one_below_ns_degree() {
1246 for m in [3usize, 4] {
1247 let (php, _) = crate::families::php(m);
1248 let holes = m - 1;
1249 let nv = php.num_vars;
1250
1251 // NS degree = least d with a degree-d refutation (scan up).
1252 let ns_degree = (1..=nv)
1253 .find(|&d| nullstellensatz_refutes(nv, &php.clauses, d))
1254 .expect("PHP is UNSAT so some refutation degree exists");
1255
1256 // Symmetric depth = greatest d at which the invariant (hole-injective) pseudo-expectation is valid.
1257 let symmetric_depth = (0..=nv)
1258 .rev()
1259 .find(|&d| {
1260 let w: Vec<u64> = (0u64..(1u64 << nv))
1261 .filter(|&mo| mo.count_ones() as usize <= d && php_is_hole_injective(mo, holes))
1262 .collect();
1263 check_ns_lower_bound(nv, &php.clauses, d, &w)
1264 })
1265 .expect("the invariant witness is valid at some degree");
1266
1267 // The exact bridge, both sides computed independently.
1268 assert_eq!(ns_degree, 2 * (m - 1), "PHP({m}): NS degree is 2(m−1)");
1269 assert_eq!(symmetric_depth, 2 * m - 3, "PHP({m}): symmetric certificate depth is 2m−3");
1270 assert_eq!(
1271 ns_degree,
1272 symmetric_depth + 1,
1273 "PHP({m}): NS-degree = symmetric-depth + 1 — the witness↔refutation duality unit"
1274 );
1275 eprintln!("PHP({m}): symmetric-depth={symmetric_depth}, NS-degree={ns_degree} = depth+1 ✓");
1276 }
1277 }
1278
1279 /// **The symmetric-group arity grades the certificate depth (`∀m`).** The dichotomy (§4) says PHP's
1280 /// hardness is protected by *permutation* symmetry — the group `Sₘ × Sₘ₋₁`, of arity `m` (the pigeon
1281 /// count). This is the tracking that the dichotomy predicts *should* exist on the permutation side, and it
1282 /// does, exactly: the depth of the symmetry-invariant certificate (the hole-injective indicator, §5.3) is
1283 /// `2m − 3`, an exact strictly-increasing linear function of the arity — **each unit of arity buys exactly
1284 /// two units of certificate depth**. Not a correlation; a closed form, certified at `m = 3, 4` and proven
1285 /// for all `m` by the uniform witness. This is the graded law the failed shear/symplectic chases were
1286 /// groping for — on the correct axis (arity), not the wrong one (linear-symmetry weight, structurally
1287 /// pinned at 2 by the weight-2 generation of the classical groups).
1288 #[test]
1289 fn the_symmetric_group_arity_grades_the_certificate_depth() {
1290 let mut points = Vec::new();
1291 for m in [3usize, 4] {
1292 let (php, _) = crate::families::php(m);
1293 let holes = m - 1;
1294 let nv = php.num_vars;
1295 let cert_depth = (0..=nv)
1296 .rev()
1297 .find(|&d| {
1298 let w: Vec<u64> = (0u64..(1u64 << nv))
1299 .filter(|&mo| mo.count_ones() as usize <= d && php_is_hole_injective(mo, holes))
1300 .collect();
1301 check_ns_lower_bound(nv, &php.clauses, d, &w)
1302 })
1303 .expect("the invariant witness is valid at some degree");
1304 assert_eq!(cert_depth, 2 * m - 3, "arity {m}: certificate depth = 2·arity − 3");
1305 points.push((m, cert_depth));
1306 }
1307 let (m0, d0) = points[0];
1308 let (m1, d1) = points[1];
1309 assert_eq!(
1310 (d1 - d0) / (m1 - m0),
1311 2,
1312 "each unit of symmetric-group arity buys exactly two units of certificate depth"
1313 );
1314 for (m, d) in points {
1315 eprintln!("arity m={m} → certificate depth {d} = 2m−3");
1316 }
1317 }
1318
1319 /// **THE CAPSTONE — one group-theoretic number grades a machine-certified lower bound across three proof
1320 /// systems.** For `PHP_m`, a single quantity, the *arity* `m` of the protecting symmetric group `Sₘ ×
1321 /// Sₘ₋₁` (order `m!·(m−1)!`), drives — through certified chains, no trust — three independent
1322 /// proof-complexity coordinates, each computed from a *different* proof system and each strictly increasing
1323 /// with `m`:
1324 /// arity `m` → symmetric-certificate depth `2m−3` → Nullstellensatz degree `2m−2` → resolution
1325 /// width `m−1` (lower bound re-checked by a closed set).
1326 /// This is the thesis "symmetry = compression = complexity" made an *exact, executable, cross-system law*:
1327 /// the amount of symmetry (arity) determines the amount of hardness (degree, width), end to end, in the
1328 /// kernel's own currency of re-checkable certificates. By Ben-Sasson–Wigderson the growing width forces
1329 /// super-polynomial resolution *size* — the classical exponential lower bound this chain terminates in.
1330 ///
1331 /// **What this is not** — the honest boundary, stated in the theorem itself. This lives entirely in the
1332 /// *structured / symmetric* regime. As `work/PROOF_SKETCH.md` records: *"a fast algorithm for structured or
1333 /// symmetric instances says nothing; NP-hardness lives in the worst case."* The chain measures how a
1334 /// symmetry number places a *symmetric family* between the two kernel poles (trivial structure at degree 1;
1335 /// completeness at degree `n`); it says nothing about worst-case instances and is **not** a step toward P
1336 /// vs NP. The ultimate here is an exact law of the measurement science, not a resolution of the open
1337 /// problem — and it is stronger for being honest about which it is.
1338 #[test]
1339 fn the_ultimate_symmetry_to_hardness_chain_is_certified() {
1340 use crate::res_width::{
1341 check_res_width_lower_bound, min_res_width_clauses, resolution_width_closure, WidthConvention,
1342 };
1343 let mut chain = Vec::new();
1344 for m in [3usize, 4] {
1345 let (php, _) = crate::families::php(m);
1346 let holes = m - 1;
1347 let nv = php.num_vars;
1348 let group_order = (1..=m as u128).product::<u128>() * (1..=holes as u128).product::<u128>();
1349
1350 // (1) symmetric-certificate depth
1351 let cert_depth = (0..=nv)
1352 .rev()
1353 .find(|&d| {
1354 let w: Vec<u64> = (0u64..(1u64 << nv))
1355 .filter(|&mo| mo.count_ones() as usize <= d && php_is_hole_injective(mo, holes))
1356 .collect();
1357 check_ns_lower_bound(nv, &php.clauses, d, &w)
1358 })
1359 .unwrap();
1360 // (2) Nullstellensatz degree
1361 let ns_degree = (1..=nv).find(|&d| nullstellensatz_refutes(nv, &php.clauses, d)).unwrap();
1362 // (3) resolution width, with a re-checked lower-bound certificate (zero trust)
1363 let res_width = min_res_width_clauses(nv, &php.clauses, WidthConvention::WideAxioms).unwrap();
1364 let closed = resolution_width_closure(&php.clauses, res_width - 1, WidthConvention::WideAxioms);
1365 assert!(
1366 check_res_width_lower_bound(&php.clauses, res_width - 1, WidthConvention::WideAxioms, &closed),
1367 "PHP({m}): certified resolution width > {}",
1368 res_width - 1
1369 );
1370
1371 // Each coordinate is the arity, transformed by a certified chain.
1372 assert_eq!(cert_depth, 2 * m - 3, "arity {m}: certificate depth = 2m−3");
1373 assert_eq!(ns_degree, 2 * m - 2, "arity {m}: NS degree = 2m−2");
1374 assert_eq!(ns_degree, cert_depth + 1, "the witness↔refutation duality unit");
1375 assert_eq!(res_width, m - 1, "arity {m}: resolution width = m−1");
1376 chain.push((m, group_order, cert_depth, ns_degree, res_width));
1377 }
1378
1379 // One symmetry number, three proof systems, all graded strictly by the arity.
1380 let (a, b) = (chain[0], chain[1]);
1381 assert!(b.0 > a.0, "arity increases");
1382 assert!(b.2 > a.2, "certificate depth grows with arity");
1383 assert!(b.3 > a.3, "NS degree grows with arity");
1384 assert!(b.4 > a.4, "resolution width grows with arity");
1385 for (m, order, d, g, w) in chain {
1386 eprintln!(
1387 "arity m={m} (|Sₘ×Sₘ₋₁|={order}): cert-depth={d}=2m−3 NS-degree={g}=2m−2 res-width={w}=m−1 — all certified, all graded by m"
1388 );
1389 }
1390 }
1391
1392 /// **The dichotomy the exploration forced into the open, as asserted facts.** Over `GF(2)` a formula's
1393 /// symmetry is one of two *types*, and the type predicts the complexity regime: PHP carries growing NS
1394 /// degree yet is affine-shear-**rigid** (no shear automorphism at any depth up to the variable count — its
1395 /// hardness is permutation-protected), whereas a single parity block carries a nontrivial affine shear at
1396 /// constant depth 2 and no NS degree at all (it is satisfiable — Gaussian-trivial). Affine-shear symmetry
1397 /// and high NS degree are mutually exclusive here: the symmetry *type* is the complexity *type*. This is
1398 /// why "shear depth tracks degree" is the wrong bridge and [`the_symmetric_certificate_depth_is_exactly_one_below_ns_degree`]
1399 /// is the right one.
1400 #[test]
1401 fn affine_shear_symmetry_and_high_ns_degree_are_mutually_exclusive() {
1402 use crate::census::affine_composite_shear_generators;
1403 // PHP: growing NS degree, but rigid under affine shears at every depth.
1404 for m in [3usize, 4] {
1405 let (php, _) = crate::families::php(m);
1406 let nv = php.num_vars;
1407 for depth in 1..=nv.min(5) {
1408 let shears = affine_composite_shear_generators(nv, &php.clauses, depth);
1409 assert!(
1410 shears.iter().all(|(s, _)| s.len() != depth),
1411 "PHP({m}) is affine-shear-rigid — no genuine depth-{depth} shear"
1412 );
1413 }
1414 assert!(nullstellensatz_refutes(nv, &php.clauses, 2 * (m - 1)), "PHP({m}) NS degree grows to 2(m−1)");
1415 }
1416 // Parity block: a nontrivial shear appears at depth 2 (never depth 1), and it is satisfiable.
1417 for w in [3usize, 4, 5] {
1418 let vars: Vec<u32> = (0..w as u32).collect();
1419 let clauses: Vec<Vec<Lit>> = (0u32..(1 << w))
1420 .filter(|p| p.count_ones() % 2 == 1)
1421 .map(|p| (0..w).map(|i| Lit::new(vars[i], (p >> i) & 1 == 0)).collect())
1422 .collect();
1423 assert!(affine_composite_shear_generators(w, &clauses, 1).is_empty(), "parity({w}): no depth-1 shear");
1424 assert!(
1425 affine_composite_shear_generators(w, &clauses, 2).iter().any(|(s, _)| s.len() == 2),
1426 "parity({w}): a genuine depth-2 shear exists — constant, does not grow with w"
1427 );
1428 }
1429 }
1430
1431 /// **The characteristic-2 obstruction to symmetrizing a proof — why the witness must be found *natively*.**
1432 /// Over a field of characteristic 0 the Reynolds operator `L ↦ (1/|G|) Σ_{g∈G} g·L` averages *any* valid
1433 /// witness into a symmetric one, so "the extremal witness is symmetric" is free (Razborov's symmetric
1434 /// pseudo-expectation is obtained this way). Over `GF(2)` there is no `1/|G|`, and the un-normalized sum
1435 /// `Σ_{g∈G} g·L` evaluates on the constant monomial to `Σ_{g} L(g⁻¹·1) = |G|·L(1) = |G| (mod 2)` — so when
1436 /// `|G|` is **even** it *annihilates* the normalization `L(1)=1`, collapsing the witness to the zero
1437 /// functional. The pigeonhole group `Sₘ × Sₘ₋₁` has order `m!·(m−1)!`, even for every `m ≥ 2`. So symmetry
1438 /// is **not** free here: averaging destroys the witness, and the symmetric pseudo-expectation
1439 /// (the hole-injective indicator) must be exhibited *natively* — which is exactly what the parity-aware
1440 /// construction does. This is the GF(2)-specific sauce: the degree bound lives precisely where a native
1441 /// symmetric proof survives an obstruction that kills the averaged one.
1442 #[test]
1443 fn over_gf2_symmetrizing_a_proof_annihilates_when_the_group_is_even() {
1444 for m in [3usize, 4] {
1445 let (php, _) = crate::families::php(m);
1446 let holes = m - 1;
1447 let d = 2 * holes - 1;
1448 let group = close_perm_group(&crate::hypercube::php_perm_symmetries(m), php.num_vars);
1449 let expected_order: usize =
1450 (1..=m).product::<usize>() * (1..=holes).product::<usize>(); // m! · (m−1)!
1451 assert_eq!(group.len(), expected_order, "PHP({m}): |Sₘ × Sₘ₋₁| = m!·(m−1)!");
1452 assert_eq!(group.len() % 2, 0, "PHP({m}): the pigeonhole group is even");
1453
1454 // A genuine, re-checkable degree-d witness exists.
1455 let witness = ns_lower_bound_witness(php.num_vars, &php.clauses, d).expect("witness exists");
1456 assert!(check_ns_lower_bound(php.num_vars, &php.clauses, d, &witness), "the witness re-checks");
1457 assert!(witness.contains(&0u64), "a valid pseudo-expectation carries L(1)=1 (the empty monomial)");
1458
1459 // Reynolds over GF(2) annihilates it: the constant monomial is toggled |G| times ⟹ gone.
1460 let averaged = symmetrize(&witness, &group);
1461 assert!(
1462 !averaged.contains(&0u64),
1463 "PHP({m}): symmetrizing over an even group kills L(1)=1 — the averaged witness is degenerate"
1464 );
1465
1466 // Yet the NATIVE symmetric witness (hole-injective indicator) is both invariant AND valid.
1467 let native: Vec<u64> = (0u64..(1u64 << php.num_vars))
1468 .filter(|&mo| mo.count_ones() as usize <= d && php_is_hole_injective(mo, holes))
1469 .collect();
1470 assert!(check_ns_lower_bound(php.num_vars, &php.clauses, d, &native), "PHP({m}): native witness valid");
1471 let native_set: BTreeSet<Mono> = native.iter().copied().collect();
1472 for g in &group {
1473 for &mo in &native {
1474 assert!(native_set.contains(&apply_perm_to_mono(g, mo)), "PHP({m}): native witness is invariant");
1475 }
1476 }
1477 eprintln!(
1478 "PHP({m}): |G|={} (even) ⟹ averaged witness annihilated; native symmetric witness = {} monomials survives",
1479 group.len(), native.len()
1480 );
1481 }
1482 }
1483
1484 /// **A structural finding: over GF(2), the pigeonhole witness is NOT the classical matching one.** The
1485 /// restricted-basis tool is sound — with the full (all-monomial) sub-basis it reproduces a valid witness.
1486 /// We then probe the classical char-0 structure (Razborov's pseudo-expectation, supported on partial
1487 /// matchings) over GF(2): it does **not** carry the witness. The reason is a parity obstruction — the
1488 /// pigeon-clause constraint on the matching indicator reduces to `1 + (holes − |m|) ≡ 0 (mod 2)`, which
1489 /// fails for half the monomials. So the GF(2) degree lower bound needs a genuinely different (parity-aware)
1490 /// witness structure than the field-of-characteristic-0 case — an honest, non-obvious observation the tool
1491 /// surfaces, and a caution for anyone porting classical lower bounds to `GF(2)`.
1492 #[test]
1493 fn pigeonhole_witness_structure_differs_over_gf2() {
1494 for (m, deg) in [(3usize, 4usize), (4, 6)] {
1495 let (php, _) = crate::families::php(m);
1496 let holes = m - 1;
1497 // Soundness of the restricted-basis tool: the full sub-basis reproduces a valid witness.
1498 let full = ns_lower_bound_witness_on_basis(php.num_vars, &php.clauses, deg - 1, &|_| true)
1499 .expect("full-basis witness exists");
1500 assert!(check_ns_lower_bound(php.num_vars, &php.clauses, deg - 1, &full), "full-basis witness re-checks");
1501 // The classical partial-matching sub-basis does NOT carry the GF(2) witness (parity obstruction:
1502 // it also forbids pigeon repeats, which GF(2) does not require).
1503 let pm = move |mono: Mono| php_is_partial_matching(mono, holes);
1504 let on_pm = ns_lower_bound_witness_on_basis(php.num_vars, &php.clauses, deg - 1, &pm);
1505 // The correct GF(2) support is HOLE-INJECTIVE monomials — the at-most-one clauses vanish exactly
1506 // on hole collisions. This looser sub-basis DOES carry a valid, re-checkable witness.
1507 let hi = move |mono: Mono| php_is_hole_injective(mono, holes);
1508 let on_hi = ns_lower_bound_witness_on_basis(php.num_vars, &php.clauses, deg - 1, &hi);
1509 eprintln!(
1510 "PHP({m}) over GF(2): partial-matching carries witness? {} ; hole-injective? {}",
1511 on_pm.is_some(),
1512 on_hi.is_some()
1513 );
1514 assert!(on_pm.is_none(), "PHP({m}): partial-matching sub-basis fails (too strict for GF(2))");
1515 let w = on_hi.expect("hole-injective sub-basis must carry the GF(2) witness");
1516 assert!(check_ns_lower_bound(php.num_vars, &php.clauses, deg - 1, &w), "PHP({m}): hole-injective witness re-checks");
1517 assert!(w.iter().all(|&mo| php_is_hole_injective(mo, holes)), "PHP({m}): witness supported on hole-injective monomials");
1518 }
1519 }
1520
1521 fn parametric_family_has_machine_checked_degree_growth() {
1522 for n in 2..=5usize {
1523 let f_n: Vec<Vec<Lit>> = (0u64..(1u64 << n))
1524 .map(|a| (0..n as u32).map(|v| Lit::new(v, (a >> v) & 1 == 0)).collect())
1525 .collect();
1526 let w = ns_lower_bound_witness(n, &f_n, n - 1)
1527 .unwrap_or_else(|| panic!("F_{n}: a degree-(n-1) lower-bound witness must exist"));
1528 assert!(check_ns_lower_bound(n, &f_n, n - 1, &w), "F_{n}: the degree-{} lower bound re-checks", n - 1);
1529 assert!(nullstellensatz_refutes(n, &f_n, n), "F_{n}: a degree-{n} refutation exists (NS-degree = n)");
1530 assert!(!nullstellensatz_refutes(n, &f_n, n - 1), "F_{n}: no degree-(n-1) refutation (NS-degree > n-1)");
1531 }
1532 // Bounded-width data points: PHP is a COUNTING principle — incomparable to GF(2) algebra — so it is
1533 // algebraically hard for Nullstellensatz. We certify `NS-degree(PHP(m)) > 3` for m = 3 (6 vars) AND
1534 // m = 4 (12 vars), the latter with a monomial basis of 299 ≫ 63 (exercising the multi-word solver).
1535 // These are genuine, non-width-driven degree lower bounds, each a re-checkable dual witness.
1536 let probe = 3usize;
1537 for pigeons in [3usize, 4] {
1538 let (php, _) = crate::families::php(pigeons);
1539 let min_deg = (1..=probe).find(|&d| nullstellensatz_refutes(php.num_vars, &php.clauses, d));
1540 eprintln!("PHP({pigeons}): {} vars, min GF(2) NS degree (probed ≤{probe}) = {min_deg:?}", php.num_vars);
1541 let (d, msg) = match min_deg {
1542 Some(d) if d >= 2 => (d - 1, "min-1"),
1543 None => (probe, "> probe (counting is NS-hard)"),
1544 _ => continue,
1545 };
1546 let w = ns_lower_bound_witness(php.num_vars, &php.clauses, d)
1547 .unwrap_or_else(|| panic!("PHP({pigeons}): a degree-{d} lower-bound witness must exist"));
1548 assert!(check_ns_lower_bound(php.num_vars, &php.clauses, d, &w), "PHP({pigeons}): degree-{d} lower bound ({msg}) re-checks");
1549 }
1550 }
1551
1552 /// **Degree lower bounds are re-checkable certificates, dual to refutation existence.** The witness `L`
1553 /// exists *exactly* when no degree-`d` refutation does (`ns_lower_bound_witness` ⟺ `¬nullstellensatz_refutes`),
1554 /// and when present it re-checks — turning "our solver found no proof" into an independently verifiable
1555 /// lower bound `NS-degree(F) > d`. Checked on random formulas and on a family with minimum degree exactly
1556 /// `n` (the all-corners cube: a witness at `d = n−1`, none at `d = n`).
1557 #[test]
1558 fn ns_degree_lower_bounds_are_certifiable_and_dual_to_refutation() {
1559 // Consistency + re-checkability on random formulas across degrees.
1560 let mut s = 0xCAFE_F00D_1234_9999u64;
1561 let mut rng = || {
1562 s ^= s << 13;
1563 s ^= s >> 7;
1564 s ^= s << 17;
1565 s
1566 };
1567 for _ in 0..120 {
1568 let n = 3 + (rng() % 3) as usize; // 3..=5
1569 let m = 2 + (rng() % 8) as usize;
1570 let clauses: Vec<Vec<Lit>> = (0..m)
1571 .map(|_| {
1572 let mut c = Vec::new();
1573 for v in 0..n {
1574 if rng() % 2 == 0 {
1575 c.push(Lit::new(v as u32, rng() % 2 == 0));
1576 }
1577 }
1578 if c.is_empty() {
1579 c.push(Lit::new((rng() % n as u64) as u32, rng() % 2 == 0));
1580 }
1581 c
1582 })
1583 .collect();
1584 for d in 1..=n {
1585 let refutes = nullstellensatz_refutes(n, &clauses, d);
1586 match ns_lower_bound_witness(n, &clauses, d) {
1587 Some(w) => {
1588 assert!(!refutes, "a lower-bound witness exists only when there is NO degree-{d} refutation");
1589 assert!(check_ns_lower_bound(n, &clauses, d, &w), "the lower-bound witness must re-check");
1590 }
1591 None => assert!(refutes, "no witness ⟹ a degree-{d} refutation exists"),
1592 }
1593 }
1594 }
1595 // A family with minimum degree exactly n: the all-corners cube. Certified lower bound at n−1.
1596 for n in 3..=4usize {
1597 let all_corners: Vec<Vec<Lit>> = (0u64..(1u64 << n))
1598 .map(|a| (0..n as u32).map(|v| Lit::new(v, (a >> v) & 1 == 0)).collect())
1599 .collect();
1600 let w = ns_lower_bound_witness(n, &all_corners, n - 1)
1601 .expect("all-corners has no degree-(n−1) refutation — a lower bound witness exists");
1602 assert!(check_ns_lower_bound(n, &all_corners, n - 1, &w), "the degree-(n−1) lower bound re-checks");
1603 assert!(ns_lower_bound_witness(n, &all_corners, n).is_none(), "at full degree n a refutation exists");
1604 }
1605 }
1606
1607 /// **No finite randomness — but at exponential cost, which is why it is NOT P = NP.** "For finite `n`,
1608 /// random does not exist" is a *theorem*: every unsatisfiable formula over `n` variables has a degree-`≤n`
1609 /// GF(2) Nullstellensatz certificate (completeness), so nothing is structureless — verified constructively
1610 /// on the hardest object (all `2ⁿ` corners forbidden) at `n = 5, 6`. What this does NOT give is efficiency:
1611 /// the degree-`n` certificate lives in the full multilinear basis of size `2ⁿ`, so its *existence* is an
1612 /// information-theoretic fact about the finite cube, not a fast proof. P vs NP is asymptotic (about a
1613 /// family as `n → ∞`); "P = NP for finite `n`" is vacuous (a fixed finite problem is `O(1)` by table
1614 /// lookup). The honest content is exactly this gap: structure always exists, and it always costs `2ⁿ`.
1615 #[test]
1616 fn no_finite_randomness_but_the_certificate_is_exponentially_large() {
1617 // No structureless finite formula: the hardest object (every corner forbidden) is refuted at n.
1618 for n in 5..=6usize {
1619 let all_corners: Vec<Vec<Lit>> = (0u64..(1u64 << n))
1620 .map(|a| (0..n as u32).map(|v| Lit::new(v, (a >> v) & 1 == 0)).collect())
1621 .collect();
1622 assert!(build_ns_certificate(n, &all_corners).is_ok(), "n={n}: no structureless formula — a certificate exists");
1623 }
1624 // But the full-degree NS system is exactly 2ⁿ wide — the certificate exists in EXPONENTIAL space.
1625 for n in 1..=16usize {
1626 assert_eq!(nullstellensatz_basis_size(n, n), 1u128 << n, "the degree-n NS basis is exactly 2ⁿ");
1627 // Monotone in the degree budget; the cheap (bounded-degree) fragment is a vanishing slice at large n.
1628 assert!(nullstellensatz_basis_size(n, n / 2) <= nullstellensatz_basis_size(n, n), "basis grows with degree");
1629 }
1630 // Exponential, not polynomial: by n=40 the certificate space (2⁴⁰ ≈ 10¹²) dwarfs any fixed poly like n⁶.
1631 assert!(nullstellensatz_basis_size(40, 40) > 40u128.pow(6), "the certificate space is exponential, not polynomial");
1632 }
1633
1634 /// **We PROVE, we do not iterate: structureless = 0 past the census wall.** The `Bₙ`-orbit census is
1635 /// infeasible at `n = 5` (~10⁷ orbits); it can only *measure* `structureless = 0` up to `n = 4`. The
1636 /// uniform construction settles it by *proof*: at `n = 5, 6, 7` it is a total, certifying decision on
1637 /// hundreds of random formulas — every UNSAT instance gets a certificate that re-checks (degree ≤ n) and
1638 /// is genuinely modelless by brute force; every SAT instance gets a model that satisfies it. One
1639 /// construction, correct at every `n`: no minimal-UNSAT family is ever structureless.
1640 #[test]
1641 fn ns_construction_is_total_and_sound_past_the_census_wall() {
1642 let mut state = 0x1234_5678_9abc_def0u64;
1643 let mut rng = || {
1644 state ^= state << 13;
1645 state ^= state >> 7;
1646 state ^= state << 17;
1647 state
1648 };
1649 for &n in &[5usize, 6, 7] {
1650 for _ in 0..150 {
1651 let num_clauses = n + (rng() % (3 * n as u64)) as usize;
1652 let clauses: Vec<Vec<Lit>> = (0..num_clauses)
1653 .map(|_| {
1654 let width = 2 + (rng() % 2) as usize; // 2- or 3-clauses
1655 let mut seen = std::collections::HashSet::new();
1656 let mut c = Vec::new();
1657 while c.len() < width {
1658 let v = (rng() % n as u64) as u32;
1659 if seen.insert(v) {
1660 c.push(Lit::new(v, rng() & 1 == 0));
1661 }
1662 }
1663 c
1664 })
1665 .collect();
1666 match build_ns_certificate(n, &clauses) {
1667 Ok(cert) => {
1668 assert!(cert.verify(&clauses), "n={n}: the constructive certificate must re-check");
1669 assert!(cert.degree() <= n, "n={n}: the certificate degree must be ≤ n");
1670 assert!(!sat(n, &clauses), "n={n}: a certificate is issued only for a genuinely UNSAT formula");
1671 }
1672 Err(model) => {
1673 assert!(sat(n, &clauses), "n={n}: SAT witness ⟹ the formula is genuinely satisfiable");
1674 assert!(
1675 clauses.iter().all(|c| c.iter().any(|l| model[l.var() as usize] == l.is_positive())),
1676 "n={n}: the returned model must satisfy every clause"
1677 );
1678 }
1679 }
1680 }
1681 }
1682 }
1683
1684 /// **Truly random can't live in a finite hypercube — the complexity is *capped by the dimension*.**
1685 /// True (Martin-Löf) randomness needs *unbounded* Kolmogorov complexity. But the `n`-cube is finite,
1686 /// and Nullstellensatz is **complete at degree `n`** — every unsatisfiable formula over `n` variables
1687 /// has its `1`-in-the-ideal certificate by degree `n`, no higher. So the proof-complexity (the
1688 /// "randomness" measure) is **bounded by `n`**, never unbounded. The cube holds only the *finite
1689 /// shadow* of randomness — incompressible-relative-to-size, capped at the dimension. Truly random,
1690 /// being unbounded, has no room. We verify the cap on the maximally-constrained ("most random") UNSAT
1691 /// formula: it is decided exactly at degree `n`.
1692 #[test]
1693 fn truly_random_cannot_live_in_a_finite_hypercube() {
1694 for nv in 3..=5 {
1695 // The hardest object: every one of the 2ⁿ assignments forbidden by a full-width clause.
1696 let mut cl: Vec<Vec<Lit>> = Vec::new();
1697 for a in 0..(1u32 << nv) {
1698 cl.push((0..nv as u32).map(|v| Lit::new(v, (a >> v) & 1 == 0)).collect());
1699 }
1700 assert!(!sat(nv, &cl), "all assignments forbidden ⟹ UNSAT");
1701 // It is decided at the dimension-bound degree n — the cap. No formula over n vars needs more.
1702 assert!(
1703 nullstellensatz_refutes(nv, &cl, nv),
1704 "the hardest n={nv} formula is refuted at degree n — complexity capped at the dimension"
1705 );
1706 }
1707 }
1708
1709 /// **Symmetry collapses Polynomial Calculus to a counting problem — and for pigeonhole it *is* the
1710 /// pigeonhole count.** The degree-2 monomial basis of PHP(n), `Θ(n⁴)` monomials, collapses under the
1711 /// grid group `Sₙ × Sₙ₋₁` to a handful of orbit-types — and that handful is *constant in n*. The
1712 /// symmetric Nullstellensatz system has one column per orbit, so it is `O(1)` wide regardless of
1713 /// scale; counting those orbit-types is the whole cut. Everything came down to counting.
1714 #[test]
1715 fn symmetry_collapses_the_pc_basis_to_a_counting_problem() {
1716 let mut orbit_counts = Vec::new();
1717 for n in 3..=5 {
1718 let (cnf, _) = crate::families::php(n);
1719 let nv = cnf.num_vars;
1720 let generators = crate::hypercube::php_perm_symmetries(n);
1721 let orbits = monomial_orbits(nv, 2, &generators);
1722 let full = 1 + nv + nv * (nv - 1) / 2; // C(nv,0)+C(nv,1)+C(nv,2)
1723
1724 // The basis collapses hard — orbit-types are a tiny fraction of the monomials.
1725 assert!(
1726 orbits.len() * 3 < full,
1727 "PHP({n}): {} orbit-types ≪ {full} monomials — the counting collapse",
1728 orbits.len()
1729 );
1730 // Burnside check: the orbits partition the basis exactly (counting is consistent).
1731 assert_eq!(
1732 orbits.iter().map(|o| o.len()).sum::<usize>(),
1733 full,
1734 "the orbits partition the monomial basis"
1735 );
1736 // Each orbit is closed under the symmetry.
1737 for orbit in &orbits {
1738 let set: BTreeSet<Mono> = orbit.iter().copied().collect();
1739 for &m in orbit {
1740 for g in &generators {
1741 assert!(set.contains(&apply_perm_to_mono(g, m)), "orbit closed under the group");
1742 }
1743 }
1744 }
1745 orbit_counts.push(orbits.len());
1746 }
1747 // The orbit-type count is essentially constant in n — O(1) symmetric NS width, at every scale.
1748 let max = *orbit_counts.iter().max().unwrap();
1749 let min = *orbit_counts.iter().min().unwrap();
1750 assert!(max - min <= 2, "orbit-type count is ~constant in n: {orbit_counts:?}");
1751 }
1752
1753 /// **THE SYMMETRY COST-CUT: `2ⁿ → n+1`, exponential to linear.** The full-degree Nullstellensatz basis is
1754 /// `2ⁿ` monomials — the brute-force cost of finding structure. But when a formula is *fully symmetric*
1755 /// (the symmetric group `Sₙ` permutes its variables), an invariant certificate is constant on the monomial
1756 /// orbits, and under `Sₙ` two monomials are equivalent iff they have the same degree — so the `2ⁿ` basis
1757 /// collapses to exactly **`n+1` orbit-columns, one per degree**. The Gaussian runs over `n+1` columns, not
1758 /// `2ⁿ`. That is symmetry = compression, made exact: the exponential search becomes linear. The reduced
1759 /// certificate is *sound* (an invariant `1`-in-the-span is a genuine refutation), checked here on a
1760 /// fully-`Sₙ`-symmetric UNSAT family.
1761 #[test]
1762 fn symmetry_cuts_the_full_ns_basis_from_exponential_to_linear() {
1763 for n in 2..=8usize {
1764 let gens = symmetric_group_generators(n);
1765 let orbits = monomial_orbits(n, n, &gens);
1766 // Under Sₙ, the 2ⁿ full-degree basis collapses to exactly n+1 orbits — one per monomial degree.
1767 assert_eq!(orbits.len(), n + 1, "Sₙ collapses the 2ⁿ basis to n+1 degree-orbits (n={n})");
1768 assert_eq!(nullstellensatz_basis_size(n, n), 1u128 << n, "the full basis is 2ⁿ");
1769 // Each orbit is *all* monomials of one degree, so the orbit sizes are exactly {C(n,k)}.
1770 let mut sizes: Vec<u128> = orbits.iter().map(|o| o.len() as u128).collect();
1771 sizes.sort_unstable();
1772 let mut binoms: Vec<u128> = (0..=n).map(|k| binom(n, k)).collect();
1773 binoms.sort_unstable();
1774 assert_eq!(sizes, binoms, "each degree-orbit k has C(n,k) monomials");
1775 }
1776 // The cut deepens without bound: 2ⁿ / (n+1) → ∞. Symmetry turns the exponential basis linear.
1777 let cut = |n: u32| (1u128 << n) / (n as u128 + 1);
1778 assert!(cut(8) > cut(4) && cut(4) > cut(2), "the symmetry cut 2ⁿ/(n+1) grows with n");
1779
1780 // SOUNDNESS of the collapsed certificate: on a fully-Sₙ-symmetric UNSAT family (every corner
1781 // forbidden), the symmetry-reduced NS — over the n+1 orbit columns, not 2ⁿ — still refutes, and agrees
1782 // with full NS. The cheap certificate is a real one.
1783 for n in 2..=5usize {
1784 let all_corners: Vec<Vec<Lit>> = (0u64..(1u64 << n))
1785 .map(|a| (0..n as u32).map(|v| Lit::new(v, (a >> v) & 1 == 0)).collect())
1786 .collect();
1787 let gens = symmetric_group_generators(n);
1788 assert!(nullstellensatz_refutes(n, &all_corners, n), "full NS refutes all-corners at degree n");
1789 assert!(
1790 nullstellensatz_refutes_symmetric(n, &all_corners, n, &gens),
1791 "the symmetry-reduced NS (n+1 columns) still refutes — the 2ⁿ→n+1 cut is sound (n={n})"
1792 );
1793 }
1794 }
1795
1796 /// **Nullstellensatz at full degree is a complete, sound decision** — verified against brute force on
1797 /// a fuzz: a degree-`num_vars` refutation exists iff the formula is unsatisfiable.
1798 #[test]
1799 fn nullstellensatz_full_degree_matches_brute_force() {
1800 fn sm(s: &mut u64) -> u64 {
1801 *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
1802 let mut z = *s;
1803 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
1804 z ^ (z >> 31)
1805 }
1806 let mut state = 0x9015_0001u64;
1807 for _ in 0..60 {
1808 let nv = 3 + (sm(&mut state) % 3) as usize; // 3..5 vars
1809 let m = 3 + (sm(&mut state) % 8) as usize;
1810 let mut cl: Vec<Vec<Lit>> = Vec::new();
1811 for _ in 0..m {
1812 let mut c = Vec::new();
1813 for v in 0..nv {
1814 if sm(&mut state) % 2 == 0 {
1815 c.push(Lit::new(v as u32, sm(&mut state) % 2 == 0));
1816 }
1817 }
1818 if !c.is_empty() {
1819 cl.push(c);
1820 }
1821 }
1822 if cl.is_empty() {
1823 continue;
1824 }
1825 let unsat = !sat(nv, &cl);
1826 assert_eq!(
1827 nullstellensatz_refutes(nv, &cl, nv),
1828 unsat,
1829 "NS at full degree must decide exactly: {cl:?}"
1830 );
1831 }
1832 }
1833
1834 /// **Parity is the degree-1 fragment.** An odd XOR cycle (`x_i ≠ x_{i+1}` around a 5-cycle) is
1835 /// unsatisfiable, and Nullstellensatz refutes it at the low degree its width permits — the algebraic
1836 /// form of the parity cut, now inside the one general engine.
1837 #[test]
1838 fn nullstellensatz_refutes_parity_at_low_degree() {
1839 // 2-colouring an odd cycle: (x_u ∨ x_v) ∧ (¬x_u ∨ ¬x_v) per edge — UNSAT for an odd cycle.
1840 let edges = [(0u32, 1u32), (1, 2), (2, 3), (3, 4), (4, 0)];
1841 let mut cl = Vec::new();
1842 for (u, v) in edges {
1843 cl.push(vec![Lit::new(u, true), Lit::new(v, true)]);
1844 cl.push(vec![Lit::new(u, false), Lit::new(v, false)]);
1845 }
1846 assert!(!sat(5, &cl), "odd-cycle 2-colouring is UNSAT");
1847 assert!(nullstellensatz_refutes(5, &cl, 2), "NS refutes the parity obstruction at degree 2");
1848 }
1849
1850 /// **The degree dial has real teeth: a refutation can need more than the minimum.** A formula that is
1851 /// UNSAT but whose only Nullstellensatz certificate needs the full degree is *not* refuted below it —
1852 /// the engine honestly reports "no low-degree algebraic proof," which is the hardness signal.
1853 #[test]
1854 fn the_degree_is_a_genuine_power_dial() {
1855 // All eight 3-clauses over x0,x1,x2 forbidding each assignment ⟹ UNSAT; needs degree 3 (the width).
1856 let mut cl = Vec::new();
1857 for a in 0u32..8 {
1858 cl.push(
1859 (0..3u32)
1860 .map(|v| Lit::new(v, (a >> v) & 1 == 0))
1861 .collect::<Vec<Lit>>(),
1862 );
1863 }
1864 assert!(!sat(3, &cl), "all 8 assignments forbidden ⟹ UNSAT");
1865 assert!(!nullstellensatz_refutes(3, &cl, 2), "no degree-2 certificate — width-3 clauses unusable");
1866 assert!(nullstellensatz_refutes(3, &cl, 3), "degree 3 refutes it");
1867 }
1868
1869 /// Soundness, isolated: a satisfiable formula has **no** Nullstellensatz refutation at any degree.
1870 #[test]
1871 fn satisfiable_formulas_are_never_refuted() {
1872 let cl = vec![vec![Lit::new(0, true), Lit::new(1, true)], vec![Lit::new(0, false), Lit::new(2, true)]];
1873 assert!(sat(3, &cl));
1874 for d in 0..=3 {
1875 assert!(!nullstellensatz_refutes(3, &cl, d), "a SAT formula is refuted at no degree (d={d})");
1876 }
1877 }
1878
1879 /// **Symmetry-reduced Nullstellensatz collapses the basis and stays sound.** The "all assignments
1880 /// forbidden" instance is fully symmetric and UNSAT; full NS spans `2ⁿ` monomials, but the
1881 /// symmetry-reduced refutation runs over the `n+1` weight-class orbits and still refutes — an
1882 /// exponential column collapse. Soundness: a satisfiable symmetric formula is refuted at no degree.
1883 #[test]
1884 fn symmetry_reduced_nullstellensatz_collapses_and_is_sound() {
1885 for nv in 3..=5usize {
1886 let mut cl: Vec<Vec<Lit>> = Vec::new();
1887 for a in 0..(1u32 << nv) {
1888 cl.push((0..nv as u32).map(|v| Lit::new(v, (a >> v) & 1 == 0)).collect());
1889 }
1890 assert!(!sat(nv, &cl), "all assignments forbidden ⟹ UNSAT");
1891 let gens = crate::symmetry_detect::find_generators(nv, &cl);
1892 assert!(
1893 nullstellensatz_refutes_symmetric(nv, &cl, nv, &gens),
1894 "symmetry-reduced NS refutes the all-forbidden instance (n={nv})"
1895 );
1896 let cols = monomial_orbits(nv, nv, &gens).len();
1897 assert!(cols < (1usize << nv), "n={nv}: {cols} orbit columns ≪ {} monomials", 1usize << nv);
1898 }
1899 // Soundness: a satisfiable symmetric formula is refuted at no degree.
1900 let sat_cl = vec![vec![Lit::new(0, true), Lit::new(1, true), Lit::new(2, true)]];
1901 let gens = crate::symmetry_detect::find_generators(3, &sat_cl);
1902 for d in 0..=3 {
1903 assert!(
1904 !nullstellensatz_refutes_symmetric(3, &sat_cl, d, &gens),
1905 "a satisfiable formula has no symmetry-reduced refutation (d={d})"
1906 );
1907 }
1908 }
1909
1910 /// A deterministic LCG for reproducible fuzz corpora inside tests (no `rand`, no wall clock).
1911 fn lcg(state: &mut u64) -> u64 {
1912 *state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
1913 *state >> 33
1914 }
1915
1916 /// **The polynomial-generator NS engine agrees with the clause engine on CNFs.** `ns_refutes_polys`
1917 /// / `ns_lower_bound_witness_polys` / `check_ns_lower_bound_polys` generalize the clause API to
1918 /// arbitrary `GF(2)` generator systems (the substrate for the linear encoding and the symmetric-family
1919 /// machinery). Fed the clause polynomials of a CNF they must reproduce the clause engine exactly —
1920 /// refutability at every degree, witness existence (the duality `refutes(d) ⟺ no witness at d`), and
1921 /// cross-checker validation both ways. Corpus: pigeonhole, modular counting, a transitive-XOR core,
1922 /// and a deterministic random-3-CNF fuzz sweep.
1923 #[test]
1924 fn ns_over_polynomial_generators_agrees_with_the_clause_encoding_on_cnfs() {
1925 let mut corpus: Vec<(usize, Vec<Vec<Lit>>)> = Vec::new();
1926 let (php3, _) = crate::families::php(3);
1927 corpus.push((php3.num_vars, php3.clauses));
1928 let (cnt32, _) = crate::families::mod_counting(3, 2);
1929 corpus.push((cnt32.num_vars, cnt32.clauses));
1930 let (cnt42, _) = crate::families::mod_counting(4, 2);
1931 corpus.push((cnt42.num_vars, cnt42.clauses));
1932 let p = |v: u32| Lit::pos(v);
1933 let q = |v: u32| Lit::neg(v);
1934 corpus.push((3, vec![
1935 vec![q(0), p(1)], vec![p(0), q(1)],
1936 vec![q(1), p(2)], vec![p(1), q(2)],
1937 vec![p(0), p(2)], vec![q(0), q(2)],
1938 ]));
1939 let mut seed = 0x5EED_CAFE_u64;
1940 for _ in 0..24 {
1941 let nv = 4 + (lcg(&mut seed) % 3) as usize; // 4..=6 variables
1942 let nc = 6 + (lcg(&mut seed) % 12) as usize;
1943 let mut cl = Vec::new();
1944 for _ in 0..nc {
1945 let mut vars: Vec<u32> = Vec::new();
1946 while vars.len() < 3 {
1947 let v = (lcg(&mut seed) % nv as u64) as u32;
1948 if !vars.contains(&v) {
1949 vars.push(v);
1950 }
1951 }
1952 cl.push(vars.iter().map(|&v| Lit::new(v, lcg(&mut seed) & 1 == 1)).collect());
1953 }
1954 corpus.push((nv, cl));
1955 }
1956
1957 for (nv, clauses) in &corpus {
1958 let gens: Vec<Poly> = clauses.iter().map(|c| clause_polynomial(c)).collect();
1959 for d in 1..=(*nv).min(5) {
1960 let clause_verdict = nullstellensatz_refutes(*nv, clauses, d);
1961 assert_eq!(
1962 ns_refutes_polys(*nv, &gens, d),
1963 clause_verdict,
1964 "n={nv} d={d}: the polynomial-generator engine matches the clause engine"
1965 );
1966 let w_clause = ns_lower_bound_witness(*nv, clauses, d);
1967 let w_polys = ns_lower_bound_witness_polys(*nv, &gens, d);
1968 assert_eq!(
1969 w_clause.is_some(),
1970 w_polys.is_some(),
1971 "n={nv} d={d}: witness existence agrees across the two engines"
1972 );
1973 assert_eq!(
1974 w_polys.is_none(),
1975 clause_verdict,
1976 "n={nv} d={d}: duality — a refutation at d exists iff no degree-d pseudo-expectation"
1977 );
1978 if let (Some(wc), Some(wp)) = (w_clause, w_polys) {
1979 assert!(
1980 check_ns_lower_bound_polys(*nv, &gens, d, &wc),
1981 "n={nv} d={d}: the clause-engine witness passes the polynomial checker"
1982 );
1983 assert!(
1984 check_ns_lower_bound(*nv, clauses, d, &wp),
1985 "n={nv} d={d}: the polynomial-engine witness passes the clause checker"
1986 );
1987 }
1988 }
1989 }
1990 }
1991
1992 /// **Degree-bounded monomial enumeration lifts the 20-variable cap.** The clause engine enumerates
1993 /// `0..2ⁿ` and filters — dead at `n > 20`. `monomials_up_to_degree` walks only the `C(n, ≤d)`
1994 /// monomials, so fixed-degree work scales to the full `Mono = u64` range. Differential against the
1995 /// filter on every `n ≤ 12`; counts pinned to `nullstellensatz_basis_size`; and the real payoff
1996 /// exercised end-to-end: a re-checked degree-2 lower-bound witness on a 22-variable instance (PHP(3)
1997 /// padded with sixteen spectator variables — the ideal, hence the NS degree, is unchanged), where the
1998 /// bounded basis has 254 monomials against the 4-million-corner cube.
1999 #[test]
2000 fn degree_bounded_monomial_enumeration_scales_past_twenty_variables() {
2001 for n in 0..=12usize {
2002 for d in 0..=n {
2003 let bounded: BTreeSet<Mono> = monomials_up_to_degree(n, d).into_iter().collect();
2004 let filtered: BTreeSet<Mono> =
2005 (0u64..(1u64 << n)).filter(|m| m.count_ones() as usize <= d).collect();
2006 assert_eq!(bounded, filtered, "n={n} d={d}: bounded enumeration = filtered cube");
2007 assert_eq!(
2008 bounded.len() as u128,
2009 nullstellensatz_basis_size(n, d),
2010 "n={n} d={d}: the count is Σ C(n,k)"
2011 );
2012 }
2013 }
2014 // Sorted ascending — the stable index order the witness machinery relies on.
2015 let ms = monomials_up_to_degree(10, 3);
2016 assert!(ms.windows(2).all(|w| w[0] < w[1]), "monomials come sorted ascending");
2017 // Far past the cube: C(40, ≤2) monomials without touching 2^40.
2018 assert_eq!(monomials_up_to_degree(40, 2).len(), 821); // 1 + 40 + C(40,2)
2019 assert_eq!(nullstellensatz_basis_size(40, 2), 821);
2020
2021 // End-to-end at 22 variables: PHP(3) with spectator variables. NS-degree(PHP(3)) = 4, so a
2022 // degree-2 pseudo-expectation exists — found and re-checked entirely on the bounded basis.
2023 let (php3, _) = crate::families::php(3);
2024 let gens: Vec<Poly> = php3.clauses.iter().map(|c| clause_polynomial(c)).collect();
2025 let nv = 22usize;
2026 let w = ns_lower_bound_witness_polys(nv, &gens, 2)
2027 .expect("a degree-2 witness exists for padded PHP(3) — the degree is 4");
2028 assert!(
2029 check_ns_lower_bound_polys(nv, &gens, 2, &w),
2030 "the 22-variable witness re-checks on the bounded basis"
2031 );
2032 // Spectators change nothing: the same verdicts as the unpadded instance.
2033 assert!(!ns_refutes_polys(nv, &gens, 3), "padded PHP(3) is not refuted at degree 3");
2034 assert!(ns_refutes_polys(nv, &gens, 4), "padded PHP(3) is refuted at degree 4");
2035 }
2036
2037 /// **The clause and linear encodings interreduce at bounded degree — with an asymmetric, degree-exact
2038 /// direction.** For an exactly-one group `G` (at-least-one clause + at-most-one pairs), the wide ALO
2039 /// clause polynomial `Π_{v∈G}(1+x_v)` and the linear generator `1 + Σ_{v∈G} x_v` differ by a sum of
2040 /// pair-generator multiples: every `≥2`-subset monomial is a multiple of some pair `x_u x_v`. So:
2041 /// (i) the syntactic identity `Π(1+x) + (1+Σx) ∈ span{m·(x_u x_v)}` at degree `|G|`, exactly;
2042 /// (ii) **clause-refutable at `d` ⟹ linear-refutable at `d`** (degree-preserving — every clause
2043 /// generator rewrites into linear generators of no larger degree), which is why lower bounds
2044 /// proven against the linear encoding are the stronger statements (the literature-standard form);
2045 /// (iii) linear-refutable at `d` ⟹ clause-refutable at `d + k−1` (`k` = the widest group) — the
2046 /// honest reverse transfer with its explicit degree tax.
2047 #[test]
2048 fn the_linear_and_clause_encodings_interreduce_at_bounded_degree() {
2049 // (i) The syntactic identity, exactly, for group sizes 2..=6.
2050 for k in 2..=6usize {
2051 let group: Vec<u32> = (0..k as u32).collect();
2052 let alo: Vec<Lit> = group.iter().map(|&v| Lit::pos(v)).collect();
2053 let clause_poly = clause_polynomial(&alo);
2054 let gens = exactly_one_linear_generators(&[group.clone()]);
2055 let linear: &Poly = &gens[0]; // the 1 + Σ x_v generator leads; pairs follow
2056 assert_eq!(linear.len(), k + 1, "the linear generator is 1 + Σ x (k+1 monomials)");
2057 assert_eq!(gens.len(), 1 + k * (k - 1) / 2, "one linear generator plus C(k,2) pairs");
2058 let mut diff = clause_poly.clone();
2059 for &m in linear {
2060 toggle(&mut diff, m);
2061 }
2062 // diff = Σ_{|S|≥2} x_S must lie in span{ m · pair : deg ≤ k }.
2063 let mut index: HashMap<Mono, usize> = HashMap::new();
2064 for m in 0u64..(1u64 << k) {
2065 let i = index.len();
2066 index.insert(m, i);
2067 }
2068 let words = index.len().div_ceil(64).max(1);
2069 let to_bits = |p: &Poly| -> Vec<u64> {
2070 let mut b = vec![0u64; words];
2071 for &m in p {
2072 b[index[&m] / 64] |= 1 << (index[&m] % 64);
2073 }
2074 b
2075 };
2076 let mut rows = Vec::new();
2077 for pair in &gens[1..] {
2078 for m in monomials_up_to_degree(k, k) {
2079 let prod = poly_mul_mono(pair, m);
2080 if !prod.is_empty() && poly_degree(&prod) <= k {
2081 rows.push(to_bits(&prod));
2082 }
2083 }
2084 }
2085 assert!(
2086 in_gf2_span(rows, &to_bits(&diff)),
2087 "k={k}: Π(1+x) + (1+Σx) is a sum of pair-generator multiples at degree k"
2088 );
2089 }
2090
2091 // (ii)+(iii) The semantic transfer, measured on modular counting (the W2 substrate).
2092 for (n, q) in [(3usize, 2usize), (5, 2)] {
2093 let (cnf, _) = crate::families::mod_counting(n, q);
2094 let nv = cnf.num_vars;
2095 // The exactly-one groups are the all-positive covering clauses; AMO pairs are the rest.
2096 let groups: Vec<Vec<u32>> = cnf
2097 .clauses
2098 .iter()
2099 .filter(|c| c.iter().all(|l| l.is_positive()))
2100 .map(|c| c.iter().map(|l| l.var()).collect())
2101 .collect();
2102 assert_eq!(groups.len(), n, "one exactly-one group per point");
2103 let k = groups.iter().map(|g| g.len()).max().unwrap();
2104 let linear_gens = exactly_one_linear_generators(&groups);
2105 let clause_gens: Vec<Poly> = cnf.clauses.iter().map(|c| clause_polynomial(c)).collect();
2106 let dmax = nv.min(5);
2107 for d in 1..=dmax {
2108 if ns_refutes_polys(nv, &clause_gens, d) {
2109 assert!(
2110 ns_refutes_polys(nv, &linear_gens, d),
2111 "Count_{q}({n}) d={d}: clause-refutable ⟹ linear-refutable at the SAME degree"
2112 );
2113 }
2114 if ns_refutes_polys(nv, &linear_gens, d) {
2115 assert!(
2116 ns_refutes_polys(nv, &clause_gens, (d + k - 1).min(nv)),
2117 "Count_{q}({n}) d={d}: linear-refutable ⟹ clause-refutable at d + k − 1"
2118 );
2119 }
2120 }
2121 // The linear encoding refutes an UNSAT counting instance at low degree somewhere ≤ dmax.
2122 assert!(
2123 (1..=dmax).any(|d| ns_refutes_polys(nv, &linear_gens, d)),
2124 "Count_{q}({n}): the linear encoding refutes within the probed degrees"
2125 );
2126 }
2127 }
2128
2129 /// **The char-matched control: `Count_2` collapses to degree 1 over `GF(2)`.** The linear encoding's
2130 /// point generators are `GF(2)` linear equations `1 + Σ_{e∋i} x_e`; summing all `n` of them counts
2131 /// each edge `q = 2` times, so the edge terms cancel and the sum is `n·1 = 1` for odd `n` — a
2132 /// degree-**1** Nullstellensatz refutation, pure linear algebra. This is the characteristic-matched
2133 /// foil for the mismatch row: the same family that is resolution-hard (Ajtai; Beame–Pitassi) is
2134 /// trivial for the algebra whose characteristic divides the count. Even `n` is SAT (a perfect
2135 /// matching exists), and soundness holds: no refutation at any probed degree.
2136 #[test]
2137 fn count_two_is_char_matched_and_falls_to_low_degree_gf2_ns() {
2138 for n in [3usize, 5, 7] {
2139 let (cnf, _) = crate::families::mod_counting(n, 2);
2140 let gens = exactly_one_linear_generators(&crate::families::mod_counting_groups(n, 2));
2141 assert!(
2142 ns_refutes_polys(cnf.num_vars, &gens, 1),
2143 "Count_2({n}), n odd: linear-encoded NS degree 1 — the char-matched collapse"
2144 );
2145 }
2146 for n in [4usize, 6] {
2147 let (cnf, _) = crate::families::mod_counting(n, 2);
2148 let gens = exactly_one_linear_generators(&crate::families::mod_counting_groups(n, 2));
2149 for d in 1..=3 {
2150 assert!(
2151 !ns_refutes_polys(cnf.num_vars, &gens, d),
2152 "Count_2({n}), n even: SAT (a perfect matching) ⟹ no refutation at degree {d}"
2153 );
2154 }
2155 }
2156 }
2157
2158 /// **The char-mismatch row: `Count_3` has certified, growing, non-width `GF(2)` NS degree — in two
2159 /// provably distinct regimes.** The modular counting principle with `3 ∤ n` is UNSAT by a mod-3
2160 /// argument `GF(2)` algebra cannot make at low degree; on the **linear encoding** (degree-1 point
2161 /// generators `P_i = 1 + Σ_{e∋i} x_e` + degree-2 overlap pairs — the encoding the literature states
2162 /// bounds against, and the *stronger* side of the interreduction):
2163 ///
2164 /// - **The dense degenerate regime `n < 2q` (`n = 4, 5`): exact degree 2.** Below `n = 6` every two
2165 /// triples of `[n]` intersect, so for any `f` and `i ∉ f`, `x_f·P_i ≡ x_f` mod the pairs — every
2166 /// variable enters the degree-2 span, and with `Σ_i P_i = n + Σ_e x_e (mod 2)` (each edge counted
2167 /// `q = 3 ≡ 1` times) the constant `1` follows. Degree 1 is impossible: a sum `Σ_{i∈S} P_i = 1`
2168 /// needs `|e ∩ S|` even for every triple `e` with `|S|` odd, and no such `S ⊆ [n]` exists (a
2169 /// singleton meets some triple once; a triple meets itself thrice). Both halves certified.
2170 /// - **The genuine regime (`n = 7, 8` — 35 and 56 variables, reachable only through the
2171 /// degree-bounded basis): NS-degree ≥ 3, certified.** The re-checked dual witness at degree 2
2172 /// exceeds every generator's degree (non-width) and the bound grows from the degenerate regime's
2173 /// 2. The exact upper half (refutation at degree 3) is release-scale Gaussian elimination and
2174 /// lives in the `#[ignore]` scale probe, measured `= 3` at both `n`.
2175 ///
2176 /// The `GF(3)` route refutes the same family in microseconds — together the marquee two-sided
2177 /// characteristic-mismatch row of the separations atlas.
2178 #[test]
2179 fn count_three_has_certified_growing_non_width_ns_degree_over_gf2() {
2180 // Dense degenerate regime: exact degree 2, both halves certified.
2181 for n in [4usize, 5] {
2182 let (cnf, _) = crate::families::mod_counting(n, 3);
2183 let nv = cnf.num_vars;
2184 let gens = exactly_one_linear_generators(&crate::families::mod_counting_groups(n, 3));
2185 assert!(!ns_refutes_polys(nv, &gens, 1), "Count_3({n}): no degree-1 refutation");
2186 let w1 = ns_lower_bound_witness_polys(nv, &gens, 1).expect("dual witness at degree 1");
2187 assert!(check_ns_lower_bound_polys(nv, &gens, 1, &w1), "Count_3({n}): NS-degree > 1 re-checks");
2188 assert!(
2189 ns_refutes_polys(nv, &gens, 2),
2190 "Count_3({n}), n < 2q: every two blocks overlap ⟹ the degree-2 collapse"
2191 );
2192 eprintln!("Count_3({n}): certified exact linear-encoded GF(2) NS degree = 2 (dense regime)");
2193 }
2194 // Genuine regime: certified NS-degree ≥ 3 — non-width, and growth past the dense regime.
2195 for n in [7usize, 8] {
2196 let (cnf, _) = crate::families::mod_counting(n, 3);
2197 let nv = cnf.num_vars;
2198 assert!(nv > 20, "the genuine regime lives past the clause engine's cap ({nv} vars)");
2199 let gens = exactly_one_linear_generators(&crate::families::mod_counting_groups(n, 3));
2200 let w2 = ns_lower_bound_witness_polys(nv, &gens, 2)
2201 .expect("a degree-2 pseudo-expectation exists — the degree exceeds 2");
2202 assert!(
2203 check_ns_lower_bound_polys(nv, &gens, 2, &w2),
2204 "Count_3({n}): NS-degree ≥ 3 re-checks with zero trust"
2205 );
2206 eprintln!("Count_3({n}) [{nv} vars]: certified NS-degree ≥ 3 (exact = 3 in the scale probe)");
2207 }
2208 }
2209
2210 /// **The exact upper half at scale: `Count_3(7)` and `Count_3(8)` refute at degree 3, exactly.**
2211 /// The certified test carries the lower half (re-checked degree-2 dual witnesses); this probe pins
2212 /// the refutations at degree 3 — 35- and 56-variable Gaussian eliminations over the degree-bounded
2213 /// basis, minutes at test-profile optimization — locking the exact linear-encoded degree
2214 /// `NS-degree(Count_3(7)) = NS-degree(Count_3(8)) = 3`.
2215 #[test]
2216 #[ignore = "scale measurement — minutes of Gaussian elimination; run explicitly or via the fast suite"]
2217 fn count_three_scale_probe_measures_the_degree_growth() {
2218 for n in [7usize, 8] {
2219 let (cnf, _) = crate::families::mod_counting(n, 3);
2220 let nv = cnf.num_vars;
2221 let gens = exactly_one_linear_generators(&crate::families::mod_counting_groups(n, 3));
2222 assert!(!ns_refutes_polys(nv, &gens, 2), "Count_3({n}): no degree-2 refutation");
2223 assert!(ns_refutes_polys(nv, &gens, 3), "Count_3({n}): refuted at degree 3 — exact");
2224 eprintln!("Count_3({n}) [{nv} vars]: exact linear-encoded GF(2) NS degree = 3");
2225 }
2226 }
2227
2228 /// Is a `Count_q` monomial a **partial partition** — a set of pairwise-disjoint blocks? The
2229 /// counting-family analog of pigeonhole's partial matchings.
2230 fn count_is_disjoint(mono: Mono, edges: &[Vec<usize>]) -> bool {
2231 let mut used = 0u64;
2232 let mut bits = mono;
2233 while bits != 0 {
2234 let e = bits.trailing_zeros() as usize;
2235 let mask: u64 = edges[e].iter().fold(0, |m, &v| m | (1u64 << v));
2236 if used & mask != 0 {
2237 return false;
2238 }
2239 used |= mask;
2240 bits &= bits - 1;
2241 }
2242 true
2243 }
2244
2245 /// **The `Count_3` witness support: the invariant closed form lives on a Lucas schedule in `n`, and
2246 /// off-schedule symmetry and validity part ways.** The `Sₙ`-invariant degree-2 pseudo-expectation
2247 /// on the **partial-partition support** (pairwise-disjoint blocks — the counting analog of partial
2248 /// matchings) is forced onto type values `(a, b₀)` (singles, disjoint pairs); solving the type
2249 /// constraints by hand: the pair generators vanish outright, `⟨L, P_i⟩ = 1 + C(n−1,2)·a` pins `a`,
2250 /// and `⟨L, x_f·P_i⟩` with `i ∉ f` contributes `a + C(n−4, 2)·b₀` — so an **invariant** witness on
2251 /// this support exists iff those binomials are odd: a parity-of-binomials condition, `n ≡ 3 (mod 4)`
2252 /// (Lucas). Machine-locked, both faces:
2253 ///
2254 /// - **`n = 7` (on schedule):** the explicit indicator `L(M) = [M pairwise disjoint]` is valid — a
2255 /// closed-form, symmetry-invariant witness, the analog of pigeonhole's hole-injective one.
2256 /// - **`n = 8` (off schedule):** *all four* invariant candidates on the disjoint support (the whole
2257 /// `(a, b₀)` cube) are invalid — machine-enumerated — yet the sub-basis search still finds a
2258 /// valid witness on the very same support, necessarily **non-invariant**. The Reynolds
2259 /// obstruction (`over_gf2_symmetrizing_a_proof_annihilates_when_the_group_is_even`) live at a
2260 /// concrete scale: over `GF(2)`, off the Lucas schedule, no symmetric witness survives where
2261 /// asymmetric ones do. This is the phenomenon the symmetric-collapse machinery must respect (its
2262 /// verdicts are about *invariant* certificates), and the first concrete face of the
2263 /// periodicity-in-`n` it is built to decide.
2264 #[test]
2265 fn count_three_witness_support_structure_is_probed_on_sub_bases() {
2266 // Dense regime, degree 1: the disjoint support carries (singles are always disjoint).
2267 for n in [4usize, 5] {
2268 let (cnf, _) = crate::families::mod_counting(n, 3);
2269 let nv = cnf.num_vars;
2270 let gens = exactly_one_linear_generators(&crate::families::mod_counting_groups(n, 3));
2271 let full = ns_lower_bound_witness_polys_on_basis(nv, &gens, 1, &|_| true)
2272 .expect("the unrestricted sub-basis search reproduces the witness");
2273 assert!(check_ns_lower_bound_polys(nv, &gens, 1, &full), "the control witness re-checks");
2274 let edges = crate::families::mod_counting_edges(n, 3);
2275 let probe =
2276 ns_lower_bound_witness_polys_on_basis(nv, &gens, 1, &|m| count_is_disjoint(m, &edges));
2277 let w = probe.expect("dense regime, degree 1: the disjoint support carries the witness");
2278 assert!(check_ns_lower_bound_polys(nv, &gens, 1, &w), "the sub-basis witness re-checks");
2279 }
2280
2281 // Genuine regime, degree 2: the invariant closed form on its n mod 4 schedule.
2282 for n in [7usize, 8] {
2283 let on_schedule = n % 4 == 3;
2284 let (cnf, _) = crate::families::mod_counting(n, 3);
2285 let nv = cnf.num_vars;
2286 let gens = exactly_one_linear_generators(&crate::families::mod_counting_groups(n, 3));
2287 let edges = crate::families::mod_counting_edges(n, 3);
2288 let disjoint: Vec<Mono> = monomials_up_to_degree(nv, 2)
2289 .into_iter()
2290 .filter(|&m| count_is_disjoint(m, &edges))
2291 .collect();
2292 // Every invariant candidate on the disjoint support: L(1)=1, singles ∈ {0,1}, pairs ∈ {0,1}.
2293 for (a, b0) in [(false, false), (false, true), (true, false), (true, true)] {
2294 let candidate: Vec<Mono> = disjoint
2295 .iter()
2296 .copied()
2297 .filter(|&m| match m.count_ones() {
2298 0 => true,
2299 1 => a,
2300 _ => b0,
2301 })
2302 .collect();
2303 let is_indicator = a && b0;
2304 let expect = on_schedule && is_indicator;
2305 assert_eq!(
2306 check_ns_lower_bound_polys(nv, &gens, 2, &candidate),
2307 expect,
2308 "Count_3({n}): invariant candidate (a={a}, b0={b0}) valid iff on the Lucas \
2309 schedule and the full indicator"
2310 );
2311 }
2312 // The support still carries a witness at every n — off schedule it must be non-invariant.
2313 let probe =
2314 ns_lower_bound_witness_polys_on_basis(nv, &gens, 2, &|m| count_is_disjoint(m, &edges));
2315 let w = probe.expect("the disjoint support carries a (possibly asymmetric) witness");
2316 assert!(check_ns_lower_bound_polys(nv, &gens, 2, &w), "the sub-basis witness re-checks");
2317 eprintln!(
2318 "Count_3({n}) at degree 2 (n mod 4 = {}): invariant closed form {}; support witness found",
2319 n % 4,
2320 if on_schedule { "VALID (the partial-partition indicator)" } else { "IMPOSSIBLE — witness is necessarily asymmetric" },
2321 );
2322 }
2323 }
2324}