1use crate::cdcl::Lit;
29pub use crate::polycalc::Mono;
30use std::collections::BTreeMap;
31
32pub type ZmPoly = BTreeMap<Mono, u64>;
34
35#[inline]
36fn zm_add(m: u64, a: u64, b: u64) -> u64 {
37 (a % m + b % m) % m
38}
39#[inline]
40fn zm_neg(m: u64, a: u64) -> u64 {
41 (m - a % m) % m
42}
43#[inline]
44fn zm_mul(m: u64, a: u64, b: u64) -> u64 {
45 (a % m) * (b % m) % m
46}
47
48fn gcd(a: u64, b: u64) -> u64 {
49 let (mut a, mut b) = (a, b);
50 while b != 0 {
51 let t = a % b;
52 a = b;
53 b = t;
54 }
55 a
56}
57
58fn egcd(a: i128, b: i128) -> (i128, i128, i128) {
60 if b == 0 {
61 (a, 1, 0)
62 } else {
63 let (g, x, y) = egcd(b, a % b);
64 (g, y, x - (a / b) * y)
65 }
66}
67
68fn unit_scaling(a: u64, m: u64) -> u64 {
73 let a = a % m;
74 debug_assert!(a != 0, "the zero pivot has no scaling");
75 let d = gcd(a, m);
76 let (ap, mp) = (a / d, m / d);
77 if mp == 1 {
78 return 1; }
80 let (_, x, _) = egcd(ap as i128, mp as i128);
81 let u0 = ((x % mp as i128 + mp as i128) % mp as i128) as u64;
82 for k in 0..d {
83 let u = u0 + k * mp;
84 if u != 0 && gcd(u, m) == 1 {
85 return u;
86 }
87 }
88 unreachable!("a unit lift of (a/d)⁻¹ mod m/d always exists below m")
89}
90
91fn add_term(m: u64, p: &mut ZmPoly, mono: Mono, c: u64) {
92 let c = c % m;
93 if c == 0 {
94 return;
95 }
96 let e = p.entry(mono).or_insert(0);
97 *e = zm_add(m, *e, c);
98 if *e == 0 {
99 p.remove(&mono);
100 }
101}
102
103fn poly_mul(m: u64, a: &ZmPoly, b: &ZmPoly) -> ZmPoly {
104 let mut r = ZmPoly::new();
105 for (&ma, &ca) in a {
106 for (&mb, &cb) in b {
107 add_term(m, &mut r, ma | mb, zm_mul(m, ca, cb));
108 }
109 }
110 r
111}
112
113fn poly_mul_mono(m: u64, p: &ZmPoly, mono: Mono) -> ZmPoly {
114 let mut r = ZmPoly::new();
115 for (&t, &c) in p {
116 add_term(m, &mut r, t | mono, c);
117 }
118 r
119}
120
121pub fn zm_poly_degree(p: &ZmPoly) -> usize {
123 p.keys().map(|mo| mo.count_ones() as usize).max().unwrap_or(0)
124}
125
126pub fn clause_polynomial_zm(m: u64, clause: &[Lit]) -> ZmPoly {
130 let mut p: ZmPoly = [(0u64, 1u64)].into_iter().collect();
131 for l in clause {
132 let bit = 1u64 << l.var();
133 let indicator: ZmPoly = if l.is_positive() {
134 [(0u64, 1u64), (bit, zm_neg(m, 1))].into_iter().collect()
135 } else {
136 [(bit, 1u64)].into_iter().collect()
137 };
138 p = poly_mul(m, &p, &indicator);
139 }
140 p
141}
142
143fn point_indicator_zm(m: u64, a: u64, num_vars: usize) -> ZmPoly {
145 let mask = (1u64 << num_vars).wrapping_sub(1);
146 let ones = a & mask;
147 let zeros = !a & mask;
148 let mut p = ZmPoly::new();
149 let mut sub = zeros;
150 loop {
151 let sign = if sub.count_ones() % 2 == 0 { 1 } else { zm_neg(m, 1) };
152 p.insert(ones | sub, sign);
153 if sub == 0 {
154 break;
155 }
156 sub = (sub - 1) & zeros;
157 }
158 p
159}
160
161pub fn pou_atom_zm(m: u64, v: usize) -> ZmPoly {
164 let mut atom: ZmPoly = [(0u64, 1u64), (1u64 << v, zm_neg(m, 1))].into_iter().collect();
165 add_term(m, &mut atom, 1u64 << v, 1);
166 atom
167}
168
169pub fn partition_of_unity_zm(m: u64, n: usize) -> ZmPoly {
171 let mut sum = ZmPoly::new();
172 for a in 0..(1u64 << n) {
173 for (mo, c) in point_indicator_zm(m, a, n) {
174 add_term(m, &mut sum, mo, c);
175 }
176 }
177 sum
178}
179
180#[derive(Clone, Debug)]
182pub struct NsCertificateZm {
183 modulus: u64,
184 num_vars: usize,
185 coeffs: Vec<ZmPoly>,
186}
187
188impl NsCertificateZm {
189 pub fn modulus(&self) -> u64 {
190 self.modulus
191 }
192
193 pub fn num_vars(&self) -> usize {
194 self.num_vars
195 }
196
197 pub fn degree(&self) -> usize {
199 self.coeffs.iter().map(zm_poly_degree).max().unwrap_or(0)
200 }
201
202 pub fn coeff_monomial_count(&self) -> usize {
205 self.coeffs.iter().map(|g| g.len()).sum()
206 }
207
208 pub fn verify(&self, clauses: &[Vec<Lit>]) -> bool {
212 if self.coeffs.len() != clauses.len() {
213 return false;
214 }
215 let m = self.modulus;
216 let mut sum = ZmPoly::new();
217 for (c, g) in clauses.iter().zip(&self.coeffs) {
218 if g.is_empty() {
219 continue;
220 }
221 for (mo, co) in poly_mul(m, &clause_polynomial_zm(m, c), g) {
222 add_term(m, &mut sum, mo, co);
223 }
224 }
225 if !(sum.len() == 1 && sum.get(&0u64) == Some(&1)) {
226 return false;
227 }
228 if self.num_vars <= 20 {
229 let eval = |p: &ZmPoly, a: u64| -> u64 {
230 p.iter()
231 .fold(0u64, |acc, (&mo, &c)| if mo & !a == 0 { zm_add(m, acc, c) } else { acc })
232 };
233 for a in 0u64..(1u64 << self.num_vars) {
234 let total = clauses.iter().zip(&self.coeffs).fold(0u64, |acc, (c, g)| {
235 zm_add(m, acc, zm_mul(m, eval(&clause_polynomial_zm(m, c), a), eval(g, a)))
236 });
237 if total != 1 {
238 return false;
239 }
240 }
241 }
242 true
243 }
244}
245
246pub fn build_ns_certificate_zm(
253 m: u64,
254 num_vars: usize,
255 clauses: &[Vec<Lit>],
256) -> Result<NsCertificateZm, Vec<bool>> {
257 assert!(m >= 2, "a modulus needs at least two residues");
258 assert!(num_vars <= 20, "the explicit-corner construction is bounded to num_vars ≤ 20");
259 let mut coeffs: Vec<ZmPoly> = vec![ZmPoly::new(); clauses.len()];
260 for a in 0u64..(1u64 << num_vars) {
261 let sel = clauses
262 .iter()
263 .position(|c| !c.iter().any(|l| ((a >> l.var()) & 1 == 1) == l.is_positive()));
264 match sel {
265 None => return Err((0..num_vars).map(|i| (a >> i) & 1 == 1).collect()),
266 Some(ci) => {
267 for (mo, c) in point_indicator_zm(m, a, num_vars) {
268 add_term(m, &mut coeffs[ci], mo, c);
269 }
270 }
271 }
272 }
273 Ok(NsCertificateZm { modulus: m, num_vars, coeffs })
274}
275
276struct ZmEchelon {
282 m: u64,
283 pivots: std::collections::HashMap<usize, Vec<u64>>,
284}
285
286fn row_lead(row: &[u64]) -> Option<usize> {
287 row.iter().rposition(|&c| c != 0)
288}
289
290impl ZmEchelon {
291 fn new(m: u64) -> Self {
292 ZmEchelon { m, pivots: std::collections::HashMap::new() }
293 }
294
295 fn insert(&mut self, mut row: Vec<u64>) {
296 let m = self.m;
297 loop {
298 let Some(c) = row_lead(&row) else { return };
299 let Some(pivot) = self.pivots.get(&c) else {
300 let u = unit_scaling(row[c], m);
302 for v in 0..=c {
303 row[v] = zm_mul(m, row[v], u);
304 }
305 let d = row[c];
306 debug_assert!(m % d == 0, "a normalized pivot divides the modulus");
307 let ann: Vec<u64> = row.iter().map(|&x| zm_mul(m, x, m / d)).collect();
308 self.pivots.insert(c, row);
309 if ann.iter().any(|&x| x != 0) {
310 self.insert(ann); }
312 return;
313 };
314 let d = pivot[c];
315 let a = row[c];
316 if a % d == 0 {
317 let f = a / d;
319 let pivot = pivot.clone();
320 for v in 0..=c {
321 row[v] = zm_add(m, row[v], zm_neg(m, zm_mul(m, pivot[v], f)));
322 }
323 } else {
324 let (g0, x, y) = egcd(d as i128, a as i128);
326 let g = g0 as u64; let (s, t) = (
328 ((x % m as i128 + m as i128) % m as i128) as u64,
329 ((y % m as i128 + m as i128) % m as i128) as u64,
330 );
331 let pivot = pivot.clone();
332 let comb: Vec<u64> = (0..=c)
333 .map(|v| zm_add(m, zm_mul(m, s, pivot[v]), zm_mul(m, t, row[v])))
334 .collect();
335 debug_assert_eq!(comb[c], g % m, "the combined pivot leads with the gcd");
336 let rem_p: Vec<u64> = (0..=c)
337 .map(|v| zm_add(m, pivot[v], zm_neg(m, zm_mul(m, comb[v], d / g))))
338 .collect();
339 let rem_r: Vec<u64> = (0..=c)
340 .map(|v| zm_add(m, row[v], zm_neg(m, zm_mul(m, comb[v], a / g))))
341 .collect();
342 let ann: Vec<u64> = comb.iter().map(|&v| zm_mul(m, v, m / g)).collect();
343 self.pivots.insert(c, comb);
344 if rem_p.iter().any(|&v| v != 0) {
345 self.insert(rem_p);
346 }
347 if ann.iter().any(|&v| v != 0) {
348 self.insert(ann);
349 }
350 row = rem_r;
351 }
352 }
353 }
354
355 fn contains(&self, mut target: Vec<u64>) -> bool {
356 let m = self.m;
357 while let Some(c) = row_lead(&target) {
358 let Some(pivot) = self.pivots.get(&c) else { return false };
359 let d = pivot[c];
360 if target[c] % d != 0 {
361 return false; }
363 let f = target[c] / d;
364 for v in 0..=c {
365 target[v] = zm_add(m, target[v], zm_neg(m, zm_mul(m, pivot[v], f)));
366 }
367 }
368 true
369 }
370}
371
372pub fn ns_refutes_polys_zm(m: u64, num_vars: usize, gens: &[ZmPoly], degree: usize) -> bool {
377 let basis = crate::polycalc::monomials_up_to_degree(num_vars, degree);
378 let index: std::collections::HashMap<Mono, usize> =
379 basis.iter().enumerate().map(|(i, &mo)| (mo, i)).collect();
380 let nb = basis.len();
381 let mut ech = ZmEchelon::new(m);
382 for g in gens {
383 if g.is_empty() {
384 continue;
385 }
386 for &mo in &basis {
387 let prod = poly_mul_mono(m, g, mo);
388 if !prod.is_empty() && zm_poly_degree(&prod) <= degree {
389 let mut row = vec![0u64; nb];
390 for (t, c) in prod {
391 row[index[&t]] = c;
392 }
393 ech.insert(row);
394 }
395 }
396 }
397 let mut target = vec![0u64; nb];
398 target[index[&0u64]] = 1;
399 ech.contains(target)
400}
401
402pub fn ns_refutes_zm(m: u64, num_vars: usize, clauses: &[Vec<Lit>], degree: usize) -> bool {
405 if clauses.iter().any(|c| c.is_empty()) {
406 return true;
407 }
408 let gens: Vec<ZmPoly> = clauses.iter().map(|c| clause_polynomial_zm(m, c)).collect();
409 ns_refutes_polys_zm(m, num_vars, &gens, degree)
410}
411
412pub fn check_ns_lower_bound_polys_zm(
418 m: u64,
419 num_vars: usize,
420 gens: &[ZmPoly],
421 degree: usize,
422 witness: &[(Mono, u64)],
423) -> bool {
424 let mut l: BTreeMap<Mono, u64> = BTreeMap::new();
425 for &(mo, v) in witness {
426 add_term(m, &mut l, mo, v);
427 }
428 if l.get(&0u64).copied().unwrap_or(0) == 0 {
429 return false; }
431 let value = |mo: &Mono| l.get(mo).copied().unwrap_or(0);
432 for g in gens {
433 if g.is_empty() {
434 continue;
435 }
436 for &mo in &crate::polycalc::monomials_up_to_degree(num_vars, degree) {
437 let prod = poly_mul_mono(m, g, mo);
438 if !prod.is_empty() && zm_poly_degree(&prod) <= degree {
439 let pairing =
440 prod.iter().fold(0u64, |acc, (t, &c)| zm_add(m, acc, zm_mul(m, c, value(t))));
441 if pairing != 0 {
442 return false;
443 }
444 }
445 }
446 }
447 true
448}
449
450pub fn check_ns_lower_bound_zm(
452 m: u64,
453 num_vars: usize,
454 clauses: &[Vec<Lit>],
455 degree: usize,
456 witness: &[(Mono, u64)],
457) -> bool {
458 if clauses.iter().any(|c| c.is_empty()) {
459 return false;
460 }
461 let gens: Vec<ZmPoly> = clauses.iter().map(|c| clause_polynomial_zm(m, c)).collect();
462 check_ns_lower_bound_polys_zm(m, num_vars, &gens, degree, witness)
463}
464
465pub fn lift_prime_witness_to_zm(m: u64, p: u64, witness_p: &[(Mono, u64)]) -> Vec<(Mono, u64)> {
472 assert!(p >= 2 && m % p == 0, "the lift needs a prime divisor of the modulus");
473 let scale = m / p;
474 witness_p
475 .iter()
476 .filter_map(|&(mo, v)| {
477 let lifted = zm_mul(m, scale, v % p);
478 (lifted != 0).then_some((mo, lifted))
479 })
480 .collect()
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486
487 fn lcg(state: &mut u64) -> u64 {
489 *state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
490 *state >> 33
491 }
492
493 fn eval_at(m: u64, p: &ZmPoly, a: u64) -> u64 {
494 p.iter().fold(0u64, |acc, (&mo, &c)| if mo & !a == 0 { zm_add(m, acc, c) } else { acc })
495 }
496
497 fn falsifies(clause: &[Lit], a: u64) -> bool {
498 !clause.iter().any(|l| ((a >> l.var()) & 1 == 1) == l.is_positive())
499 }
500
501 fn sat(num_vars: usize, clauses: &[Vec<Lit>]) -> bool {
502 (0u64..(1u64 << num_vars)).any(|x| {
503 clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
504 })
505 }
506
507 struct AddGroup {
509 name: &'static str,
510 n: usize,
511 add: Vec<Vec<usize>>,
512 }
513
514 fn cyclic_group(n: usize, name: &'static str) -> AddGroup {
515 let add = (0..n).map(|a| (0..n).map(|b| (a + b) % n).collect()).collect();
516 AddGroup { name, n, add }
517 }
518
519 fn product_group(p: usize, q: usize, name: &'static str) -> AddGroup {
521 let n = p * q;
522 let add = (0..n)
523 .map(|a| {
524 (0..n)
525 .map(|b| ((a / q + b / q) % p) * q + ((a % q + b % q) % q))
526 .collect()
527 })
528 .collect();
529 AddGroup { name, n, add }
530 }
531
532 fn scalar_mul(g: &AddGroup, k: usize, x: usize) -> usize {
533 (0..k).fold(0, |acc, _| g.add[acc][x])
534 }
535
536 fn add_order(g: &AddGroup, x: usize) -> usize {
537 let mut acc = x;
538 let mut k = 1;
539 while acc != 0 {
540 acc = g.add[acc][x];
541 k += 1;
542 }
543 k
544 }
545
546 fn field_axiom_failure(g: &AddGroup, mul: &[Vec<usize>]) -> Option<&'static str> {
550 let n = g.n;
551 for a in 0..n {
552 for b in 0..n {
553 for c in 0..n {
554 if mul[a][g.add[b][c]] != g.add[mul[a][b]][mul[a][c]]
555 || mul[g.add[a][b]][c] != g.add[mul[a][c]][mul[b][c]]
556 {
557 return Some("distributivity");
558 }
559 }
560 }
561 }
562 for a in 0..n {
563 for b in 0..n {
564 if mul[a][b] != mul[b][a] {
565 return Some("commutativity");
566 }
567 }
568 }
569 for a in 0..n {
570 for b in 0..n {
571 for c in 0..n {
572 if mul[mul[a][b]][c] != mul[a][mul[b][c]] {
573 return Some("associativity");
574 }
575 }
576 }
577 }
578 let Some(e) = (0..n).find(|&e| (0..n).all(|x| mul[e][x] == x && mul[x][e] == x)) else {
579 return Some("no multiplicative identity");
580 };
581 for a in 1..n {
582 for b in 1..n {
583 if mul[a][b] == 0 {
584 return Some("a zero divisor");
585 }
586 }
587 }
588 if (1..n).any(|a| !(1..n).any(|b| mul[a][b] == e)) {
589 return Some("a nonzero element without an inverse");
590 }
591 None
592 }
593
594 #[test]
618 fn finite_fields_compose_exactly_at_prime_power_orders() {
619 let mut field_counts: std::collections::BTreeMap<(usize, &'static str), usize> =
620 std::collections::BTreeMap::new();
621 let mut census: std::collections::BTreeMap<(usize, &'static str), std::collections::BTreeMap<&'static str, usize>> =
622 std::collections::BTreeMap::new();
623 let mut gf4_iso_found = false;
624
625 for (order, g) in [
627 (4usize, cyclic_group(4, "ℤ/4")),
628 (6, cyclic_group(6, "ℤ/6")),
629 (9, cyclic_group(9, "ℤ/9")),
630 (10, cyclic_group(10, "ℤ/10")),
631 ] {
632 for u in 0..g.n {
633 let mul: Vec<Vec<usize>> =
634 (0..g.n).map(|a| (0..g.n).map(|b| scalar_mul(&g, a * b, u)).collect()).collect();
635 match field_axiom_failure(&g, &mul) {
636 None => *field_counts.entry((order, g.name)).or_insert(0) += 1,
637 Some(why) => {
638 *census.entry((order, g.name)).or_default().entry(why).or_insert(0) += 1
639 }
640 }
641 }
642 field_counts.entry((order, g.name)).or_insert(0);
643 }
644
645 for (order, p, q, name) in [
648 (4usize, 2usize, 2usize, "ℤ/2×ℤ/2"),
649 (6, 2, 3, "ℤ/2×ℤ/3"),
650 (9, 3, 3, "ℤ/3×ℤ/3"),
651 (10, 2, 5, "ℤ/2×ℤ/5"),
652 ] {
653 let g = product_group(p, q, name);
654 let (e1, e2) = (q, 1); let allowed = |bound: usize| -> Vec<usize> {
656 (0..g.n).filter(|&x| x == 0 || bound % add_order(&g, x) == 0).collect()
657 };
658 let gpq = gcd(p as u64, q as u64) as usize;
659 let (c11, c12, c21, c22) = (allowed(p), allowed(gpq), allowed(gpq), allowed(q));
660 if gpq == 1 {
662 assert_eq!(c12, vec![0], "{name}: bilinearity forces e1·e2 = 0 — the composed zero divisor");
663 assert_eq!(c21, vec![0], "{name}: bilinearity forces e2·e1 = 0");
664 }
665 for &m11 in &c11 {
666 for &m12 in &c12 {
667 for &m21 in &c21 {
668 for &m22 in &c22 {
669 let mul: Vec<Vec<usize>> = (0..g.n)
670 .map(|a| {
671 let (a1, a2) = (a / q, a % q);
672 (0..g.n)
673 .map(|b| {
674 let (b1, b2) = (b / q, b % q);
675 let mut acc = scalar_mul(&g, a1 * b1, m11);
676 acc = g.add[acc][scalar_mul(&g, a1 * b2, m12)];
677 acc = g.add[acc][scalar_mul(&g, a2 * b1, m21)];
678 g.add[acc][scalar_mul(&g, a2 * b2, m22)]
679 })
680 .collect()
681 })
682 .collect();
683 match field_axiom_failure(&g, &mul) {
684 None => {
685 *field_counts.entry((order, name)).or_insert(0) += 1;
686 if order == 4 && !gf4_iso_found {
688 let f = crate::polycalc_gfp::NsField::Gf4;
689 let perms: Vec<[usize; 4]> = vec![
690 [0, 1, 2, 3], [0, 1, 3, 2], [0, 2, 1, 3],
691 [0, 2, 3, 1], [0, 3, 1, 2], [0, 3, 2, 1],
692 ];
693 gf4_iso_found = perms.iter().any(|phi| {
694 (0..4).all(|a| {
695 (0..4).all(|b| {
696 phi[g.add[a][b]] as u64
697 == (phi[a] as u64 ^ phi[b] as u64)
698 && phi[mul[a][b]] as u64
699 == f.mul(phi[a] as u64, phi[b] as u64)
700 })
701 })
702 });
703 }
704 }
705 Some(why) => {
706 *census.entry((order, name)).or_default().entry(why).or_insert(0) += 1
707 }
708 }
709 }
710 }
711 }
712 }
713 field_counts.entry((order, name)).or_insert(0);
714 }
715
716 for ((order, name), reasons) in &census {
717 eprintln!("order {order} on {name}: candidate deaths {reasons:?}");
718 }
719 eprintln!("field structures found: {field_counts:?}");
720
721 assert_eq!(field_counts[&(4, "ℤ/4")], 0, "no field on the nilpotent additive group ℤ/4");
723 assert!(field_counts[&(4, "ℤ/2×ℤ/2")] > 0, "GF(4) composes from ℤ/2 × ℤ/2");
724 assert!(gf4_iso_found, "a composed order-4 field is isomorphic to the engine's NsField::Gf4");
725 assert_eq!(field_counts[&(9, "ℤ/9")], 0, "no field on ℤ/9");
726 assert!(field_counts[&(9, "ℤ/3×ℤ/3")] > 0, "GF(9) composes from ℤ/3 × ℤ/3");
727 for (order, presentations) in
729 [(6usize, ["ℤ/6", "ℤ/2×ℤ/3"]), (10, ["ℤ/10", "ℤ/2×ℤ/5"])]
730 {
731 for name in presentations {
732 assert_eq!(
733 field_counts[&(order, name)],
734 0,
735 "GF({order}) is impossible: zero survivors on {name}, exhaustively"
736 );
737 }
738 }
739 }
740
741 fn parity_triangle() -> Vec<Vec<Lit>> {
744 let p = |v: u32| Lit::pos(v);
745 let q = |v: u32| Lit::neg(v);
746 vec![
747 vec![p(0), p(1)], vec![q(0), q(1)],
748 vec![p(1), p(2)], vec![q(1), q(2)],
749 vec![p(2), p(0)], vec![q(2), q(0)],
750 ]
751 }
752
753 fn ring_corpus() -> Vec<(&'static str, usize, Vec<Vec<Lit>>)> {
755 let (php3, _) = crate::families::php(3);
756 let (cnt34, _) = crate::families::mod_counting(4, 3);
757 let (cnt23, _) = crate::families::mod_counting(3, 2);
758 let mut corpus: Vec<(&'static str, usize, Vec<Vec<Lit>>)> = vec![
759 ("parity", 3, parity_triangle()),
760 ("php3", php3.num_vars, php3.clauses),
761 ("cnt34", cnt34.num_vars, cnt34.clauses),
762 ("cnt23", cnt23.num_vars, cnt23.clauses),
763 ];
764 let mut seed = 0x0CA7_C047u64;
765 for _ in 0..8 {
766 let nv = 3 + (lcg(&mut seed) % 2) as usize;
767 let nc = 2 * nv + (lcg(&mut seed) % 6) as usize;
768 let clauses: Vec<Vec<Lit>> = (0..nc)
769 .map(|_| {
770 let width = 2 + (lcg(&mut seed) % 2) as usize;
771 let mut vars: Vec<u32> = Vec::new();
772 while vars.len() < width {
773 let v = (lcg(&mut seed) % nv as u64) as u32;
774 if !vars.contains(&v) {
775 vars.push(v);
776 }
777 }
778 vars.iter().map(|&v| Lit::new(v, lcg(&mut seed) & 1 == 1)).collect()
779 })
780 .collect();
781 corpus.push(("rand", nv, clauses));
782 }
783 corpus
784 }
785
786 #[test]
799 fn zm_ns_at_coprime_composite_moduli_is_the_conjunction_of_its_parts() {
800 use crate::polycalc_gfp::{ns_refutes_gfp, NsField};
801 for (name, nv, clauses) in &ring_corpus() {
802 for d in 1..=(*nv).min(4) {
803 let g2 = ns_refutes_gfp(NsField::Prime(2), *nv, clauses, d);
804 let g3 = ns_refutes_gfp(NsField::Prime(3), *nv, clauses, d);
805 assert_eq!(
806 ns_refutes_zm(6, *nv, clauses, d),
807 g2 && g3,
808 "{name} n={nv} d={d}: ℤ/6-NS = GF(2)-NS ∧ GF(3)-NS"
809 );
810 assert_eq!(
811 ns_refutes_zm(12, *nv, clauses, d),
812 ns_refutes_zm(4, *nv, clauses, d) && ns_refutes_zm(3, *nv, clauses, d),
813 "{name} n={nv} d={d}: ℤ/12-NS = ℤ/4-NS ∧ ℤ/3-NS"
814 );
815 }
816 }
817 let parity = parity_triangle();
820 assert!(ns_refutes_gfp(crate::polycalc_gfp::NsField::Prime(2), 3, &parity, 2));
821 assert!(!ns_refutes_zm(6, 3, &parity, 2), "ℤ/6 is blocked by its GF(3) part on parity");
822 let (php3, _) = crate::families::php(3);
824 assert!(!ns_refutes_zm(6, php3.num_vars, &php3.clauses, 3), "PHP(3): ℤ/6 degree > 3");
825 assert!(ns_refutes_zm(6, php3.num_vars, &php3.clauses, 4), "PHP(3): ℤ/6 degree = 4 = max(4, 4)");
826 }
827
828 #[test]
840 fn a_prime_witness_lifts_to_a_ring_witness_with_zero_divisor_normalization() {
841 use crate::polycalc_gfp::{ns_lower_bound_witness_gfp, NsField};
842 let (php3, _) = crate::families::php(3);
844 let w2 = ns_lower_bound_witness_gfp(NsField::Prime(2), php3.num_vars, &php3.clauses, 3)
845 .expect("PHP(3) has a GF(2) witness at degree 3");
846 let lifted2 = lift_prime_witness_to_zm(6, 2, &w2);
847 assert_eq!(
848 lifted2.iter().find(|&&(mo, _)| mo == 0).map(|&(_, v)| v),
849 Some(3),
850 "the GF(2) lift is normalized at the zero divisor L(1) = 6/2 = 3"
851 );
852 assert!(
853 check_ns_lower_bound_zm(6, php3.num_vars, &php3.clauses, 3, &lifted2),
854 "the lifted witness certifies ℤ/6-NS-degree(PHP(3)) > 3 with zero trust"
855 );
856 let parity = parity_triangle();
859 let w3 = ns_lower_bound_witness_gfp(NsField::Prime(3), 3, &parity, 2)
860 .expect("parity has a GF(3) witness at degree 2");
861 let lifted3 = lift_prime_witness_to_zm(6, 3, &w3);
862 assert_eq!(
863 lifted3.iter().find(|&&(mo, _)| mo == 0).map(|&(_, v)| v),
864 Some(2),
865 "the GF(3) lift is normalized at L(1) = 6/3 = 2"
866 );
867 assert!(
868 check_ns_lower_bound_zm(6, 3, &parity, 2, &lifted3),
869 "one prime part's witness certifies the ring lower bound"
870 );
871 let no_one: Vec<(Mono, u64)> = lifted2.iter().copied().filter(|&(mo, _)| mo != 0).collect();
874 assert!(!check_ns_lower_bound_zm(6, php3.num_vars, &php3.clauses, 3, &no_one));
875 let annihilated: Vec<(Mono, u64)> =
876 lifted2.iter().map(|&(mo, v)| (mo, zm_mul(6, v, 2))).collect(); assert!(!check_ns_lower_bound_zm(6, php3.num_vars, &php3.clauses, 3, &annihilated));
878 let gens: Vec<ZmPoly> =
879 php3.clauses.iter().map(|c| clause_polynomial_zm(6, c)).collect();
880 let target = gens
881 .iter()
882 .find_map(|g| {
883 crate::polycalc::monomials_up_to_degree(php3.num_vars, 3).into_iter().find_map(
884 |mo| {
885 let prod = poly_mul_mono(6, g, mo);
886 (!prod.is_empty() && zm_poly_degree(&prod) <= 3)
887 .then(|| *prod.keys().next_back().unwrap())
888 },
889 )
890 })
891 .expect("an admitted generator product exists");
892 let mut perturbed: Vec<(Mono, u64)> =
893 lifted2.iter().copied().filter(|&(mo, _)| mo != target).collect();
894 let old = lifted2.iter().find(|&&(mo, _)| mo == target).map_or(0, |&(_, v)| v);
895 perturbed.push((target, zm_add(6, old, 1)));
896 assert!(!check_ns_lower_bound_zm(6, php3.num_vars, &php3.clauses, 3, &perturbed));
897 }
898
899 #[test]
909 fn z4_is_strictly_weaker_than_its_residue_field_at_fixed_degree() {
910 use crate::polycalc_gfp::{ns_refutes_gfp, NsField};
911 for (name, nv, clauses) in &ring_corpus() {
913 for d in 1..=(*nv).min(4) {
914 if ns_refutes_zm(4, *nv, clauses, d) {
915 assert!(
916 ns_refutes_gfp(NsField::Prime(2), *nv, clauses, d),
917 "{name} n={nv} d={d}: a ℤ/4 refutation projects to GF(2)"
918 );
919 }
920 }
921 }
922 for (name, nv, clauses) in
924 [("parity", 3usize, parity_triangle()), ("cnt23", 3, crate::families::mod_counting(3, 2).0.clauses)]
925 {
926 assert!(ns_refutes_gfp(NsField::Prime(2), nv, &clauses, 2), "{name}: GF(2) refutes at 2");
927 assert!(!ns_refutes_zm(4, nv, &clauses, 2), "{name}: ℤ/4 cannot refute at 2 — the nilpotent tax");
928 assert!(ns_refutes_zm(4, nv, &clauses, 3), "{name}: ℤ/4 refutes at 3 (within the Hensel 2d bound)");
929 }
930 }
931
932 #[test]
937 fn the_partition_of_unity_atom_is_one_over_zero_divisor_moduli() {
938 for &m in &[4u64, 6, 9, 12] {
939 let one: ZmPoly = [(0u64, 1u64)].into_iter().collect();
940 for v in 0..8 {
941 assert_eq!(pou_atom_zm(m, v), one, "m={m}: the atom (1−x{v})+x{v} reduces to 1");
942 }
943 for n in 0..=8 {
944 assert_eq!(partition_of_unity_zm(m, n), one, "m={m}: Σ_a δ_a = 1 over the {n}-cube");
945 }
946 }
947 }
948
949 #[test]
954 fn z6_clause_polynomial_is_the_signed_false_indicator_on_every_corner() {
955 let pos = clause_polynomial_zm(6, &[Lit::pos(0)]);
956 let expected: ZmPoly = [(0u64, 1u64), (1u64, 5u64)].into_iter().collect();
957 assert_eq!(pos, expected, "positive literal → 1 − x = 1 + 5x over ℤ/6");
958 let mut seed = 0x0006_C0DEu64;
959 for &m in &[4u64, 6, 9, 12] {
960 for _ in 0..30 {
961 let n = 2 + (lcg(&mut seed) % 4) as usize; let width = 1 + (lcg(&mut seed) % n as u64) as usize;
963 let mut vars: Vec<u32> = Vec::new();
964 while vars.len() < width {
965 let v = (lcg(&mut seed) % n as u64) as u32;
966 if !vars.contains(&v) {
967 vars.push(v);
968 }
969 }
970 let clause: Vec<Lit> =
971 vars.iter().map(|&v| Lit::new(v, lcg(&mut seed) & 1 == 1)).collect();
972 let poly = clause_polynomial_zm(m, &clause);
973 for a in 0u64..(1u64 << n) {
974 let want = if falsifies(&clause, a) { 1 } else { 0 };
975 assert_eq!(eval_at(m, &poly, a), want, "m={m} clause={clause:?} corner={a:b}");
976 }
977 }
978 }
979 }
980
981 #[test]
989 fn zm_span_membership_matches_the_exhaustive_oracle() {
990 let mut seed = 0x40E1_1000u64;
991 for &m in &[4u64, 6, 9, 12] {
992 for _ in 0..25 {
993 let n = 2 + (lcg(&mut seed) % 3) as usize; let nrows = 1 + (lcg(&mut seed) % 3) as usize; let rows: Vec<Vec<u64>> = (0..nrows)
996 .map(|_| (0..n).map(|_| lcg(&mut seed) % m).collect())
997 .collect();
998 let mut span: std::collections::BTreeSet<Vec<u64>> = std::collections::BTreeSet::new();
1000 let mut combo = vec![0u64; nrows];
1001 loop {
1002 let v: Vec<u64> = (0..n)
1003 .map(|j| {
1004 rows.iter()
1005 .zip(&combo)
1006 .fold(0u64, |acc, (r, &c)| zm_add(m, acc, zm_mul(m, c, r[j])))
1007 })
1008 .collect();
1009 span.insert(v);
1010 let mut i = 0;
1011 while i < nrows {
1012 combo[i] += 1;
1013 if combo[i] < m {
1014 break;
1015 }
1016 combo[i] = 0;
1017 i += 1;
1018 }
1019 if i == nrows {
1020 break;
1021 }
1022 }
1023 let mut ech = ZmEchelon::new(m);
1025 for r in &rows {
1026 ech.insert(r.clone());
1027 }
1028 let mut target = vec![0u64; n];
1029 loop {
1030 assert_eq!(
1031 ech.contains(target.clone()),
1032 span.contains(&target),
1033 "m={m} rows={rows:?} target={target:?}"
1034 );
1035 let mut i = 0;
1036 while i < n {
1037 target[i] += 1;
1038 if target[i] < m {
1039 break;
1040 }
1041 target[i] = 0;
1042 i += 1;
1043 }
1044 if i == n {
1045 break;
1046 }
1047 }
1048 }
1049 }
1050 }
1051
1052 #[test]
1056 fn the_ring_engine_at_prime_m_agrees_with_the_field_engine() {
1057 let mut seed = 0x9219_0E11u64;
1058 for _ in 0..25 {
1059 let nv = 3 + (lcg(&mut seed) % 3) as usize; let nc = 4 + (lcg(&mut seed) % 8) as usize;
1061 let clauses: Vec<Vec<Lit>> = (0..nc)
1062 .map(|_| {
1063 let width = 1 + (lcg(&mut seed) % 3) as usize;
1064 let mut vars: Vec<u32> = Vec::new();
1065 while vars.len() < width {
1066 let v = (lcg(&mut seed) % nv as u64) as u32;
1067 if !vars.contains(&v) {
1068 vars.push(v);
1069 }
1070 }
1071 vars.iter().map(|&v| Lit::new(v, lcg(&mut seed) & 1 == 1)).collect()
1072 })
1073 .collect();
1074 for &p in &[2u64, 3, 5] {
1075 for d in 1..=nv.min(4) {
1076 assert_eq!(
1077 ns_refutes_zm(p, nv, &clauses, d),
1078 crate::polycalc_gfp::ns_refutes_gfp(
1079 crate::polycalc_gfp::NsField::Prime(p),
1080 nv,
1081 &clauses,
1082 d
1083 ),
1084 "p={p} n={nv} d={d}: ℤ/p ring engine = GF(p) field engine"
1085 );
1086 }
1087 }
1088 }
1089 }
1090
1091 #[test]
1097 fn build_ns_certificate_zm_is_total_sound_and_fail_closed_over_z6_and_z4() {
1098 let mut seed = 0x0BAD_0604u64;
1099 for &m in &[6u64, 4] {
1100 let mut unsat_seen = 0usize;
1101 for _ in 0..60 {
1102 let nv = 4 + (lcg(&mut seed) % 3) as usize; let nc = 2 * nv + (lcg(&mut seed) % (3 * nv as u64)) as usize; let clauses: Vec<Vec<Lit>> = (0..nc)
1105 .map(|_| {
1106 let width = 2 + (lcg(&mut seed) % 2) as usize;
1107 let mut vars: Vec<u32> = Vec::new();
1108 while vars.len() < width {
1109 let v = (lcg(&mut seed) % nv as u64) as u32;
1110 if !vars.contains(&v) {
1111 vars.push(v);
1112 }
1113 }
1114 vars.iter().map(|&v| Lit::new(v, lcg(&mut seed) & 1 == 1)).collect()
1115 })
1116 .collect();
1117 match build_ns_certificate_zm(m, nv, &clauses) {
1118 Ok(cert) => {
1119 unsat_seen += 1;
1120 assert_eq!(cert.modulus(), m);
1121 assert!(cert.verify(&clauses), "m={m} n={nv}: the ring certificate re-checks");
1122 assert!(cert.degree() <= nv, "m={m} n={nv}: certificate degree ≤ n");
1123 assert!(!sat(nv, &clauses), "m={m}: certificates only for genuine UNSAT");
1124 assert!(
1125 !cert.verify(&clauses[..clauses.len() - 1]),
1126 "m={m}: a certificate must not verify a different clause set"
1127 );
1128 }
1129 Err(model) => {
1130 assert!(sat(nv, &clauses), "m={m}: SAT verdicts only for satisfiable formulas");
1131 assert!(
1132 clauses
1133 .iter()
1134 .all(|c| c.iter().any(|l| model[l.var() as usize] == l.is_positive())),
1135 "m={m}: the returned model satisfies every clause"
1136 );
1137 }
1138 }
1139 }
1140 assert!(unsat_seen >= 5, "m={m}: the corpus exercises the UNSAT branch ({unsat_seen})");
1141 }
1142 }
1143
1144 #[test]
1156 fn no_finite_formula_is_structureless_over_any_modulus() {
1157 let mut corpus: Vec<(usize, Vec<Vec<Lit>>)> = Vec::new();
1158 let (php3, _) = crate::families::php(3);
1159 corpus.push((php3.num_vars, php3.clauses));
1160 let (cnt34, _) = crate::families::mod_counting(4, 3);
1161 corpus.push((cnt34.num_vars, cnt34.clauses));
1162 let p = |v: u32| Lit::pos(v);
1164 let q = |v: u32| Lit::neg(v);
1165 corpus.push((3, vec![
1166 vec![p(0), p(1)], vec![q(0), q(1)],
1167 vec![p(1), p(2)], vec![q(1), q(2)],
1168 vec![p(2), p(0)], vec![q(2), q(0)],
1169 ]));
1170 let mut seed = 0xA11_0DD5u64;
1171 for _ in 0..10 {
1172 let nv = 3 + (lcg(&mut seed) % 3) as usize;
1173 let nc = nv + (lcg(&mut seed) % (2 * nv as u64)) as usize;
1174 let clauses: Vec<Vec<Lit>> = (0..nc)
1175 .map(|_| {
1176 let width = 2 + (lcg(&mut seed) % 2) as usize;
1177 let mut vars: Vec<u32> = Vec::new();
1178 while vars.len() < width {
1179 let v = (lcg(&mut seed) % nv as u64) as u32;
1180 if !vars.contains(&v) {
1181 vars.push(v);
1182 }
1183 }
1184 vars.iter().map(|&v| Lit::new(v, lcg(&mut seed) & 1 == 1)).collect()
1185 })
1186 .collect();
1187 corpus.push((nv, clauses));
1188 }
1189 let mut certified = 0usize;
1190 for m in 2u64..=12 {
1191 for (nv, clauses) in &corpus {
1192 match build_ns_certificate_zm(m, *nv, clauses) {
1193 Ok(cert) => {
1194 assert!(cert.verify(clauses), "m={m} n={nv}: the certificate re-checks");
1195 assert!(cert.degree() <= *nv, "m={m}: degree ≤ n — structure within the cube");
1196 certified += 1;
1197 }
1198 Err(model) => {
1199 assert!(
1200 clauses
1201 .iter()
1202 .all(|c| c.iter().any(|l| model[l.var() as usize] == l.is_positive())),
1203 "m={m}: SAT half — the model satisfies every clause"
1204 );
1205 }
1206 }
1207 }
1208 }
1209 assert!(certified >= 3 * 11, "the UNSAT half is exercised across all 11 moduli ({certified})");
1210 eprintln!(
1211 "structureless-witness count over m = 2..12: 0 of {} (UNSAT instances certified: {certified})",
1212 corpus.len() * 11
1213 );
1214 }
1215}