Skip to main content

logicaffeine_proof/
polycalc_gfp.rs

1//! Nullstellensatz over **any prime field `GF(p)` and over `GF(4)`** — the characteristic axis of the
2//! algebraic proof system that [`crate::polycalc`] fixes at `GF(2)`.
3//!
4//! Each characteristic is a genuinely *different* proof system: the mod-`p` counting principles fall to
5//! degree 1 over `GF(p)` yet carry growing degree over `GF(q)` for every other prime `q` — the pairwise
6//! incomparability that makes "which field?" a real dial, not a convention. Extension fields, by
7//! contrast, add nothing: a certificate over `GF(pᵏ)` projects coefficient-wise through any `GF(p)`-linear
8//! functional fixing `1`, so NS degree depends only on the characteristic (the `GF(4)` engine here exists
9//! to *prove* that collapse, constructively). The `GF(2)` engine stays as the specialized fast path — its
10//! coefficient-free bitset representation packs 64 basis columns per word — and this general engine is
11//! pinned to it by a `p = 2` differential test.
12//!
13//! Everything [`crate::polycalc`] certifies has its analogue here, now with explicit coefficients:
14//! signed clause false-indicators (`1 − x`, which characteristic 2 silently conflates with `1 + x`),
15//! the field-generic partition of unity (`(1 − x) + x = 1` holds in every field, so constructive
16//! completeness is not a `GF(2)` artifact), degree-`d` refutation decisions, and dual-witness degree
17//! lower bounds with zero-trust re-checking.
18
19use crate::cdcl::Lit;
20pub use crate::polycalc::Mono;
21use std::collections::BTreeMap;
22
23/// The coefficient field of the general engine. Elements are `u64`-encoded: `Prime(p)` uses the residues
24/// `0..p`; `Gf4` uses 2-bit pairs `a + b·ω` in the basis `{1, ω}` with `ω² = ω + 1` (value `a | b<<1`).
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum NsField {
27    Prime(u64),
28    Gf4,
29}
30
31/// A multilinear polynomial over an [`NsField`]: monomial → nonzero coefficient (absent = 0). The
32/// coefficient-explicit generalization of [`crate::polycalc::Poly`], whose `GF(2)` monomial-set form is
33/// the special case "every present coefficient is 1".
34pub type GfpPoly = BTreeMap<Mono, u64>;
35
36impl NsField {
37    /// The number of field elements (`p`, resp. 4).
38    pub fn order(self) -> u64 {
39        match self {
40            NsField::Prime(p) => p,
41            NsField::Gf4 => 4,
42        }
43    }
44
45    /// The field characteristic (`p`, resp. 2) — the only parameter NS degree actually depends on.
46    pub fn characteristic(self) -> u64 {
47        match self {
48            NsField::Prime(p) => p,
49            NsField::Gf4 => 2,
50        }
51    }
52
53    pub fn add(self, a: u64, b: u64) -> u64 {
54        match self {
55            NsField::Prime(p) => (a % p + b % p) % p,
56            NsField::Gf4 => (a ^ b) & 3,
57        }
58    }
59
60    pub fn neg(self, a: u64) -> u64 {
61        match self {
62            NsField::Prime(p) => (p - a % p) % p,
63            NsField::Gf4 => a & 3,
64        }
65    }
66
67    pub fn sub(self, a: u64, b: u64) -> u64 {
68        self.add(a, self.neg(b))
69    }
70
71    pub fn mul(self, a: u64, b: u64) -> u64 {
72        match self {
73            NsField::Prime(p) => (a % p) * (b % p) % p,
74            NsField::Gf4 => {
75                // (a0 + a1ω)(b0 + b1ω) with ω² = ω + 1.
76                let (a0, a1, b0, b1) = (a & 1, (a >> 1) & 1, b & 1, (b >> 1) & 1);
77                let c0 = (a0 & b0) ^ (a1 & b1);
78                let c1 = (a0 & b1) ^ (a1 & b0) ^ (a1 & b1);
79                c0 | (c1 << 1)
80            }
81        }
82    }
83
84    /// Multiplicative inverse of a nonzero element (Fermat over `Prime(p)`; the 4-entry table over `Gf4`).
85    pub fn inv(self, a: u64) -> u64 {
86        match self {
87            NsField::Prime(p) => {
88                let a = a % p;
89                assert!(a != 0, "the zero element has no inverse");
90                let (mut base, mut e, mut r) = (a, p - 2, 1u64 % p);
91                while e > 0 {
92                    if e & 1 == 1 {
93                        r = r * base % p;
94                    }
95                    base = base * base % p;
96                    e >>= 1;
97                }
98                r
99            }
100            NsField::Gf4 => {
101                assert!(a & 3 != 0, "the zero element has no inverse");
102                [0, 1, 3, 2][(a & 3) as usize] // ω·(ω+1) = ω² + ω = 1
103            }
104        }
105    }
106
107    /// The image of an integer under the unique ring map `ℤ → F` — `n mod characteristic`. This is how
108    /// group orders and counting arguments enter the field (`|G| = 0` in `F` iff `char | |G|`).
109    pub fn embed_int(self, n: u128) -> u64 {
110        (n % self.characteristic() as u128) as u64
111    }
112}
113
114/// Add `c · m` into `p`, dropping the monomial when its coefficient cancels to zero — the coefficient
115/// analogue of the `GF(2)` engine's `toggle`.
116fn add_term(f: NsField, p: &mut GfpPoly, m: Mono, c: u64) {
117    let c = f.add(c, 0);
118    if c == 0 {
119        return;
120    }
121    let e = p.entry(m).or_insert(0);
122    *e = f.add(*e, c);
123    if *e == 0 {
124        p.remove(&m);
125    }
126}
127
128/// Multilinear product: `x·x = x` collapses variable sets by OR, and — the correctness subtlety `GF(2)`
129/// hides — colliding monomials **add coefficients**.
130fn poly_mul(f: NsField, a: &GfpPoly, b: &GfpPoly) -> GfpPoly {
131    let mut r = GfpPoly::new();
132    for (&ma, &ca) in a {
133        for (&mb, &cb) in b {
134            add_term(f, &mut r, ma | mb, f.mul(ca, cb));
135        }
136    }
137    r
138}
139
140/// The degree of a multilinear polynomial: its largest monomial's popcount (0 for the zero polynomial).
141pub fn gfp_poly_degree(p: &GfpPoly) -> usize {
142    p.keys().map(|m| m.count_ones() as usize).max().unwrap_or(0)
143}
144
145/// The clause polynomial over `f` — the **signed** false-indicator: a positive literal `x` contributes
146/// `1 − x` (over `GF(3)`: `1 + 2x`, *not* `1 + x` — over `GF(2)` the two coincide, which is exactly the
147/// sign the general engine must make explicit), a negative literal `¬x` contributes `x`. The product is
148/// `1` exactly on the clause's falsifying corners and `0` elsewhere, over every field.
149pub fn clause_polynomial_gfp(f: NsField, clause: &[Lit]) -> GfpPoly {
150    let mut p: GfpPoly = [(0u64, 1u64)].into_iter().collect();
151    for l in clause {
152        let bit = 1u64 << l.var();
153        let indicator: GfpPoly = if l.is_positive() {
154            [(0u64, 1u64), (bit, f.neg(1))].into_iter().collect() // 1 − x
155        } else {
156            [(bit, 1u64)].into_iter().collect() // x
157        };
158        p = poly_mul(f, &p, &indicator);
159    }
160    p
161}
162
163/// The single-point indicator `δ_a = Π_{a_i=1} x_i · Π_{a_i=0} (1 − x_i)`: expanded, `Σ_{T ⊆ zeros}
164/// (−1)^{|T|} x^{ones ∪ T}` — the signed subset walk whose `GF(2)` shadow is sign-free.
165fn point_indicator_gfp(f: NsField, a: u64, num_vars: usize) -> GfpPoly {
166    let mask = (1u64 << num_vars).wrapping_sub(1);
167    let ones = a & mask;
168    let zeros = !a & mask;
169    let mut p = GfpPoly::new();
170    let mut sub = zeros;
171    loop {
172        let sign = if sub.count_ones() % 2 == 0 { 1 } else { f.neg(1) };
173        p.insert(ones | sub, sign); // masks are distinct (T ⊆ zeros, disjoint from ones) — no cancellation
174        if sub == 0 {
175            break;
176        }
177        sub = (sub - 1) & zeros;
178    }
179    p
180}
181
182/// The partition-of-unity **atom** on variable `v`: `(1 − x_v) + x_v`, the constant `1` in **every**
183/// field — the identity is characteristic-free, so the completeness construction it powers is not a
184/// `GF(2)` artifact.
185pub fn pou_atom_gfp(f: NsField, v: usize) -> GfpPoly {
186    let mut atom: GfpPoly = [(0u64, 1u64), (1u64 << v, f.neg(1))].into_iter().collect();
187    add_term(f, &mut atom, 1u64 << v, 1);
188    atom
189}
190
191/// The partition of unity `Σ_{a ∈ {0,1}ⁿ} δ_a` over `f`, by direct summation — the constant `1` at every
192/// `n` over every field ([`pou_atom_gfp`] is the closed-form reason).
193pub fn partition_of_unity_gfp(f: NsField, n: usize) -> GfpPoly {
194    let mut sum = GfpPoly::new();
195    for a in 0..(1u64 << n) {
196        for (m, c) in point_indicator_gfp(f, a, n) {
197            add_term(f, &mut sum, m, c);
198        }
199    }
200    sum
201}
202
203/// Solve a linear system `Σ aᵢ·x_{vᵢ} = rhs` over `f` by incremental-echelon Gaussian elimination with
204/// modular-inverse pivot normalization. Each equation is a sparse coefficient vector over `nvars`
205/// unknowns plus its right-hand side. Returns a solution (free variables 0) or `None` on inconsistency.
206/// The field-general sibling of the `GF(2)` engine's bit-packed `gf2_solve`: rows join an echelon basis
207/// one at a time (memory stays `O(rank · nvars)` however many equations stream through), each stored row
208/// is pivot-normalized with every entry strictly below its pivot column, so back-substitution in
209/// increasing pivot order reads a solution off directly.
210pub fn gfp_solve(f: NsField, equations: &[(Vec<(usize, u64)>, u64)], nvars: usize) -> Option<Vec<u64>> {
211    let mut basis: Vec<(Vec<u64>, u64)> = Vec::new(); // pivot-normalized rows
212    let mut pivot_of_col: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
213    let lead = |row: &[u64]| row.iter().rposition(|&c| c != 0);
214    for (coeffs, rhs) in equations {
215        let mut row = vec![0u64; nvars];
216        for &(v, c) in coeffs {
217            row[v] = f.add(row[v], c);
218        }
219        let mut rhs = f.add(*rhs, 0);
220        // Reduce the leading column against the basis until it is fresh or the row dies.
221        while let Some(col) = lead(&row) {
222            let Some(&bi) = pivot_of_col.get(&col) else { break };
223            let factor = row[col];
224            let (brow, brhs) = &basis[bi];
225            for v in 0..=col {
226                row[v] = f.sub(row[v], f.mul(factor, brow[v]));
227            }
228            rhs = f.sub(rhs, f.mul(factor, *brhs));
229        }
230        match lead(&row) {
231            None => {
232                if rhs != 0 {
233                    return None; // 0 = nonzero — inconsistent
234                }
235            }
236            Some(col) => {
237                let factor = f.inv(row[col]);
238                for v in 0..=col {
239                    row[v] = f.mul(row[v], factor);
240                }
241                rhs = f.mul(rhs, factor);
242                pivot_of_col.insert(col, basis.len());
243                basis.push((row, rhs));
244            }
245        }
246    }
247    // Back-substitution in increasing pivot order: every non-pivot entry of a stored row sits strictly
248    // below its own pivot, so each referenced unknown is already fixed (free variables stay 0).
249    let mut x = vec![0u64; nvars];
250    let mut cols: Vec<usize> = pivot_of_col.keys().copied().collect();
251    cols.sort_unstable();
252    for col in cols {
253        let (row, rhs) = &basis[pivot_of_col[&col]];
254        let mut val = *rhs;
255        for v in 0..col {
256            if row[v] != 0 {
257                val = f.sub(val, f.mul(row[v], x[v]));
258            }
259        }
260        x[col] = val;
261    }
262    Some(x)
263}
264
265/// Multilinear product by a single monomial: variable sets OR together, colliding images add.
266fn poly_mul_mono_gfp(f: NsField, p: &GfpPoly, m: Mono) -> GfpPoly {
267    let mut r = GfpPoly::new();
268    for (&t, &c) in p {
269        add_term(f, &mut r, t | m, c);
270    }
271    r
272}
273
274/// Does a **degree-`d` Nullstellensatz refutation over `f`** exist for an arbitrary polynomial generator
275/// system — is `1` in the `f`-span of `{ m·g : deg(m·g) ≤ d }`? The general-field sibling of
276/// [`crate::polycalc::ns_refutes_polys`], answered by incremental-echelon span membership (memory
277/// `O(rank · basis)` however many generator products stream through). Degree-bounded enumeration —
278/// `num_vars ≤ 63`.
279pub fn ns_refutes_polys_gfp(f: NsField, num_vars: usize, gens: &[GfpPoly], degree: usize) -> bool {
280    let basis = crate::polycalc::monomials_up_to_degree(num_vars, degree);
281    let index: std::collections::HashMap<Mono, usize> =
282        basis.iter().enumerate().map(|(i, &m)| (m, i)).collect();
283    let nb = basis.len();
284    let mut echelon: Vec<Vec<u64>> = Vec::new();
285    let mut pivot_of_col: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
286    let lead = |row: &[u64]| row.iter().rposition(|&c| c != 0);
287    let mut insert = |mut row: Vec<u64>| {
288        while let Some(col) = lead(&row) {
289            let Some(&bi) = pivot_of_col.get(&col) else { break };
290            let factor = row[col];
291            for v in 0..=col {
292                row[v] = f.sub(row[v], f.mul(factor, echelon[bi][v]));
293            }
294        }
295        if let Some(col) = lead(&row) {
296            let factor = f.inv(row[col]);
297            for v in 0..=col {
298                row[v] = f.mul(row[v], factor);
299            }
300            pivot_of_col.insert(col, echelon.len());
301            echelon.push(row);
302        }
303    };
304    for g in gens {
305        if g.is_empty() {
306            continue; // the zero polynomial generates nothing
307        }
308        for &m in &basis {
309            let prod = poly_mul_mono_gfp(f, g, m);
310            if !prod.is_empty() && gfp_poly_degree(&prod) <= degree {
311                let mut row = vec![0u64; nb];
312                for (t, c) in prod {
313                    row[index[&t]] = c;
314                }
315                insert(row);
316            }
317        }
318    }
319    // Reduce the target — the constant polynomial 1 — and check it vanishes.
320    let mut target = vec![0u64; nb];
321    target[index[&0u64]] = 1;
322    while let Some(col) = lead(&target) {
323        let Some(&bi) = pivot_of_col.get(&col) else { break };
324        let factor = target[col];
325        for v in 0..=col {
326            target[v] = f.sub(target[v], f.mul(factor, echelon[bi][v]));
327        }
328    }
329    lead(&target).is_none()
330}
331
332/// [`ns_refutes_polys_gfp`] for a CNF: the generators are the signed clause false-indicators. An empty
333/// clause is `1 = 0` outright.
334pub fn ns_refutes_gfp(f: NsField, num_vars: usize, clauses: &[Vec<Lit>], degree: usize) -> bool {
335    if clauses.iter().any(|c| c.is_empty()) {
336        return true;
337    }
338    let gens: Vec<GfpPoly> = clauses.iter().map(|c| clause_polynomial_gfp(f, c)).collect();
339    ns_refutes_polys_gfp(f, num_vars, &gens, degree)
340}
341
342/// A **constructive Nullstellensatz certificate over `f`**: one coefficient polynomial `g_C` per input
343/// clause with `Σ_C p_C · g_C = 1` in the multilinear ring over `f` — the coefficient-explicit
344/// generalization of [`crate::polycalc::NsCertificate`].
345#[derive(Clone, Debug)]
346pub struct NsCertificateGfp {
347    field: NsField,
348    num_vars: usize,
349    /// `coeffs[i]` is `g_{C_i}` for the `i`-th input clause (parallel indexing to the clause list).
350    coeffs: Vec<GfpPoly>,
351}
352
353impl NsCertificateGfp {
354    pub fn field(&self) -> NsField {
355        self.field
356    }
357
358    pub fn num_vars(&self) -> usize {
359        self.num_vars
360    }
361
362    /// The maximum monomial degree among the coefficient polynomials.
363    pub fn degree(&self) -> usize {
364        self.coeffs.iter().map(gfp_poly_degree).max().unwrap_or(0)
365    }
366
367    /// **Re-check against the original clauses** (zero trust in the producer), twice over: recompute
368    /// `Σ_C p_C · g_C` in the multilinear ring over `field` and confirm it is the constant `1`; then the
369    /// engine-independent corner check — at every assignment `a` of the cube (up to 20 variables),
370    /// `Σ_C p_C(a) · g_C(a) = 1` as field elements. Fails closed on a clause-count mismatch.
371    pub fn verify(&self, clauses: &[Vec<Lit>]) -> bool {
372        if self.coeffs.len() != clauses.len() {
373            return false;
374        }
375        let f = self.field;
376        let mut sum = GfpPoly::new();
377        for (c, g) in clauses.iter().zip(&self.coeffs) {
378            if g.is_empty() {
379                continue;
380            }
381            for (m, co) in poly_mul(f, &clause_polynomial_gfp(f, c), g) {
382                add_term(f, &mut sum, m, co);
383            }
384        }
385        if !(sum.len() == 1 && sum.get(&0u64) == Some(&1)) {
386            return false;
387        }
388        if self.num_vars <= 20 {
389            let eval = |p: &GfpPoly, a: u64| -> u64 {
390                p.iter().fold(0u64, |acc, (&m, &c)| if m & !a == 0 { f.add(acc, c) } else { acc })
391            };
392            for a in 0u64..(1u64 << self.num_vars) {
393                let total = clauses.iter().zip(&self.coeffs).fold(0u64, |acc, (c, g)| {
394                    f.add(acc, f.mul(eval(&clause_polynomial_gfp(f, c), a), eval(g, a)))
395                });
396                if total != 1 {
397                    return false;
398                }
399            }
400        }
401        true
402    }
403}
404
405/// **The uniform completeness construction over `f`** — the partition-of-unity charging of
406/// [`crate::polycalc::build_ns_certificate`], field-generic because its every ingredient is: the signed
407/// point indicators sum to `1` in any field ([`partition_of_unity_gfp`]), and `p_C · δ_a = δ_a` whenever
408/// `p_C(a) = 1` (multilinear representations on the cube are unique over any field). Returns a
409/// constructive [`NsCertificateGfp`] proving UNSAT or a satisfying assignment proving SAT. Bounded to
410/// `num_vars ≤ 20` (the explicit-corner construction).
411pub fn build_ns_certificate_gfp(
412    f: NsField,
413    num_vars: usize,
414    clauses: &[Vec<Lit>],
415) -> Result<NsCertificateGfp, Vec<bool>> {
416    assert!(num_vars <= 20, "the explicit-corner construction is bounded to num_vars ≤ 20");
417    let mut coeffs: Vec<GfpPoly> = vec![GfpPoly::new(); clauses.len()];
418    for a in 0u64..(1u64 << num_vars) {
419        let sel = clauses
420            .iter()
421            .position(|c| !c.iter().any(|l| ((a >> l.var()) & 1 == 1) == l.is_positive()));
422        match sel {
423            None => return Err((0..num_vars).map(|i| (a >> i) & 1 == 1).collect()),
424            Some(ci) => {
425                for (m, c) in point_indicator_gfp(f, a, num_vars) {
426                    add_term(f, &mut coeffs[ci], m, c);
427                }
428            }
429        }
430    }
431    Ok(NsCertificateGfp { field: f, num_vars, coeffs })
432}
433
434/// A **degree-`d` pseudo-expectation over `f`** for an arbitrary generator system: a functional `L` on
435/// the degree-`≤ d` monomials with `L(1) = 1` and `L(m·g) = 0` for every admitted generator product,
436/// returned as its nonzero values `(monomial, value)`. `Some(L)` certifies `NS-degree > d` over `f`
437/// (re-checkable by [`check_ns_lower_bound_polys_gfp`], zero trust in the solver); `None` means a
438/// degree-`d` refutation exists — the exact witness↔refutation duality of the `GF(2)` engine, at every
439/// characteristic. Degree-bounded enumeration — `num_vars ≤ 63`.
440pub fn ns_lower_bound_witness_polys_gfp(
441    f: NsField,
442    num_vars: usize,
443    gens: &[GfpPoly],
444    degree: usize,
445) -> Option<Vec<(Mono, u64)>> {
446    let basis = crate::polycalc::monomials_up_to_degree(num_vars, degree);
447    let index: std::collections::HashMap<Mono, usize> =
448        basis.iter().enumerate().map(|(i, &m)| (m, i)).collect();
449    let mut eqs: Vec<(Vec<(usize, u64)>, u64)> = Vec::new();
450    for g in gens {
451        if g.is_empty() {
452            continue;
453        }
454        for &m in &basis {
455            let prod = poly_mul_mono_gfp(f, g, m);
456            if !prod.is_empty() && gfp_poly_degree(&prod) <= degree {
457                eqs.push((prod.iter().map(|(t, &c)| (index[t], c)).collect(), 0)); // ⟨L, m·g⟩ = 0
458            }
459        }
460    }
461    eqs.push((vec![(index[&0u64], 1)], 1)); // L(1) = 1
462    let l = gfp_solve(f, &eqs, basis.len())?;
463    Some(basis.iter().enumerate().filter(|&(i, _)| l[i] != 0).map(|(i, &m)| (m, l[i])).collect())
464}
465
466/// [`ns_lower_bound_witness_polys_gfp`] for a CNF (signed clause false-indicators as generators). An
467/// empty clause is an immediate refutation — no lower bound at any degree.
468pub fn ns_lower_bound_witness_gfp(
469    f: NsField,
470    num_vars: usize,
471    clauses: &[Vec<Lit>],
472    degree: usize,
473) -> Option<Vec<(Mono, u64)>> {
474    if clauses.iter().any(|c| c.is_empty()) {
475        return None;
476    }
477    let gens: Vec<GfpPoly> = clauses.iter().map(|c| clause_polynomial_gfp(f, c)).collect();
478    ns_lower_bound_witness_polys_gfp(f, num_vars, &gens, degree)
479}
480
481/// [`ns_lower_bound_witness_polys_gfp`] restricted to a **sub-basis**: the functional `L` is sought only
482/// on monomials passing `in_basis` (`L = 0` elsewhere), while the constraints `⟨L, m·g⟩ = 0` still range
483/// over *all* admitted generator products — so any `Some` is a fully valid,
484/// [`check_ns_lower_bound_polys_gfp`]-verifiable witness, and `None` means only "no witness on this
485/// sub-basis". The structure probe of the `GF(2)` engine, carried to every characteristic: which
486/// candidate supports hold a family's lower bound, field by field. Degree-bounded enumeration —
487/// `num_vars ≤ 63`.
488pub fn ns_lower_bound_witness_on_basis_gfp(
489    f: NsField,
490    num_vars: usize,
491    gens: &[GfpPoly],
492    degree: usize,
493    in_basis: &dyn Fn(Mono) -> bool,
494) -> Option<Vec<(Mono, u64)>> {
495    let all = crate::polycalc::monomials_up_to_degree(num_vars, degree);
496    let basis: Vec<Mono> = all.iter().copied().filter(|&m| in_basis(m)).collect();
497    let index: std::collections::HashMap<Mono, usize> =
498        basis.iter().enumerate().map(|(i, &m)| (m, i)).collect();
499    index.get(&0u64)?; // the empty monomial must be in the sub-basis for L(1) = 1
500    let mut eqs: Vec<(Vec<(usize, u64)>, u64)> = Vec::new();
501    for g in gens {
502        if g.is_empty() {
503            continue;
504        }
505        for &m in &all {
506            let prod = poly_mul_mono_gfp(f, g, m);
507            if !prod.is_empty() && gfp_poly_degree(&prod) <= degree {
508                let coeffs: Vec<(usize, u64)> = prod
509                    .iter()
510                    .filter_map(|(t, &c)| index.get(t).map(|&i| (i, c)))
511                    .collect();
512                eqs.push((coeffs, 0));
513            }
514        }
515    }
516    eqs.push((vec![(index[&0u64], 1)], 1));
517    let l = gfp_solve(f, &eqs, basis.len())?;
518    Some(basis.iter().enumerate().filter(|&(i, _)| l[i] != 0).map(|(i, &m)| (m, l[i])).collect())
519}
520
521/// Re-check a [`ns_lower_bound_witness_polys_gfp`] certificate (zero trust in the producer): `L(1) = 1`
522/// and `⟨L, m·g⟩ = 0` over `f` for every admitted generator product. `true` ⟹ the system genuinely has
523/// no degree-`d` Nullstellensatz refutation over `f`. Degree-bounded enumeration — `num_vars ≤ 63`.
524pub fn check_ns_lower_bound_polys_gfp(
525    f: NsField,
526    num_vars: usize,
527    gens: &[GfpPoly],
528    degree: usize,
529    witness: &[(Mono, u64)],
530) -> bool {
531    let mut l: BTreeMap<Mono, u64> = BTreeMap::new();
532    for &(m, v) in witness {
533        add_term(f, &mut l, m, v);
534    }
535    if l.get(&0u64) != Some(&1) {
536        return false; // L(1) must be 1
537    }
538    let value = |m: &Mono| l.get(m).copied().unwrap_or(0);
539    for g in gens {
540        if g.is_empty() {
541            continue;
542        }
543        for &m in &crate::polycalc::monomials_up_to_degree(num_vars, degree) {
544            let prod = poly_mul_mono_gfp(f, g, m);
545            if !prod.is_empty() && gfp_poly_degree(&prod) <= degree {
546                let pairing =
547                    prod.iter().fold(0u64, |acc, (t, &c)| f.add(acc, f.mul(c, value(t))));
548                if pairing != 0 {
549                    return false; // ⟨L, m·g⟩ must be 0
550                }
551            }
552        }
553    }
554    true
555}
556
557/// The **linear encoding of exactly-one constraints over `f`**: for each group `G` the degree-1
558/// generator `(Σ_{v∈G} x_v) − 1` plus the pairwise products `x_u·x_v`, deduplicated — the signed
559/// generalization of [`crate::polycalc::exactly_one_linear_generators`] (over `GF(2)` the two coincide,
560/// since `−1 = 1`). This is the encoding the modular-counting degree bounds are stated against; over
561/// `GF(p)` the point generators telescope — `Σ_i P_i = −n` when every edge meets exactly `p` points — so
562/// a counting family with `p ∤ n` collapses at degree 1 over its **own** characteristic.
563pub fn exactly_one_linear_generators_gfp(f: NsField, groups: &[Vec<u32>]) -> Vec<GfpPoly> {
564    let mut gens: Vec<GfpPoly> = Vec::new();
565    for g in groups {
566        let mut lin: GfpPoly = [(0u64, f.neg(1))].into_iter().collect();
567        for &v in g {
568            assert!(v < 63, "the u64 monomial mask carries ≤ 63 variables");
569            add_term(f, &mut lin, 1u64 << v, 1);
570        }
571        gens.push(lin);
572    }
573    let mut pairs: std::collections::BTreeSet<Mono> = std::collections::BTreeSet::new();
574    for g in groups {
575        for (i, &u) in g.iter().enumerate() {
576            for &v in &g[i + 1..] {
577                pairs.insert((1u64 << u) | (1u64 << v));
578            }
579        }
580    }
581    gens.extend(pairs.into_iter().map(|m| [(m, 1u64)].into_iter().collect::<GfpPoly>()));
582    gens
583}
584
585/// A **degree-`d` certificate EXTRACTION over `f`**: where [`ns_refutes_polys_gfp`] only decides span
586/// membership, this runs the same incremental echelon with **provenance** — each basis row remembers the
587/// combination of original generator products it is — and reads the certificate `g_C = Σ λ_{C,m}·m` off
588/// the target's reduction, exactly the technique of [`crate::modp::solve`]'s refutation combinations.
589/// Returns `None` when no degree-`d` refutation exists. The extracted certificate is what the
590/// `GF(4) → GF(2)` projection ([`project_gf4_certificate_to_gf2`]) operates on. Provenance rows cost
591/// `O(rank · products)` memory — meant for the small-census scale, not the witness frontier.
592pub fn ns_certificate_at_degree_gfp(
593    f: NsField,
594    num_vars: usize,
595    clauses: &[Vec<Lit>],
596    degree: usize,
597) -> Option<NsCertificateGfp> {
598    if clauses.iter().any(|c| c.is_empty()) {
599        return None; // 1 = 0 needs no polynomial certificate; this extractor works over generators
600    }
601    let gens: Vec<GfpPoly> = clauses.iter().map(|c| clause_polynomial_gfp(f, c)).collect();
602    let basis = crate::polycalc::monomials_up_to_degree(num_vars, degree);
603    let index: std::collections::HashMap<Mono, usize> =
604        basis.iter().enumerate().map(|(i, &m)| (m, i)).collect();
605    let nb = basis.len();
606    type Prov = BTreeMap<(usize, Mono), u64>; // (clause, multiplier) → λ
607    let mut echelon: Vec<(Vec<u64>, Prov)> = Vec::new();
608    let mut pivot_of_col: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
609    let lead = |row: &[u64]| row.iter().rposition(|&c| c != 0);
610    let prov_axpy = |f: NsField, dst: &mut Prov, factor: u64, src: &Prov, negate: bool| {
611        for (&k, &v) in src {
612            let delta = if negate { f.neg(f.mul(factor, v)) } else { f.mul(factor, v) };
613            let e = dst.entry(k).or_insert(0);
614            *e = f.add(*e, delta);
615            if *e == 0 {
616                dst.remove(&k);
617            }
618        }
619    };
620    for (ci, g) in gens.iter().enumerate() {
621        if g.is_empty() {
622            continue;
623        }
624        for &m in &basis {
625            let prod = poly_mul_mono_gfp(f, g, m);
626            if prod.is_empty() || gfp_poly_degree(&prod) > degree {
627                continue;
628            }
629            let mut row = vec![0u64; nb];
630            for (t, c) in prod {
631                row[index[&t]] = c;
632            }
633            let mut prov: Prov = [((ci, m), 1u64)].into_iter().collect();
634            while let Some(col) = lead(&row) {
635                let Some(&bi) = pivot_of_col.get(&col) else { break };
636                let factor = row[col];
637                let (brow, bprov) = &echelon[bi];
638                for v in 0..=col {
639                    row[v] = f.sub(row[v], f.mul(factor, brow[v]));
640                }
641                let bprov = bprov.clone();
642                prov_axpy(f, &mut prov, factor, &bprov, true);
643            }
644            if let Some(col) = lead(&row) {
645                let factor = f.inv(row[col]);
646                for v in 0..=col {
647                    row[v] = f.mul(row[v], factor);
648                }
649                let scaled: Prov = prov.iter().map(|(&k, &v)| (k, f.mul(v, factor))).collect();
650                pivot_of_col.insert(col, echelon.len());
651                echelon.push((row, scaled));
652            }
653        }
654    }
655    // Reduce the target `1`, accumulating the combination that produced it.
656    let mut target = vec![0u64; nb];
657    target[index[&0u64]] = 1;
658    let mut comb: Prov = Prov::new();
659    while let Some(col) = lead(&target) {
660        let Some(&bi) = pivot_of_col.get(&col) else { break };
661        let factor = target[col];
662        let (brow, bprov) = &echelon[bi];
663        for v in 0..=col {
664            target[v] = f.sub(target[v], f.mul(factor, brow[v]));
665        }
666        let bprov = bprov.clone();
667        prov_axpy(f, &mut comb, factor, &bprov, false);
668    }
669    if lead(&target).is_some() {
670        return None; // 1 is not in the degree-d span
671    }
672    let mut coeffs: Vec<GfpPoly> = vec![GfpPoly::new(); clauses.len()];
673    for ((ci, m), lambda) in comb {
674        add_term(f, &mut coeffs[ci], m, lambda);
675    }
676    Some(NsCertificateGfp { field: f, num_vars, coeffs })
677}
678
679/// The **`GF(4) → GF(2)` coefficient projection** — the constructive half of the extension-field
680/// collapse. `λ : a + b·ω ↦ a` is the `GF(2)`-linear functional fixing `1`; because every clause
681/// polynomial has prime-subfield (0/1) coefficients, applying `λ` coefficient-wise to a `GF(4)`
682/// certificate identity `Σ_C p_C · g_C = 1` commutes with the clause factors and lands on a `GF(2)`
683/// certificate of no larger degree. So nothing proved over `GF(4)` needed the extension: **NS degree
684/// depends only on the characteristic**. Returns `None` unless the certificate is over `Gf4`.
685pub fn project_gf4_certificate_to_gf2(cert: &NsCertificateGfp) -> Option<NsCertificateGfp> {
686    if cert.field != NsField::Gf4 {
687        return None;
688    }
689    let coeffs: Vec<GfpPoly> = cert
690        .coeffs
691        .iter()
692        .map(|g| g.iter().filter(|&(_, &c)| c & 1 == 1).map(|(&m, _)| (m, 1u64)).collect())
693        .collect();
694    Some(NsCertificateGfp { field: NsField::Prime(2), num_vars: cert.num_vars, coeffs })
695}
696
697/// The **unnormalized group-sum symmetrization** `Σ_{g∈G} g·L` of a functional over `f` — coefficients
698/// genuinely ADD, where the `GF(2)` engine could only toggle. On the constant monomial it evaluates to
699/// `|G| · L(1)`, which is the whole dichotomy: zero (annihilation) iff `char | |G|`. Pass the full
700/// closed group, not a generating set.
701pub fn symmetrize_gfp(
702    f: NsField,
703    l: &[(Mono, u64)],
704    group: &[crate::proof::Perm],
705) -> Vec<(Mono, u64)> {
706    let mut sym: BTreeMap<Mono, u64> = BTreeMap::new();
707    for &(m, v) in l {
708        for g in group {
709            add_term(f, &mut sym, crate::polycalc::apply_perm_to_mono(g, m), v);
710        }
711    }
712    sym.into_iter().collect()
713}
714
715/// The **Reynolds operator** `L ↦ |G|⁻¹ · Σ_{g∈G} g·L` over `f` — the characteristic-0 averaging trick,
716/// available exactly when `|G|` is invertible, i.e. `gcd(|G|, char) = 1`. Returns `None` when
717/// `char | |G|` (the annihilation branch: the group-sum kills `L(1)` and no normalization can restore
718/// it), and the averaged functional otherwise. Averaging a valid pseudo-expectation of a `G`-invariant
719/// generator system yields a valid, `G`-invariant one — the branch the `GF(2)` engine can never take on
720/// an even group. Pass the full closed group.
721pub fn reynolds_gfp(
722    f: NsField,
723    l: &[(Mono, u64)],
724    group: &[crate::proof::Perm],
725) -> Option<Vec<(Mono, u64)>> {
726    let order = f.embed_int(group.len() as u128);
727    if order == 0 {
728        return None; // char | |G| — the annihilation branch
729    }
730    let scale = f.inv(order);
731    Some(symmetrize_gfp(f, l, group).into_iter().map(|(m, v)| (m, f.mul(v, scale))).collect())
732}
733
734/// [`check_ns_lower_bound_polys_gfp`] for a CNF. An empty clause admits no lower bound at any degree.
735pub fn check_ns_lower_bound_gfp(
736    f: NsField,
737    num_vars: usize,
738    clauses: &[Vec<Lit>],
739    degree: usize,
740    witness: &[(Mono, u64)],
741) -> bool {
742    if clauses.iter().any(|c| c.is_empty()) {
743        return false;
744    }
745    let gens: Vec<GfpPoly> = clauses.iter().map(|c| clause_polynomial_gfp(f, c)).collect();
746    check_ns_lower_bound_polys_gfp(f, num_vars, &gens, degree, witness)
747}
748
749#[cfg(test)]
750mod tests {
751    use super::*;
752
753    /// A deterministic LCG for reproducible fuzz corpora inside tests (no `rand`, no wall clock).
754    fn lcg(state: &mut u64) -> u64 {
755        *state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
756        *state >> 33
757    }
758
759    /// Evaluate a polynomial at a cube corner: monomial `m` contributes its coefficient iff `m ⊆ a`.
760    fn eval_at(f: NsField, p: &GfpPoly, a: u64) -> u64 {
761        p.iter().fold(0u64, |acc, (&m, &c)| if m & !a == 0 { f.add(acc, c) } else { acc })
762    }
763
764    fn falsifies(clause: &[Lit], a: u64) -> bool {
765        !clause.iter().any(|l| ((a >> l.var()) & 1 == 1) == l.is_positive())
766    }
767
768    /// **The clause polynomial over `GF(p)` is the SIGNED false-indicator, pinned on every corner.** Over
769    /// `GF(2)`, `1 − x = 1 + x`, so the sign of the positive-literal factor is invisible; over `GF(3)` it
770    /// is not — the factor must be `1 + 2x`, and using `1 + x` silently breaks the indicator semantics.
771    /// We pin the sign convention explicitly, then verify the corner semantics (1 exactly on falsifying
772    /// corners, 0 elsewhere) on a deterministic random-clause corpus across `p ∈ {2, 3, 5, 7}`.
773    #[test]
774    fn gf3_clause_polynomial_is_the_signed_false_indicator_on_every_corner() {
775        let f3 = NsField::Prime(3);
776        // The sign pin: the false-indicator of the positive literal x0 is 1 − x = 1 + 2x over GF(3).
777        let pos = clause_polynomial_gfp(f3, &[Lit::pos(0)]);
778        let expected: GfpPoly = [(0u64, 1u64), (1u64, 2u64)].into_iter().collect();
779        assert_eq!(pos, expected, "positive literal → 1 − x = 1 + 2x over GF(3), not 1 + x");
780        let neg = clause_polynomial_gfp(f3, &[Lit::neg(0)]);
781        let expected_neg: GfpPoly = [(1u64, 1u64)].into_iter().collect();
782        assert_eq!(neg, expected_neg, "negative literal → x");
783
784        let mut seed = 0x00C0_FFEEu64;
785        for &p in &[2u64, 3, 5, 7] {
786            let f = NsField::Prime(p);
787            for _ in 0..40 {
788                let n = 2 + (lcg(&mut seed) % 5) as usize; // 2..=6 variables
789                let width = 1 + (lcg(&mut seed) % n as u64) as usize;
790                let mut vars: Vec<u32> = Vec::new();
791                while vars.len() < width {
792                    let v = (lcg(&mut seed) % n as u64) as u32;
793                    if !vars.contains(&v) {
794                        vars.push(v);
795                    }
796                }
797                let clause: Vec<Lit> =
798                    vars.iter().map(|&v| Lit::new(v, lcg(&mut seed) & 1 == 1)).collect();
799                let poly = clause_polynomial_gfp(f, &clause);
800                assert_eq!(gfp_poly_degree(&poly), width, "p={p}: clause polynomial degree = width");
801                for a in 0u64..(1u64 << n) {
802                    let want = if falsifies(&clause, a) { 1 } else { 0 };
803                    assert_eq!(
804                        eval_at(f, &poly, a),
805                        want,
806                        "p={p} clause={clause:?} corner={a:0width$b}",
807                        width = n
808                    );
809                }
810            }
811        }
812    }
813
814    /// **The partition-of-unity atom `(1 − x) + x = 1` holds in EVERY prime field** — the identity behind
815    /// constructive NS completeness is characteristic-free, so "no finite randomness" (§3 of the paper) is
816    /// not a `GF(2)` artifact. The atom is the constant `1` at `p ∈ {2, 3, 5, 7}`, and the `2ⁿ`-corner sum
817    /// `Σ_a δ_a` — now with genuinely signed indicator terms that must cancel — collapses to `1` at every
818    /// `n ≤ 10` over every one of those fields.
819    #[test]
820    fn the_partition_of_unity_atom_is_one_over_every_prime_field() {
821        let one: GfpPoly = [(0u64, 1u64)].into_iter().collect();
822        for &p in &[2u64, 3, 5, 7] {
823            let f = NsField::Prime(p);
824            for v in 0..8 {
825                assert_eq!(pou_atom_gfp(f, v), one, "p={p}: the atom (1−x{v})+x{v} reduces to 1");
826            }
827            for n in 0..=10 {
828                assert_eq!(partition_of_unity_gfp(f, n), one, "p={p}: Σ_a δ_a = 1 over the {n}-cube");
829            }
830        }
831    }
832
833    /// **The incomparability row, side 1: `Count_3` collapses at degree 1 over its OWN characteristic
834    /// and carries certified growing degree over the other.** The linear-encoded point generators
835    /// telescope over `GF(3)`: every 3-block meets exactly 3 points, so `Σ_i P_i = −n` — a nonzero
836    /// CONSTANT whenever `3 ∤ n`, hence a degree-**1** refutation at every scale (`n = 4, 5, 7`, the last
837    /// at 35 variables). The identity is asserted as polynomial algebra, not just its consequence. Over
838    /// `GF(2)` the same family has certified exact degree 2 (`n = 4, 5`, both halves) growing to `≥ 3`
839    /// (`n = 7`, re-checked dual witness) — the facts of the paper's mismatch row, re-invoked beside
840    /// their `GF(3)` foil. One family, two characteristics, opposite complexity.
841    #[test]
842    fn count_three_falls_to_degree_one_over_gf3_but_has_certified_growing_degree_over_gf2() {
843        let f3 = NsField::Prime(3);
844        for n in [4usize, 5, 7] {
845            let (cnf, _) = crate::families::mod_counting(n, 3);
846            let nv = cnf.num_vars;
847            let groups = crate::families::mod_counting_groups(n, 3);
848            let gens3 = exactly_one_linear_generators_gfp(f3, &groups);
849            // The telescoping identity: the n point generators sum to the constant −n over GF(3).
850            let mut total = GfpPoly::new();
851            for g in gens3.iter().take(n) {
852                for (&m, &c) in g {
853                    add_term(f3, &mut total, m, c);
854                }
855            }
856            let minus_n = f3.neg(f3.embed_int(n as u128));
857            assert_ne!(minus_n, 0, "3 ∤ {n}, so the telescoped constant is nonzero");
858            let expected: GfpPoly = [(0u64, minus_n)].into_iter().collect();
859            assert_eq!(total, expected, "Count_3({n}): Σ_i P_i = −n over GF(3)");
860            assert!(
861                ns_refutes_polys_gfp(f3, nv, &gens3, 1),
862                "Count_3({n}): a degree-1 GF(3) refutation — the char-matched collapse"
863            );
864            assert!(
865                ns_lower_bound_witness_polys_gfp(f3, nv, &gens3, 1).is_none(),
866                "Count_3({n}): duality — no GF(3) pseudo-expectation survives at degree 1"
867            );
868        }
869        // The GF(2) half: exact degree 2 at n = 4, 5; ≥ 3 at n = 7 — strictly above 1, and growing.
870        for n in [4usize, 5] {
871            let (cnf, _) = crate::families::mod_counting(n, 3);
872            let nv = cnf.num_vars;
873            let gens2 = crate::polycalc::exactly_one_linear_generators(
874                &crate::families::mod_counting_groups(n, 3),
875            );
876            let w1 = crate::polycalc::ns_lower_bound_witness_polys(nv, &gens2, 1)
877                .expect("Count_3: no degree-1 GF(2) refutation");
878            assert!(crate::polycalc::check_ns_lower_bound_polys(nv, &gens2, 1, &w1));
879            assert!(crate::polycalc::ns_refutes_polys(nv, &gens2, 2), "Count_3({n}): exact GF(2) degree 2");
880        }
881        let n = 7usize;
882        let (cnf, _) = crate::families::mod_counting(n, 3);
883        let gens2 = crate::polycalc::exactly_one_linear_generators(
884            &crate::families::mod_counting_groups(n, 3),
885        );
886        let w2 = crate::polycalc::ns_lower_bound_witness_polys(cnf.num_vars, &gens2, 2)
887            .expect("Count_3(7): the GF(2) degree exceeds 2");
888        assert!(crate::polycalc::check_ns_lower_bound_polys(cnf.num_vars, &gens2, 2, &w2));
889        eprintln!("Count_3: GF(3) degree = 1 at n = 4, 5, 7; GF(2) degree = 2, 2, ≥3 — incomparability side 1");
890    }
891
892    /// **The incomparability row, side 2 — the exact mirror: `Count_2` collapses at degree 1 over
893    /// `GF(2)` and carries certified growing degree over `GF(3)`.** The paper's char-matched collapse
894    /// (`Count_2`, odd `n`, degree-1 `GF(2)` refutation) is re-invoked beside the new fact: over `GF(3)`
895    /// the same family has exact NS degree **2 at `n = 3` and 3 at `n = 5`** — both halves certified (a
896    /// re-checked dual witness below, a refutation at the degree) — strictly growing, strictly above the
897    /// `GF(2)` degree of 1. Even `n` is satisfiable and refuted at no probed degree over either field
898    /// (the soundness foil). Together with side 1 this machine-certifies that `GF(2)`- and `GF(3)`-NS
899    /// are **incomparable proof systems**: each crushes at degree 1 a family the other can only refute
900    /// with growing degree.
901    #[test]
902    fn count_two_falls_to_degree_one_over_gf2_but_has_certified_growing_degree_over_gf3() {
903        let f3 = NsField::Prime(3);
904        let mut gf3_degrees = Vec::new();
905        for (n, exact) in [(3usize, 2usize), (5, 3)] {
906            let (cnf, _) = crate::families::mod_counting(n, 2);
907            let nv = cnf.num_vars;
908            let groups = crate::families::mod_counting_groups(n, 2);
909            let gens2 = crate::polycalc::exactly_one_linear_generators(&groups);
910            assert!(
911                crate::polycalc::ns_refutes_polys(nv, &gens2, 1),
912                "Count_2({n}), n odd: GF(2) degree 1 — the char-matched collapse"
913            );
914            let gens3 = exactly_one_linear_generators_gfp(f3, &groups);
915            for d in 1..exact {
916                let w = ns_lower_bound_witness_polys_gfp(f3, nv, &gens3, d)
917                    .expect("a dual witness exists below the exact degree");
918                assert!(
919                    check_ns_lower_bound_polys_gfp(f3, nv, &gens3, d, &w),
920                    "Count_2({n}): GF(3) NS-degree > {d} re-checks with zero trust"
921                );
922            }
923            assert!(
924                ns_refutes_polys_gfp(f3, nv, &gens3, exact),
925                "Count_2({n}): GF(3) refuted at degree {exact} — exact"
926            );
927            gf3_degrees.push(exact);
928            eprintln!("Count_2({n}) [{nv} vars]: GF(2) degree 1, certified exact GF(3) degree {exact}");
929        }
930        assert!(
931            gf3_degrees.windows(2).all(|w| w[1] > w[0]),
932            "the GF(3) degree grows with n: {gf3_degrees:?}"
933        );
934        // Soundness foil: even n is SAT (a perfect matching exists) — no refutation over either field.
935        let (cnf, _) = crate::families::mod_counting(4, 2);
936        let groups = crate::families::mod_counting_groups(4, 2);
937        let gens3 = exactly_one_linear_generators_gfp(f3, &groups);
938        let gens2 = crate::polycalc::exactly_one_linear_generators(&groups);
939        for d in 1..=3 {
940            assert!(!ns_refutes_polys_gfp(f3, cnf.num_vars, &gens3, d), "Count_2(4) is SAT (GF(3), d={d})");
941            assert!(!crate::polycalc::ns_refutes_polys(cnf.num_vars, &gens2, d), "Count_2(4) is SAT (GF(2), d={d})");
942        }
943    }
944
945    /// **The exact `GF(3)` degree of `Count_2` at scale: a wide-tread staircase, mirroring the
946    /// `GF(2)` side.** `Count_2(7)` (21 variables, basis `C(21,≤3) = 1562`) has exact linear-encoded
947    /// `GF(3)` NS degree **3** — certified both halves: a re-checked degree-2 dual witness and a
948    /// refutation at 3. So the mismatch-side staircase reads `2 (n=3), 3 (n=5), 3 (n=7)`: the degree
949    /// grows out of the dense regime and then holds its tread — precisely the profile of the paper's
950    /// `GF(2)`-side `Count_3` measurements (`2, 2, 3, 3` across `n = 4, 5, 7, 8`). The two mismatch
951    /// rows are not just qualitatively symmetric; they climb the same wide-tread staircase from
952    /// opposite characteristics. (`n = 9` — 36 variables, a `C(36,≤4)` basis — lies past the dense
953    /// mod-`p` echelon's frontier.) Dense `GF(3)` Gaussian elimination at this width is release-scale
954    /// work, so it lives behind `#[ignore]` like its `GF(2)` siblings.
955    #[test]
956    #[ignore = "scale measurement — dense GF(3) Gaussian elimination at 1562 columns; run explicitly or via the fast suite"]
957    fn count_two_scale_probe_measures_the_gf3_degree_at_scale() {
958        let f3 = NsField::Prime(3);
959        let (cnf, _) = crate::families::mod_counting(7, 2);
960        let nv = cnf.num_vars;
961        let gens3 = exactly_one_linear_generators_gfp(f3, &crate::families::mod_counting_groups(7, 2));
962        let w = ns_lower_bound_witness_polys_gfp(f3, nv, &gens3, 2)
963            .expect("Count_2(7): a degree-2 GF(3) pseudo-expectation exists");
964        assert!(
965            check_ns_lower_bound_polys_gfp(f3, nv, &gens3, 2, &w),
966            "Count_2(7): GF(3) NS-degree ≥ 3 re-checks with zero trust"
967        );
968        assert!(
969            ns_refutes_polys_gfp(f3, nv, &gens3, 3),
970            "Count_2(7): a degree-3 GF(3) refutation exists — the exact degree is 3"
971        );
972        eprintln!("Count_2(7) [{nv} vars]: exact GF(3) NS degree = 3 — the staircase 2, 3, 3");
973    }
974
975    /// **Mod-3 Tseitin: degree-1 `GF(3)` reasoning crushes what the whole certified `GF(2)` ladder
976    /// cannot even place.** On the 3-regular-expander divergence instance with total charge `2` — `≡ 0
977    /// (mod 2)`, so the parity cut is *structurally* blind — the native mod-3 system AND the system
978    /// recovered from its opaque one-hot CNF both refute instantly, each with a re-checkable
979    /// linear-dependency combination. The certified proof-complexity ladder, probed through NS degree 3
980    /// on the 18-variable CNF, reports `BeyondBudget`: not trivial, not counting, not parity, no
981    /// low-degree GF(2) certificate. This is the `router_beats_ladder` audit gap measured on a concrete
982    /// instance — the datum the census's characteristic rung exists to close.
983    #[test]
984    fn mod3_tseitin_is_gf3_easy_and_its_gf2_route_is_the_audit_gap() {
985        let (eqs, cnf, verdict) = crate::families::mod_p_tseitin_expander(4, 3, 0xC0DE);
986        assert_eq!(verdict, crate::families::ExpectedVerdict::Unsat);
987        let ne = cnf.num_vars / 3; // one-hot: 3 boolean bits per GF(3) edge variable
988        match crate::modp::solve(&eqs, ne, 3) {
989            crate::modp::ModpOutcome::Unsat(combo) => {
990                assert!(
991                    crate::modp::is_refutation(&eqs, ne, 3, &combo),
992                    "the native GF(3) refutation re-checks"
993                );
994            }
995            crate::modp::ModpOutcome::Sat(_) => panic!("the charged divergence system is inconsistent"),
996        }
997        // The opaque one-hot CNF lifts back to the same mod-3 system (recognition, never guessing).
998        let rec = crate::modp::recover_from_cnf(cnf.num_vars, &cnf.clauses)
999            .expect("the one-hot encoding is recognized");
1000        assert_eq!(rec.modulus, 3, "the recovered modulus is the group size");
1001        assert_eq!(rec.num_vars, ne, "one recovered GF(3) variable per one-hot group");
1002        match crate::modp::solve(&rec.equations, rec.num_vars, 3) {
1003            crate::modp::ModpOutcome::Unsat(combo) => {
1004                assert!(
1005                    crate::modp::is_refutation(&rec.equations, rec.num_vars, 3, &combo),
1006                    "the recovered-system refutation re-checks"
1007                );
1008            }
1009            crate::modp::ModpOutcome::Sat(_) => panic!("the recovered system is inconsistent"),
1010        }
1011        // The audit gap, measured: the certified ladder has no rung that reaches this instance.
1012        let rung = crate::hypercube::weakest_crushing_rung(cnf.num_vars, &cnf.clauses, 3);
1013        assert_eq!(
1014            rung,
1015            crate::hypercube::ProofRung::BeyondBudget,
1016            "the GF(2) ladder cannot place what degree-1 GF(3) crushes"
1017        );
1018    }
1019
1020    /// The all-ones hole-injective indicator of the paper's §5.3, as a general-field witness.
1021    fn hole_injective_indicator(num_vars: usize, holes: usize, degree: usize) -> Vec<(Mono, u64)> {
1022        (0u64..(1u64 << num_vars))
1023            .filter(|&mo| {
1024                mo.count_ones() as usize <= degree
1025                    && crate::polycalc::php_is_hole_injective(mo, holes)
1026            })
1027            .map(|mo| (mo, 1))
1028            .collect()
1029    }
1030
1031    /// **The hole-injective indicator is a pseudo-expectation at EVERY characteristic — the paper's
1032    /// parity argument is the char-2 shadow of a binomial identity.** With the *signed* clause
1033    /// false-indicators, a pigeon clause pairs against the indicator as `Σ_{S_A⊆A, S_U⊆U}
1034    /// (−1)^{|S_A|+|S_U|} = (1−1)^{|A|}·(1−1)^{|U|}` (`A` = holes where the monomial already carries this
1035    /// pigeon's edge, `U` = untouched holes), which telescopes to `0` in **every** field whenever
1036    /// `|A| + |U| ≥ 1` — and the degree cap `2m−3` forces exactly that. Over `GF(2)` the signs are
1037    /// invisible (`−1 = 1`), so the identity *degenerates into* the paper's `Σ 1 = 2^{|U|} ≡ 0 (mod 2)`.
1038    /// The at-most-one generators vanish on hole collisions at every characteristic alike. Machine-checked
1039    /// at `m = 3, 4` over `GF(2)` (the paper's checker), `GF(3)`, and `GF(5)` — one closed-form witness,
1040    /// every prime field, proving `NS-degree(PHP_m) ≥ 2(m−1)` characteristic-free.
1041    #[test]
1042    fn the_hole_injective_indicator_is_a_pseudo_expectation_at_every_characteristic() {
1043        for m in [3usize, 4] {
1044            let (php, _) = crate::families::php(m);
1045            let holes = m - 1;
1046            let d = 2 * holes - 1;
1047            let w = hole_injective_indicator(php.num_vars, holes, d);
1048            // GF(2): the paper's fact, re-invoked through the paper's own checker.
1049            let monos: Vec<u64> = w.iter().map(|&(mo, _)| mo).collect();
1050            assert!(
1051                crate::polycalc::check_ns_lower_bound(php.num_vars, &php.clauses, d, &monos),
1052                "PHP({m}): the indicator is valid over GF(2) — the parity shadow"
1053            );
1054            // GF(3), GF(5): the same all-ones indicator, now via the binomial telescoping.
1055            for &p in &[3u64, 5] {
1056                assert!(
1057                    check_ns_lower_bound_gfp(NsField::Prime(p), php.num_vars, &php.clauses, d, &w),
1058                    "PHP({m}): the indicator is a valid degree-{d} pseudo-expectation over GF({p}) \
1059                     ⟹ NS-degree ≥ {} at characteristic {p}",
1060                    2 * holes
1061                );
1062            }
1063        }
1064    }
1065
1066    /// **Pigeonhole hardness is characteristic-INVARIANT — the foil to `Count_p`'s characteristic
1067    /// sensitivity.** `Count_p` flips from degree 1 to growing degree as the field changes; PHP does not
1068    /// budge: its `GF(3)` NS degree at `m = 3` is **exactly 4 = 2(m−1)**, the same as `GF(2)` — measured
1069    /// by scan (no refutation through degree 3, refutation at 4), with the lower half certified by a
1070    /// solver-found dual witness that re-checks, and the duality (`witness at d ⟺ no refutation at d`)
1071    /// pinned on both sides of the threshold. At `m = 4` the uniform indicator certifies `GF(3)`
1072    /// NS-degree ≥ 6 = 2(m−1), matching the certified `GF(2)` exact degree. Counting is orthogonal to
1073    /// linear algebra over **every** field — which is exactly why the pigeonhole group (`Sₘ × Sₘ₋₁`,
1074    /// permutation symmetry) rather than any field structure is what protects its hardness.
1075    #[test]
1076    fn php_gf3_ns_degree_is_measured_and_its_lower_half_certified() {
1077        let f3 = NsField::Prime(3);
1078        // m = 3: the exact degree, both halves, plus the duality at the threshold.
1079        let (php3, _) = crate::families::php(3);
1080        let nv = php3.num_vars;
1081        for d in 1..=3 {
1082            assert!(!ns_refutes_gfp(f3, nv, &php3.clauses, d), "PHP(3): no GF(3) refutation at {d}");
1083        }
1084        assert!(ns_refutes_gfp(f3, nv, &php3.clauses, 4), "PHP(3): GF(3) refuted at 4 — exact");
1085        let w3 = ns_lower_bound_witness_gfp(f3, nv, &php3.clauses, 3)
1086            .expect("PHP(3): a solver-found GF(3) witness exists at degree 3");
1087        assert!(
1088            check_ns_lower_bound_gfp(f3, nv, &php3.clauses, 3, &w3),
1089            "PHP(3): GF(3) NS-degree > 3 re-checks with zero trust"
1090        );
1091        assert!(
1092            ns_lower_bound_witness_gfp(f3, nv, &php3.clauses, 4).is_none(),
1093            "PHP(3): duality — no witness survives at the refutation degree"
1094        );
1095        // The GF(2) exact degree is 4 (the paper's certified fact) — the characteristics agree.
1096        assert!(crate::polycalc::nullstellensatz_refutes(nv, &php3.clauses, 4));
1097        assert!(!crate::polycalc::nullstellensatz_refutes(nv, &php3.clauses, 3));
1098        // m = 4: the uniform indicator certifies GF(3) NS-degree ≥ 6 = 2(m−1), matching GF(2).
1099        let (php4, _) = crate::families::php(4);
1100        let w4 = hole_injective_indicator(php4.num_vars, 3, 5);
1101        assert!(
1102            check_ns_lower_bound_gfp(f3, php4.num_vars, &php4.clauses, 5, &w4),
1103            "PHP(4): GF(3) NS-degree ≥ 6 via the uniform indicator"
1104        );
1105        eprintln!("PHP: GF(3) degree = 4 (m=3, exact), ≥ 6 (m=4) — equal to GF(2); characteristic-invariant");
1106    }
1107
1108    /// **The witness SUPPORT is where the characteristic bites — and it bites on a THRESHOLD, not just
1109    /// at 2.** The paper proved the classical (Razborov, char-0) partial-matching support cannot carry
1110    /// the `GF(2)` witness (a parity obstruction; the hole-injective support is the `GF(2)`-correct
1111    /// one). The general engine measures the full landscape: at `m = 3` the matching support is rescued
1112    /// by every odd prime (`GF(3), GF(5), GF(7)` all carry a re-checked witness), but at `m = 4` it
1113    /// fails over `GF(3)` as well — only `p ≥ 5` rescues it. The measured law across all eight
1114    /// (prime, m) points: **the classical support survives exactly when the characteristic clears a
1115    /// threshold growing with the family (`p ≥ m` here), with `GF(2)` merely the deepest failure** —
1116    /// the classical argument divides by counts that small primes annihilate, the same
1117    /// small-prime-divides-a-binomial mechanism as the paper's `Count_3` Lucas schedule. Meanwhile the
1118    /// hole-injective support carries the witness at every characteristic and every `m` (the control,
1119    /// per [`the_hole_injective_indicator_is_a_pseudo_expectation_at_every_characteristic`]). So the
1120    /// *bound* is characteristic-invariant while the *witness structure* is characteristic-graded — the
1121    /// honest refinement of the paper's "characteristic matters" theme: it matters at the support, on a
1122    /// threshold.
1123    #[test]
1124    fn the_php_witness_support_structure_differs_by_characteristic() {
1125        for m in [3usize, 4] {
1126            let (php, _) = crate::families::php(m);
1127            let nv = php.num_vars;
1128            let holes = m - 1;
1129            let d = 2 * holes - 1;
1130            let is_pm = |mo: Mono| crate::polycalc::php_is_partial_matching(mo, holes);
1131            // GF(2): the partial-matching support fails (the paper's parity-obstruction fact, re-run).
1132            let pm2 = crate::polycalc::ns_lower_bound_witness_on_basis(nv, &php.clauses, d, &is_pm);
1133            assert!(pm2.is_none(), "PHP({m}): over GF(2) the partial-matching sub-basis fails");
1134            for &p in &[3u64, 5, 7] {
1135                let f = NsField::Prime(p);
1136                let gens: Vec<GfpPoly> =
1137                    php.clauses.iter().map(|c| clause_polynomial_gfp(f, c)).collect();
1138                let pm = ns_lower_bound_witness_on_basis_gfp(f, nv, &gens, d, &is_pm);
1139                let expected = p >= m as u64; // the measured threshold: the support survives iff p ≥ m
1140                assert_eq!(
1141                    pm.is_some(),
1142                    expected,
1143                    "PHP({m}) over GF({p}): the partial-matching support carries a witness iff p ≥ m"
1144                );
1145                if let Some(w) = pm {
1146                    assert!(
1147                        check_ns_lower_bound_polys_gfp(f, nv, &gens, d, &w),
1148                        "PHP({m}) over GF({p}): the matching-support witness re-checks with zero trust"
1149                    );
1150                    assert!(
1151                        w.iter().all(|&(mo, _)| is_pm(mo)),
1152                        "PHP({m}) over GF({p}): the witness is genuinely supported on partial matchings"
1153                    );
1154                }
1155                // Control: the hole-injective sub-basis carries the witness at every characteristic.
1156                let hi = ns_lower_bound_witness_on_basis_gfp(f, nv, &gens, d, &|mo| {
1157                    crate::polycalc::php_is_hole_injective(mo, holes)
1158                })
1159                .expect("the hole-injective sub-basis carries the witness at every prime");
1160                assert!(check_ns_lower_bound_polys_gfp(f, nv, &gens, d, &hi));
1161            }
1162            eprintln!(
1163                "PHP({m}) d={d}: matching support — GF(2): ✗, GF(3): {}, GF(5): ✓, GF(7): ✓ (threshold p ≥ m)",
1164                if 3 >= m as u64 { "✓" } else { "✗" }
1165            );
1166        }
1167    }
1168
1169    /// The variable permutation of PHP(m) induced by a pigeon permutation `sigma` (holes fixed):
1170    /// `x_{p,h} ↦ x_{sigma(p),h}`, with the [`crate::families::php`] layout `var(p,h) = p·holes + h`.
1171    fn php_pigeon_perm(m: usize, sigma: &dyn Fn(usize) -> usize) -> crate::proof::Perm {
1172        let holes = m - 1;
1173        let images: Vec<Lit> = (0..m * holes)
1174            .map(|v| {
1175                let (p, h) = (v / holes, v % holes);
1176                Lit::pos((sigma(p) * holes + h) as u32)
1177            })
1178            .collect();
1179        crate::proof::Perm::from_images(images)
1180    }
1181
1182    /// **The annihilation dichotomy, both branches, both characteristics: symmetrizing kills the
1183    /// witness EXACTLY when `p` divides `|G|`.** The paper's §5.5 proved the char-2 case (every even
1184    /// group annihilates over `GF(2)`); the general engine shows the theorem is about `p | |G|`, not
1185    /// about 2. On PHP(3), take the pigeon transposition group `C₂` (order 2) and the pigeon 3-cycle
1186    /// group `C₃` (order 3), and cross them with `GF(2)` and `GF(3)`: the group-sum `Σ_g g·L` evaluates
1187    /// on the constant monomial to `|G| · L(1)`, so it annihilates in precisely two of the four cells —
1188    /// `C₂` over `GF(2)` and `C₃` over `GF(3)` — and survives (with the Reynolds operator available) in
1189    /// the other two. The same subgroup that kills a witness at one characteristic averages it perfectly
1190    /// at the other. Certified with a genuine solver-found witness in every cell.
1191    #[test]
1192    fn over_gfp_symmetrizing_annihilates_exactly_when_p_divides_the_group_order() {
1193        let m = 3usize;
1194        let (php, _) = crate::families::php(m);
1195        let nv = php.num_vars;
1196        let d = 2 * (m - 1) - 1;
1197        let c2 = crate::polycalc::close_perm_group(
1198            &[php_pigeon_perm(m, &|p| [1, 0, 2][p])],
1199            nv,
1200        );
1201        let c3 = crate::polycalc::close_perm_group(
1202            &[php_pigeon_perm(m, &|p| (p + 1) % m)],
1203            nv,
1204        );
1205        assert_eq!(c2.len(), 2, "the pigeon transposition generates C₂");
1206        assert_eq!(c3.len(), 3, "the pigeon 3-cycle generates C₃");
1207        for &p in &[2u64, 3] {
1208            let f = NsField::Prime(p);
1209            let w = ns_lower_bound_witness_gfp(f, nv, &php.clauses, d)
1210                .expect("a degree-3 witness exists at every characteristic (PHP degree is 4)");
1211            assert!(check_ns_lower_bound_gfp(f, nv, &php.clauses, d, &w), "the witness re-checks");
1212            assert_eq!(w.iter().find(|&&(mo, _)| mo == 0).map(|&(_, v)| v), Some(1), "L(1) = 1");
1213            for (group, order) in [(&c2, 2u64), (&c3, 3u64)] {
1214                let symmetrized = symmetrize_gfp(f, &w, group);
1215                let l1 = symmetrized.iter().find(|&&(mo, _)| mo == 0).map(|&(_, v)| v);
1216                let annihilates = order % p == 0;
1217                assert_eq!(
1218                    l1.is_none(),
1219                    annihilates,
1220                    "GF({p}) × C_{order}: the group-sum L(1) = |G|·1 = {order} vanishes iff {p} | {order}"
1221                );
1222                assert_eq!(
1223                    reynolds_gfp(f, &w, group).is_none(),
1224                    annihilates,
1225                    "GF({p}) × C_{order}: the Reynolds operator exists iff gcd(|G|, p) = 1"
1226                );
1227            }
1228        }
1229        // The paper's own case, through the general engine: the FULL pigeonhole group (order 12,
1230        // divisible by both 2 and 3) annihilates at BOTH characteristics.
1231        let full = crate::polycalc::close_perm_group(&crate::hypercube::php_perm_symmetries(m), nv);
1232        assert_eq!(full.len(), 12, "|S₃ × S₂| = 12");
1233        for &p in &[2u64, 3] {
1234            let f = NsField::Prime(p);
1235            let w = ns_lower_bound_witness_gfp(f, nv, &php.clauses, d).expect("witness exists");
1236            assert!(reynolds_gfp(f, &w, &full).is_none(), "GF({p}): 12 ≡ 0, the full group annihilates");
1237        }
1238    }
1239
1240    /// **The constructive branch the paper could never exhibit: Reynolds averaging produces a valid,
1241    /// invariant witness whenever the order is invertible.** Over `GF(2)` every even group annihilates,
1242    /// so the paper had to build its symmetric witness natively (§5.5). The general engine takes the
1243    /// other branch: averaging a solver-found (unstructured) witness over a group of invertible order
1244    /// yields a functional that (i) re-checks as a valid pseudo-expectation with zero trust and (ii) is
1245    /// genuinely `G`-invariant, monomial by monomial. Exhibited in both directions — `C₂` over `GF(3)`
1246    /// (2 is a unit mod 3) and `C₃` over `GF(2)` (3 is odd) — so the averaging trick classical
1247    /// proof complexity takes for granted in characteristic 0 is machine-verified to work at every
1248    /// characteristic that misses the group order.
1249    #[test]
1250    fn the_reynolds_operator_produces_a_valid_symmetric_witness_when_the_order_is_invertible() {
1251        let m = 3usize;
1252        let (php, _) = crate::families::php(m);
1253        let nv = php.num_vars;
1254        let d = 2 * (m - 1) - 1;
1255        let c2 = crate::polycalc::close_perm_group(&[php_pigeon_perm(m, &|p| [1, 0, 2][p])], nv);
1256        let c3 = crate::polycalc::close_perm_group(&[php_pigeon_perm(m, &|p| (p + 1) % m)], nv);
1257        for (p, group, label) in [(3u64, &c2, "C₂ over GF(3)"), (2, &c3, "C₃ over GF(2)")] {
1258            let f = NsField::Prime(p);
1259            let w = ns_lower_bound_witness_gfp(f, nv, &php.clauses, d).expect("witness exists");
1260            let avg = reynolds_gfp(f, &w, group).expect("the order is invertible — Reynolds exists");
1261            assert!(
1262                check_ns_lower_bound_gfp(f, nv, &php.clauses, d, &avg),
1263                "{label}: the averaged witness is a valid pseudo-expectation (zero trust)"
1264            );
1265            let value: BTreeMap<Mono, u64> = avg.iter().copied().collect();
1266            for g in group {
1267                for &(mo, v) in &avg {
1268                    let img = crate::polycalc::apply_perm_to_mono(g, mo);
1269                    assert_eq!(
1270                        value.get(&img).copied().unwrap_or(0),
1271                        v,
1272                        "{label}: the averaged witness is G-invariant monomial-by-monomial"
1273                    );
1274                }
1275            }
1276            eprintln!("{label}: Reynolds-averaged witness valid + invariant ({} monomials)", avg.len());
1277        }
1278    }
1279
1280    /// **`GF(4)` is a genuine field, checked exhaustively — every axiom over every tuple.** The 2-bit
1281    /// encoding `a + b·ω` with `ω² = ω + 1`: commutativity and identities over all 16 pairs,
1282    /// associativity and distributivity over all 64 triples, unique inverses, characteristic 2
1283    /// (`x + x = 0`), the defining quadratic, and the Frobenius fixed-point identity `x⁴ = x` that pins
1284    /// `GF(4)` as the 4-element field rather than the (non-field) ring `ℤ/4` — where `2·2 = 0` makes 2 a
1285    /// zero divisor, the exact reason "mod 4" is not a field and the extension construction is forced.
1286    #[test]
1287    fn gf4_arithmetic_satisfies_the_field_axioms_exhaustively() {
1288        let f = NsField::Gf4;
1289        let w = 2u64; // ω
1290        assert_eq!(f.characteristic(), 2);
1291        assert_eq!(f.order(), 4);
1292        assert_eq!(f.mul(w, w), f.add(w, 1), "the defining quadratic: ω² = ω + 1");
1293        for a in 0..4u64 {
1294            assert_eq!(f.add(a, 0), a, "additive identity");
1295            assert_eq!(f.mul(a, 1), a, "multiplicative identity");
1296            assert_eq!(f.add(a, a), 0, "characteristic 2");
1297            assert_eq!(f.mul(f.mul(f.mul(a, a), a), a), a, "Frobenius: x⁴ = x");
1298            if a != 0 {
1299                assert_eq!(f.mul(a, f.inv(a)), 1, "unique multiplicative inverse");
1300            }
1301            for b in 0..4u64 {
1302                assert_eq!(f.add(a, b), f.add(b, a), "commutative addition");
1303                assert_eq!(f.mul(a, b), f.mul(b, a), "commutative multiplication");
1304                if a != 0 && b != 0 {
1305                    assert_ne!(f.mul(a, b), 0, "a field has no zero divisors");
1306                }
1307                for c in 0..4u64 {
1308                    assert_eq!(f.add(f.add(a, b), c), f.add(a, f.add(b, c)), "associative +");
1309                    assert_eq!(f.mul(f.mul(a, b), c), f.mul(a, f.mul(b, c)), "associative ×");
1310                    assert_eq!(
1311                        f.mul(a, f.add(b, c)),
1312                        f.add(f.mul(a, b), f.mul(a, c)),
1313                        "distributivity"
1314                    );
1315                }
1316            }
1317        }
1318        // The contrast that motivates the extension: ℤ/4 is NOT a field — 2 is a zero divisor there.
1319        assert_eq!((2u64 * 2) % 4, 0, "in ℤ/4, 2·2 = 0 — the ring mod 4 is not GF(4)");
1320    }
1321
1322    /// **The extension-field collapse, measured across the whole small census: `GF(4)` buys NOTHING.**
1323    /// NS degree depends only on the characteristic, not the field size — so the "field ladder" is
1324    /// really the PRIME ladder, and shifting `GF(2) → GF(4)` (unlike `GF(2) → GF(3)`) changes no
1325    /// verdict. Measured exhaustively: every minimal-UNSAT orbit representative at `n = 1, 2, 3` (48
1326    /// covers), plus PHP(3) and the Count_3(4) CNF, has *identical* minimum NS degree under the `GF(4)`
1327    /// engine and the specialized `GF(2)` engine — and identical refutation verdicts at every
1328    /// intermediate degree, not just at the minimum.
1329    #[test]
1330    fn ns_degree_over_gf4_equals_ns_degree_over_gf2_across_the_small_census() {
1331        let f4 = NsField::Gf4;
1332        let mut corpus: Vec<(usize, Vec<Vec<Lit>>)> = Vec::new();
1333        for n in 1..=3usize {
1334            for cover in crate::hypercube::minimal_cover_orbits(n) {
1335                corpus.push((n, cover.clauses()));
1336            }
1337        }
1338        let (php3, _) = crate::families::php(3);
1339        corpus.push((php3.num_vars, php3.clauses));
1340        let (cnt34, _) = crate::families::mod_counting(4, 3);
1341        corpus.push((cnt34.num_vars, cnt34.clauses));
1342        let mut checked = 0usize;
1343        for (nv, clauses) in &corpus {
1344            let mut min_gf4 = None;
1345            let mut min_gf2 = None;
1346            for d in 1..=*nv {
1347                let v4 = ns_refutes_gfp(f4, *nv, clauses, d);
1348                let v2 = crate::polycalc::nullstellensatz_refutes(*nv, clauses, d);
1349                assert_eq!(v4, v2, "n={nv} d={d}: GF(4) and GF(2) verdicts agree everywhere");
1350                if v4 && min_gf4.is_none() {
1351                    min_gf4 = Some(d);
1352                }
1353                if v2 && min_gf2.is_none() {
1354                    min_gf2 = Some(d);
1355                }
1356            }
1357            assert_eq!(min_gf4, min_gf2, "n={nv}: the minimum NS degree collapses to characteristic 2");
1358            assert!(min_gf4.is_some(), "every census cover is UNSAT and refuted by degree n");
1359            checked += 1;
1360        }
1361        assert!(checked >= 50, "the sweep covered the census plus the named families ({checked})");
1362        eprintln!("GF(4) ≡ GF(2) on all {checked} covers — the field ladder is the prime ladder");
1363    }
1364
1365    /// **The collapse is CONSTRUCTIVE: a `GF(4)` certificate projects coefficient-wise to a re-checking
1366    /// `GF(2)` certificate.** `λ : a + b·ω ↦ a` is `GF(2)`-linear with `λ(1) = 1`, and clause
1367    /// polynomials have prime-subfield coefficients, so `λ` slides through `Σ_C p_C · g_C = 1`
1368    /// monomial-by-monomial. Verified end-to-end: extract a provenance-tracked `GF(4)` certificate at
1369    /// the minimal degree (it re-checks in-ring and corner-wise over `GF(4)`), project it, and the image
1370    /// re-checks as a `GF(2)` certificate of no larger degree — through BOTH the general engine's
1371    /// verifier at `Prime(2)` and the corner evaluation. The projection also refuses non-`GF(4)` input
1372    /// (fail-closed) rather than silently projecting a prime-field certificate.
1373    #[test]
1374    fn a_gf4_certificate_projects_coefficientwise_to_a_rechecking_gf2_certificate() {
1375        let f4 = NsField::Gf4;
1376        let p = |v: u32| Lit::pos(v);
1377        let q = |v: u32| Lit::neg(v);
1378        let xor_core = vec![
1379            vec![q(0), p(1)], vec![p(0), q(1)],
1380            vec![q(1), p(2)], vec![p(1), q(2)],
1381            vec![p(0), p(2)], vec![q(0), q(2)],
1382        ];
1383        let (php3, _) = crate::families::php(3);
1384        let (cnt34, _) = crate::families::mod_counting(4, 3);
1385        let corpus: Vec<(usize, Vec<Vec<Lit>>)> = vec![
1386            (3, xor_core),
1387            (php3.num_vars, php3.clauses),
1388            (cnt34.num_vars, cnt34.clauses),
1389        ];
1390        for (nv, clauses) in &corpus {
1391            let min_d = (1..=*nv)
1392                .find(|&d| ns_refutes_gfp(f4, *nv, clauses, d))
1393                .expect("every corpus formula is UNSAT");
1394            let cert4 = ns_certificate_at_degree_gfp(f4, *nv, clauses, min_d)
1395                .expect("a certificate exists exactly where the decision engine refutes");
1396            assert_eq!(cert4.field(), NsField::Gf4);
1397            assert!(cert4.verify(clauses), "n={nv}: the GF(4) certificate re-checks (ring + corners)");
1398            assert!(cert4.degree() <= min_d, "n={nv}: the extracted certificate respects the degree");
1399            let cert2 = project_gf4_certificate_to_gf2(&cert4).expect("projection of a Gf4 certificate");
1400            assert_eq!(cert2.field(), NsField::Prime(2));
1401            assert!(
1402                cert2.verify(clauses),
1403                "n={nv}: the λ-projected certificate re-checks over GF(2) — the constructive collapse"
1404            );
1405            assert!(cert2.degree() <= cert4.degree(), "n={nv}: projection never raises the degree");
1406            // Fail-closed: projecting a prime-field certificate is refused, not silently accepted.
1407            assert!(project_gf4_certificate_to_gf2(&cert2).is_none());
1408            // And extraction is fail-closed below the minimum degree: no certificate is fabricated.
1409            if min_d > 1 {
1410                assert!(ns_certificate_at_degree_gfp(f4, *nv, clauses, min_d - 1).is_none());
1411            }
1412            eprintln!(
1413                "n={nv}: GF(4) certificate at degree {min_d} projects to a GF(2) certificate (degree {})",
1414                cert2.degree()
1415            );
1416        }
1417    }
1418
1419    fn sat(num_vars: usize, clauses: &[Vec<Lit>]) -> bool {
1420        (0u64..(1u64 << num_vars)).any(|x| {
1421            clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
1422        })
1423    }
1424
1425    /// **The general engine at `p = 2` IS the specialized `GF(2)` engine — the differential anchor.**
1426    /// The bitset engine ([`crate::polycalc`]) is coefficient-free XOR; this engine carries explicit
1427    /// coefficients and signed indicators. At `p = 2` the two must coincide exactly: refutation verdicts
1428    /// at every degree, witness existence (the duality), and each engine's witness passing the *other*
1429    /// engine's zero-trust checker. Corpus: pigeonhole, modular counting, a transitive-XOR core, and a
1430    /// deterministic random-3-CNF sweep. Once this holds, every claim the general engine makes at odd `p`
1431    /// stands on machinery already pinned to the paper's `GF(2)` results.
1432    #[test]
1433    fn the_general_engine_at_p_two_agrees_with_the_specialized_gf2_engine() {
1434        let f = NsField::Prime(2);
1435        let mut corpus: Vec<(usize, Vec<Vec<Lit>>)> = Vec::new();
1436        let (php3, _) = crate::families::php(3);
1437        corpus.push((php3.num_vars, php3.clauses));
1438        let (cnt32, _) = crate::families::mod_counting(3, 2);
1439        corpus.push((cnt32.num_vars, cnt32.clauses));
1440        let p = |v: u32| Lit::pos(v);
1441        let q = |v: u32| Lit::neg(v);
1442        corpus.push((3, vec![
1443            vec![q(0), p(1)], vec![p(0), q(1)],
1444            vec![q(1), p(2)], vec![p(1), q(2)],
1445            vec![p(0), p(2)], vec![q(0), q(2)],
1446        ]));
1447        let mut seed = 0xD1FF_A4C4u64;
1448        for _ in 0..16 {
1449            let nv = 4 + (lcg(&mut seed) % 3) as usize; // 4..=6 variables
1450            let nc = 6 + (lcg(&mut seed) % 10) as usize;
1451            let mut cl = Vec::new();
1452            for _ in 0..nc {
1453                let mut vars: Vec<u32> = Vec::new();
1454                while vars.len() < 3 {
1455                    let v = (lcg(&mut seed) % nv as u64) as u32;
1456                    if !vars.contains(&v) {
1457                        vars.push(v);
1458                    }
1459                }
1460                cl.push(vars.iter().map(|&v| Lit::new(v, lcg(&mut seed) & 1 == 1)).collect());
1461            }
1462            corpus.push((nv, cl));
1463        }
1464
1465        for (nv, clauses) in &corpus {
1466            for d in 1..=(*nv).min(4) {
1467                let gf2_verdict = crate::polycalc::nullstellensatz_refutes(*nv, clauses, d);
1468                assert_eq!(
1469                    ns_refutes_gfp(f, *nv, clauses, d),
1470                    gf2_verdict,
1471                    "n={nv} d={d}: the general engine at p=2 matches the bitset engine"
1472                );
1473                let w_gf2 = crate::polycalc::ns_lower_bound_witness(*nv, clauses, d);
1474                let w_gen = ns_lower_bound_witness_gfp(f, *nv, clauses, d);
1475                assert_eq!(
1476                    w_gf2.is_some(),
1477                    w_gen.is_some(),
1478                    "n={nv} d={d}: witness existence agrees across the engines"
1479                );
1480                assert_eq!(w_gen.is_none(), gf2_verdict, "n={nv} d={d}: the duality holds");
1481                if let (Some(wc), Some(wp)) = (w_gf2, w_gen) {
1482                    let wc_as_pairs: Vec<(Mono, u64)> = wc.iter().map(|&m| (m, 1)).collect();
1483                    assert!(
1484                        check_ns_lower_bound_gfp(f, *nv, clauses, d, &wc_as_pairs),
1485                        "n={nv} d={d}: the bitset engine's witness passes the general checker"
1486                    );
1487                    assert!(wp.iter().all(|&(_, v)| v == 1), "p=2 witness values are all 1");
1488                    let wp_as_monos: Vec<u64> = wp.iter().map(|&(m, _)| m).collect();
1489                    assert!(
1490                        crate::polycalc::check_ns_lower_bound(*nv, clauses, d, &wp_as_monos),
1491                        "n={nv} d={d}: the general engine's witness passes the bitset checker"
1492                    );
1493                }
1494            }
1495        }
1496    }
1497
1498    /// **The completeness construction is field-generic: total, sound, and fail-closed over `GF(3)`.**
1499    /// The partition-of-unity charging never sees the characteristic — every UNSAT formula gets a
1500    /// certificate whose signed coefficients re-check (in the ring AND corner-by-corner), every SAT
1501    /// formula gets a model that satisfies it, and a certificate refuses to verify a clause set it was
1502    /// not built for. Cross-checked against brute-force satisfiability throughout.
1503    #[test]
1504    fn build_ns_certificate_gfp_is_total_sound_and_fail_closed_over_gf3() {
1505        let f = NsField::Prime(3);
1506        let mut seed = 0x0BAD_5EEDu64;
1507        let mut unsat_seen = 0usize;
1508        for _ in 0..80 {
1509            let nv = 4 + (lcg(&mut seed) % 3) as usize; // 4..=6 variables
1510            let nc = nv + (lcg(&mut seed) % (2 * nv as u64)) as usize;
1511            let clauses: Vec<Vec<Lit>> = (0..nc)
1512                .map(|_| {
1513                    let width = 2 + (lcg(&mut seed) % 2) as usize;
1514                    let mut vars: Vec<u32> = Vec::new();
1515                    while vars.len() < width {
1516                        let v = (lcg(&mut seed) % nv as u64) as u32;
1517                        if !vars.contains(&v) {
1518                            vars.push(v);
1519                        }
1520                    }
1521                    vars.iter().map(|&v| Lit::new(v, lcg(&mut seed) & 1 == 1)).collect()
1522                })
1523                .collect();
1524            match build_ns_certificate_gfp(f, nv, &clauses) {
1525                Ok(cert) => {
1526                    unsat_seen += 1;
1527                    assert_eq!(cert.field(), f);
1528                    assert!(cert.verify(&clauses), "n={nv}: the GF(3) certificate re-checks");
1529                    assert!(cert.degree() <= nv, "n={nv}: certificate degree ≤ n");
1530                    assert!(!sat(nv, &clauses), "n={nv}: certificates only for genuinely UNSAT formulas");
1531                    assert!(
1532                        !cert.verify(&clauses[..clauses.len() - 1]),
1533                        "n={nv}: a certificate must not verify a different clause set"
1534                    );
1535                }
1536                Err(model) => {
1537                    assert!(sat(nv, &clauses), "n={nv}: SAT verdicts only for satisfiable formulas");
1538                    assert!(
1539                        clauses.iter().all(|c| c.iter().any(|l| model[l.var() as usize] == l.is_positive())),
1540                        "n={nv}: the returned model satisfies every clause"
1541                    );
1542                }
1543            }
1544        }
1545        assert!(unsat_seen >= 5, "the corpus exercises the UNSAT branch ({unsat_seen} instances)");
1546    }
1547
1548    /// **`GF(3)` degree lower bounds are certifiable and exactly dual to refutation — and the checker is
1549    /// genuinely zero-trust.** On a deterministic random corpus: a witness exists iff no degree-`d`
1550    /// refutation does, and every witness re-checks. Then the adversarial half: a witness with its
1551    /// normalization `L(1)` dropped, one rescaled by `2` (so `L(1) = 2 ≠ 1`), and one perturbed on a
1552    /// monomial inside an admitted generator product must all be REJECTED. Empty clauses fail closed
1553    /// across the API.
1554    #[test]
1555    fn gf3_degree_lower_bounds_are_certifiable_and_dual_to_refutation() {
1556        let f = NsField::Prime(3);
1557        let mut seed = 0xD0A1_0003u64;
1558        for _ in 0..60 {
1559            let nv = 3 + (lcg(&mut seed) % 3) as usize; // 3..=5
1560            let nc = 2 + (lcg(&mut seed) % 8) as usize;
1561            let clauses: Vec<Vec<Lit>> = (0..nc)
1562                .map(|_| {
1563                    let mut c = Vec::new();
1564                    for v in 0..nv {
1565                        if lcg(&mut seed) % 2 == 0 {
1566                            c.push(Lit::new(v as u32, lcg(&mut seed) % 2 == 0));
1567                        }
1568                    }
1569                    if c.is_empty() {
1570                        c.push(Lit::new((lcg(&mut seed) % nv as u64) as u32, lcg(&mut seed) % 2 == 0));
1571                    }
1572                    c
1573                })
1574                .collect();
1575            let gens: Vec<GfpPoly> = clauses.iter().map(|c| clause_polynomial_gfp(f, c)).collect();
1576            for d in 1..=nv {
1577                let refutes = ns_refutes_gfp(f, nv, &clauses, d);
1578                match ns_lower_bound_witness_gfp(f, nv, &clauses, d) {
1579                    Some(w) => {
1580                        assert!(!refutes, "a witness exists only when there is NO degree-{d} refutation");
1581                        assert!(
1582                            check_ns_lower_bound_gfp(f, nv, &clauses, d, &w),
1583                            "the GF(3) witness must re-check"
1584                        );
1585                        // Corruption 1: drop the normalization L(1) = 1.
1586                        let no_one: Vec<(Mono, u64)> =
1587                            w.iter().copied().filter(|&(m, _)| m != 0).collect();
1588                        assert!(
1589                            !check_ns_lower_bound_gfp(f, nv, &clauses, d, &no_one),
1590                            "a witness without L(1) = 1 is rejected"
1591                        );
1592                        // Corruption 2: rescale by 2 — every constraint still holds, but L(1) = 2 ≠ 1.
1593                        let scaled: Vec<(Mono, u64)> =
1594                            w.iter().map(|&(m, v)| (m, f.mul(v, 2))).collect();
1595                        assert!(
1596                            !check_ns_lower_bound_gfp(f, nv, &clauses, d, &scaled),
1597                            "a rescaled witness breaks the normalization and is rejected"
1598                        );
1599                        // Corruption 3: perturb L on a monomial inside an admitted generator product.
1600                        let target = gens.iter().find_map(|g| {
1601                            crate::polycalc::monomials_up_to_degree(nv, d).into_iter().find_map(|m| {
1602                                let prod = poly_mul_mono_gfp(f, g, m);
1603                                (!prod.is_empty() && gfp_poly_degree(&prod) <= d)
1604                                    .then(|| *prod.keys().next_back().unwrap())
1605                            })
1606                        });
1607                        if let Some(t) = target {
1608                            let mut perturbed: Vec<(Mono, u64)> = w
1609                                .iter()
1610                                .copied()
1611                                .filter(|&(m, _)| m != t)
1612                                .collect();
1613                            let old = w.iter().find(|&&(m, _)| m == t).map_or(0, |&(_, v)| v);
1614                            let bumped = f.add(old, 1);
1615                            if bumped != 0 {
1616                                perturbed.push((t, bumped));
1617                            }
1618                            assert!(
1619                                !check_ns_lower_bound_gfp(f, nv, &clauses, d, &perturbed),
1620                                "a witness perturbed on a constrained monomial is rejected"
1621                            );
1622                        }
1623                    }
1624                    None => assert!(refutes, "no witness ⟹ a degree-{d} refutation exists"),
1625                }
1626            }
1627        }
1628        // Empty clauses fail closed across the API.
1629        let with_empty: Vec<Vec<Lit>> = vec![vec![], vec![Lit::pos(0)]];
1630        assert!(ns_refutes_gfp(f, 1, &with_empty, 1), "an empty clause is 1 = 0 outright");
1631        assert!(ns_lower_bound_witness_gfp(f, 1, &with_empty, 1).is_none());
1632        assert!(!check_ns_lower_bound_gfp(f, 1, &with_empty, 1, &[(0, 1)]));
1633    }
1634
1635    /// **`gfp_solve` decides linear systems over `GF(p)` and both verdicts re-check.** Planted-solution
1636    /// systems (rhs generated from a random assignment) come back `Some`, and the returned solution — not
1637    /// necessarily the planted one — satisfies every equation by direct substitution. Duplicating a row
1638    /// with a shifted right-hand side manufactures `r = c` ∧ `r = c+1`, which must return `None`. The
1639    /// degenerate rows behave: `0 = 0` is consistent, `0 = 1` is not.
1640    #[test]
1641    fn gfp_solve_solutions_and_inconsistencies_both_recheck() {
1642        let mut seed = 0x5EED_5017u64;
1643        for &p in &[2u64, 3, 5, 7, 11] {
1644            let f = NsField::Prime(p);
1645            for _ in 0..60 {
1646                let nvars = 1 + (lcg(&mut seed) % 8) as usize;
1647                let planted: Vec<u64> = (0..nvars).map(|_| lcg(&mut seed) % p).collect();
1648                let rows = 1 + (lcg(&mut seed) % 10) as usize;
1649                let mut eqs: Vec<(Vec<(usize, u64)>, u64)> = Vec::new();
1650                for _ in 0..rows {
1651                    let coeffs: Vec<(usize, u64)> = (0..nvars)
1652                        .filter_map(|v| {
1653                            let c = lcg(&mut seed) % p;
1654                            (c != 0).then_some((v, c))
1655                        })
1656                        .collect();
1657                    let rhs =
1658                        coeffs.iter().fold(0u64, |acc, &(v, c)| f.add(acc, f.mul(c, planted[v])));
1659                    eqs.push((coeffs, rhs));
1660                }
1661                let x = gfp_solve(f, &eqs, nvars).expect("a planted-solution system is consistent");
1662                for (coeffs, rhs) in &eqs {
1663                    let lhs = coeffs.iter().fold(0u64, |acc, &(v, c)| f.add(acc, f.mul(c, x[v])));
1664                    assert_eq!(lhs, *rhs, "p={p}: the returned solution satisfies every equation");
1665                }
1666                if let Some((coeffs, rhs)) = eqs.iter().find(|(c, _)| !c.is_empty()).cloned() {
1667                    let mut bad = eqs.clone();
1668                    bad.push((coeffs, f.add(rhs, 1)));
1669                    assert_eq!(
1670                        gfp_solve(f, &bad, nvars),
1671                        None,
1672                        "p={p}: the same row with a shifted rhs is inconsistent"
1673                    );
1674                }
1675            }
1676            assert!(gfp_solve(f, &[(vec![], 0)], 3).is_some(), "p={p}: 0 = 0 is consistent");
1677            assert_eq!(gfp_solve(f, &[(vec![], 1)], 3), None, "p={p}: 0 = 1 is a contradiction");
1678        }
1679    }
1680}