1use crate::cdcl::Lit;
20pub use crate::polycalc::Mono;
21use std::collections::BTreeMap;
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum NsField {
27 Prime(u64),
28 Gf4,
29}
30
31pub type GfpPoly = BTreeMap<Mono, u64>;
35
36impl NsField {
37 pub fn order(self) -> u64 {
39 match self {
40 NsField::Prime(p) => p,
41 NsField::Gf4 => 4,
42 }
43 }
44
45 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 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 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] }
104 }
105 }
106
107 pub fn embed_int(self, n: u128) -> u64 {
110 (n % self.characteristic() as u128) as u64
111 }
112}
113
114fn 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
128fn 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
140pub fn gfp_poly_degree(p: &GfpPoly) -> usize {
142 p.keys().map(|m| m.count_ones() as usize).max().unwrap_or(0)
143}
144
145pub 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() } else {
156 [(bit, 1u64)].into_iter().collect() };
158 p = poly_mul(f, &p, &indicator);
159 }
160 p
161}
162
163fn 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); if sub == 0 {
175 break;
176 }
177 sub = (sub - 1) & zeros;
178 }
179 p
180}
181
182pub 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
191pub 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
203pub 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(); 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 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; }
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 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
265fn 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
274pub 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; }
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 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
332pub 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#[derive(Clone, Debug)]
346pub struct NsCertificateGfp {
347 field: NsField,
348 num_vars: usize,
349 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 pub fn degree(&self) -> usize {
364 self.coeffs.iter().map(gfp_poly_degree).max().unwrap_or(0)
365 }
366
367 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
405pub 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
434pub 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)); }
459 }
460 }
461 eqs.push((vec![(index[&0u64], 1)], 1)); 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
466pub 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
481pub 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)?; 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
521pub 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; }
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; }
551 }
552 }
553 }
554 true
555}
556
557pub 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
585pub 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; }
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>; 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 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; }
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
679pub 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
697pub 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
715pub 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; }
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
734pub 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 fn lcg(state: &mut u64) -> u64 {
755 *state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
756 *state >> 33
757 }
758
759 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 #[test]
774 fn gf3_clause_polynomial_is_the_signed_false_indicator_on_every_corner() {
775 let f3 = NsField::Prime(3);
776 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; 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 #[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 #[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 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 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 #[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 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 #[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 #[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; 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 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 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 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 #[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 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 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 #[test]
1076 fn php_gf3_ns_degree_is_measured_and_its_lower_half_certified() {
1077 let f3 = NsField::Prime(3);
1078 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 assert!(crate::polycalc::nullstellensatz_refutes(nv, &php3.clauses, 4));
1097 assert!(!crate::polycalc::nullstellensatz_refutes(nv, &php3.clauses, 3));
1098 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 #[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 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; 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 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 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 #[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 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 #[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 #[test]
1287 fn gf4_arithmetic_satisfies_the_field_axioms_exhaustively() {
1288 let f = NsField::Gf4;
1289 let w = 2u64; 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 assert_eq!((2u64 * 2) % 4, 0, "in ℤ/4, 2·2 = 0 — the ring mod 4 is not GF(4)");
1320 }
1321
1322 #[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 #[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 assert!(project_gf4_certificate_to_gf2(&cert2).is_none());
1408 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 #[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; 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 #[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; 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 #[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; 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 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 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 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 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 #[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}