1#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct ModpEquation {
19 pub coeffs: Vec<(usize, u64)>,
20 pub rhs: u64,
21}
22
23impl ModpEquation {
24 pub fn new(coeffs: impl Into<Vec<(usize, u64)>>, rhs: u64) -> Self {
25 ModpEquation { coeffs: coeffs.into(), rhs }
26 }
27}
28
29#[derive(Clone, Debug, PartialEq, Eq)]
31pub enum ModpOutcome {
32 Sat(Vec<u64>),
35 Unsat(Vec<(usize, u64)>),
38}
39
40#[inline]
41fn add(a: u64, b: u64, p: u64) -> u64 {
42 (a + b) % p
43}
44#[inline]
45fn sub(a: u64, b: u64, p: u64) -> u64 {
46 (a + p - b % p) % p
47}
48#[inline]
49fn mul(a: u64, b: u64, p: u64) -> u64 {
50 (a % p) * (b % p) % p
51}
52#[inline]
53fn powm(mut a: u64, mut e: u64, p: u64) -> u64 {
54 let mut r = 1u64 % p;
55 a %= p;
56 while e > 0 {
57 if e & 1 == 1 {
58 r = mul(r, a, p);
59 }
60 a = mul(a, a, p);
61 e >>= 1;
62 }
63 r
64}
65#[inline]
67fn inv(a: u64, p: u64) -> u64 {
68 powm(a, p - 2, p)
69}
70
71pub fn solve(equations: &[ModpEquation], num_vars: usize, p: u64) -> ModpOutcome {
74 let m = equations.len();
75 let mut coeff: Vec<Vec<u64>> = Vec::with_capacity(m);
78 let mut rhs: Vec<u64> = Vec::with_capacity(m);
79 let mut prov: Vec<Vec<u64>> = Vec::with_capacity(m);
80 for (i, eq) in equations.iter().enumerate() {
81 let mut c = vec![0u64; num_vars];
82 for &(v, a) in &eq.coeffs {
83 if v < num_vars {
84 c[v] = add(c[v], a, p);
85 }
86 }
87 coeff.push(c);
88 rhs.push(eq.rhs % p);
89 let mut pr = vec![0u64; m];
90 pr[i] = 1 % p;
91 prov.push(pr);
92 }
93
94 let mut pivot_col_of_row: Vec<usize> = Vec::new();
95 let mut row = 0usize;
96 for col in 0..num_vars {
97 let Some(sel) = (row..coeff.len()).find(|&r| coeff[r][col] != 0) else {
98 continue;
99 };
100 coeff.swap(row, sel);
101 rhs.swap(row, sel);
102 prov.swap(row, sel);
103 let factor = inv(coeff[row][col], p);
105 for v in 0..num_vars {
106 coeff[row][v] = mul(coeff[row][v], factor, p);
107 }
108 rhs[row] = mul(rhs[row], factor, p);
109 for k in 0..m {
110 prov[row][k] = mul(prov[row][k], factor, p);
111 }
112 for r in 0..coeff.len() {
114 if r != row && coeff[r][col] != 0 {
115 let f = coeff[r][col];
116 for v in 0..num_vars {
117 coeff[r][v] = sub(coeff[r][v], mul(f, coeff[row][v], p), p);
118 }
119 rhs[r] = sub(rhs[r], mul(f, rhs[row], p), p);
120 for k in 0..m {
121 prov[r][k] = sub(prov[r][k], mul(f, prov[row][k], p), p);
122 }
123 }
124 }
125 pivot_col_of_row.push(col);
126 row += 1;
127 if row == coeff.len() {
128 break;
129 }
130 }
131
132 for r in 0..coeff.len() {
134 if coeff[r].iter().all(|&x| x == 0) && rhs[r] != 0 {
135 let combo: Vec<(usize, u64)> =
136 prov[r].iter().enumerate().filter(|&(_, &m)| m != 0).map(|(i, &m)| (i, m)).collect();
137 return ModpOutcome::Unsat(combo);
138 }
139 }
140
141 let mut assignment = vec![0u64; num_vars];
143 for (r, &col) in pivot_col_of_row.iter().enumerate() {
144 assignment[col] = rhs[r];
145 }
146 ModpOutcome::Sat(assignment)
147}
148
149#[derive(Clone, Debug, PartialEq, Eq)]
156pub struct SolutionSpaceP {
157 pub num_vars: usize,
158 pub p: u64,
159 pub particular: Vec<u64>,
160 pub kernel_basis: Vec<Vec<u64>>,
161}
162
163impl SolutionSpaceP {
164 pub fn count(&self) -> u128 {
166 (self.p as u128).pow(self.kernel_basis.len() as u32)
167 }
168
169 pub fn enumerate(&self) -> Vec<Vec<u64>> {
171 let k = self.kernel_basis.len();
172 let total = (self.p as u128).pow(k as u32);
173 (0..total as u64)
174 .map(|mut code| {
175 let mut x = self.particular.clone();
176 for b in 0..k {
177 let coef = code % self.p;
178 code /= self.p;
179 if coef != 0 {
180 for v in 0..self.num_vars {
181 x[v] = add(x[v], mul(coef, self.kernel_basis[b][v], self.p), self.p);
182 }
183 }
184 }
185 x
186 })
187 .collect()
188 }
189}
190
191pub fn solve_space(equations: &[ModpEquation], num_vars: usize, p: u64) -> Option<SolutionSpaceP> {
196 let mut coeff: Vec<Vec<u64>> = Vec::with_capacity(equations.len());
197 let mut rhs: Vec<u64> = Vec::with_capacity(equations.len());
198 for eq in equations {
199 let mut c = vec![0u64; num_vars];
200 for &(v, a) in &eq.coeffs {
201 if v < num_vars {
202 c[v] = add(c[v], a, p);
203 }
204 }
205 coeff.push(c);
206 rhs.push(eq.rhs % p);
207 }
208
209 let mut pivot_col_of_row: Vec<usize> = Vec::new();
210 let mut row = 0usize;
211 for col in 0..num_vars {
212 let Some(sel) = (row..coeff.len()).find(|&r| coeff[r][col] != 0) else {
213 continue;
214 };
215 coeff.swap(row, sel);
216 rhs.swap(row, sel);
217 let factor = inv(coeff[row][col], p);
218 for v in 0..num_vars {
219 coeff[row][v] = mul(coeff[row][v], factor, p);
220 }
221 rhs[row] = mul(rhs[row], factor, p);
222 for r in 0..coeff.len() {
224 if r != row && coeff[r][col] != 0 {
225 let f = coeff[r][col];
226 for v in 0..num_vars {
227 coeff[r][v] = sub(coeff[r][v], mul(f, coeff[row][v], p), p);
228 }
229 rhs[r] = sub(rhs[r], mul(f, rhs[row], p), p);
230 }
231 }
232 pivot_col_of_row.push(col);
233 row += 1;
234 }
235
236 for r in 0..coeff.len() {
238 if coeff[r].iter().all(|&x| x == 0) && rhs[r] != 0 {
239 return None;
240 }
241 }
242
243 let mut is_pivot = vec![false; num_vars];
244 for &c in &pivot_col_of_row {
245 is_pivot[c] = true;
246 }
247 let mut particular = vec![0u64; num_vars];
249 for (r, &pc) in pivot_col_of_row.iter().enumerate() {
250 particular[pc] = rhs[r];
251 }
252 let mut kernel_basis = Vec::new();
254 for f in 0..num_vars {
255 if is_pivot[f] {
256 continue;
257 }
258 let mut kv = vec![0u64; num_vars];
259 kv[f] = 1;
260 for (r, &pc) in pivot_col_of_row.iter().enumerate() {
261 kv[pc] = sub(0, coeff[r][f], p);
262 }
263 kernel_basis.push(kv);
264 }
265 Some(SolutionSpaceP { num_vars, p, particular, kernel_basis })
266}
267
268pub fn cycle_system(n: usize, p: u64) -> Vec<ModpEquation> {
274 (0..n)
275 .map(|i| ModpEquation::new(vec![(i, 1), ((i + 1) % n, p - 1)], 1))
276 .collect()
277}
278
279pub fn satisfies(equations: &[ModpEquation], assignment: &[u64], p: u64) -> bool {
281 equations.iter().all(|eq| {
282 let lhs = eq
283 .coeffs
284 .iter()
285 .fold(0u64, |acc, &(v, a)| add(acc, mul(a, *assignment.get(v).unwrap_or(&0), p), p));
286 lhs == eq.rhs % p
287 })
288}
289
290pub fn is_refutation(
293 equations: &[ModpEquation],
294 num_vars: usize,
295 p: u64,
296 combo: &[(usize, u64)],
297) -> bool {
298 if combo.is_empty() {
299 return false;
300 }
301 let mut lhs = vec![0u64; num_vars];
302 let mut rhs = 0u64;
303 for &(idx, mult) in combo {
304 let Some(eq) = equations.get(idx) else {
305 return false;
306 };
307 for &(v, a) in &eq.coeffs {
308 if v < num_vars {
309 lhs[v] = add(lhs[v], mul(mult, a, p), p);
310 }
311 }
312 rhs = add(rhs, mul(mult, eq.rhs, p), p);
313 }
314 lhs.iter().all(|&x| x == 0) && rhs != 0
315}
316
317#[derive(Clone, Debug)]
323pub struct ModpRecovery {
324 pub modulus: u64,
325 pub num_vars: usize,
327 pub equations: Vec<ModpEquation>,
328 pub groups: Vec<Vec<u32>>,
330}
331
332pub fn is_prime(p: u64) -> bool {
333 if p < 2 {
334 return false;
335 }
336 let mut d = 2u64;
337 while d * d <= p {
338 if p % d == 0 {
339 return false;
340 }
341 d += 1;
342 }
343 true
344}
345
346fn nullspace(rows: &[Vec<u64>], k: usize, p: u64) -> Vec<Vec<u64>> {
350 let mut m: Vec<Vec<u64>> = rows.iter().map(|r| r.iter().map(|&x| x % p).collect()).collect();
351 let mut where_pivot: Vec<isize> = vec![-1; k];
352 let mut row = 0usize;
353 for col in 0..k {
354 let Some(sel) = (row..m.len()).find(|&r| m[r][col] != 0) else {
355 continue;
356 };
357 m.swap(row, sel);
358 let finv = inv(m[row][col], p);
359 for c in 0..k {
360 m[row][c] = mul(m[row][c], finv, p);
361 }
362 for r in 0..m.len() {
363 if r != row && m[r][col] != 0 {
364 let f = m[r][col];
365 for c in 0..k {
366 m[r][c] = sub(m[r][c], mul(f, m[row][c], p), p);
367 }
368 }
369 }
370 where_pivot[col] = row as isize;
371 row += 1;
372 if row == m.len() {
373 break;
374 }
375 }
376 let mut basis = Vec::new();
377 for free in 0..k {
378 if where_pivot[free] != -1 {
379 continue;
380 }
381 let mut v = vec![0u64; k];
382 v[free] = 1;
383 for (col, &pr) in where_pivot.iter().enumerate() {
384 if pr != -1 {
385 v[col] = sub(0, m[pr as usize][free], p);
386 }
387 }
388 basis.push(v);
389 }
390 basis
391}
392
393fn fit_congruence(
401 k: usize,
402 allowed: &[Vec<u64>],
403 forbidden: &[Vec<u64>],
404 m: u64,
405) -> Option<(Vec<u64>, u64)> {
406 let t0 = allowed.first()?;
407 let mm = m as u128;
408 let eval = |a: &[u64], t: &[u64]| -> u64 {
409 (0..k).fold(0u128, |acc, i| (acc + a[i] as u128 * t[i] as u128) % mm) as u64
410 };
411 let candidate: Option<Vec<u64>> = if is_prime(m) {
412 if (allowed.len() as u128) != mm.pow((k - 1) as u32) {
415 return None;
416 }
417 let diffs: Vec<Vec<u64>> =
418 allowed.iter().skip(1).map(|t| (0..k).map(|i| sub(t[i], t0[i], m)).collect()).collect();
419 let basis = nullspace(&diffs, k, m);
420 (basis.len() == 1 && basis[0].iter().any(|&x| x != 0)).then(|| basis[0].clone())
421 } else {
422 let total = mm.checked_pow(k as u32)?;
424 if total.checked_mul(total)? > (1u128 << 24) {
425 return None; }
427 let mut found = None;
428 for code in 0..total {
429 let mut a = vec![0u64; k];
430 let mut x = code;
431 for slot in a.iter_mut() {
432 *slot = (x % mm) as u64;
433 x /= mm;
434 }
435 if a.iter().all(|&v| v == 0) {
436 continue;
437 }
438 let c = eval(&a, t0);
439 if allowed.iter().all(|t| eval(&a, t) == c) && forbidden.iter().all(|f| eval(&a, f) != c) {
440 found = Some(a);
441 break;
442 }
443 }
444 found
445 };
446 let a = candidate?;
447 let c = eval(&a, t0);
448 if allowed.iter().any(|t| eval(&a, t) != c) || forbidden.iter().any(|f| eval(&a, f) == c) {
449 return None;
450 }
451 Some((a, c))
452}
453
454pub fn recover_from_cnf(num_bool_vars: usize, clauses: &[Vec<crate::cdcl::Lit>]) -> Option<ModpRecovery> {
465 use std::collections::{BTreeMap, HashMap, HashSet};
466 if clauses.is_empty() {
467 return None;
468 }
469
470 let mut group_candidates: Vec<Vec<u32>> = Vec::new();
472 let mut neg_pairs: HashSet<(u32, u32)> = HashSet::new();
473 let mut appears: HashSet<u32> = HashSet::new();
474 for c in clauses {
475 for l in c {
476 appears.insert(l.var());
477 }
478 if c.len() >= 2 && c.iter().all(|l| l.is_positive()) {
479 let mut g: Vec<u32> = c.iter().map(|l| l.var()).collect();
480 g.sort_unstable();
481 g.dedup();
482 if g.len() != c.len() {
483 return None;
484 }
485 group_candidates.push(g);
486 } else if c.len() == 2 && c.iter().all(|l| !l.is_positive()) {
487 let (a, b) = (c[0].var(), c[1].var());
488 neg_pairs.insert((a.min(b), a.max(b)));
489 }
490 }
491 if group_candidates.is_empty() {
492 return None;
493 }
494 let m = group_candidates[0].len() as u64;
495 if m < 2 {
496 return None;
497 }
498
499 let mut var_to_group: HashMap<u32, usize> = HashMap::new();
501 let mut groups: Vec<Vec<u32>> = Vec::new();
502 for g in &group_candidates {
503 if g.len() as u64 != m {
504 return None;
505 }
506 for i in 0..g.len() {
507 for j in (i + 1)..g.len() {
508 if !neg_pairs.contains(&(g[i], g[j])) {
509 return None;
510 }
511 }
512 }
513 let gid = groups.len();
514 for &v in g {
515 if var_to_group.insert(v, gid).is_some() {
516 return None; }
518 }
519 groups.push(g.clone());
520 }
521 if appears.iter().any(|v| !var_to_group.contains_key(v)) {
523 return None;
524 }
525 let _ = num_bool_vars;
526 let pos_in_group = |v: u32, gid: usize| groups[gid].iter().position(|&x| x == v).unwrap() as u64;
527
528 let mut scopes: BTreeMap<Vec<usize>, Vec<Vec<u64>>> = BTreeMap::new();
530 for c in clauses {
531 if c.len() >= 2 && c.iter().all(|l| l.is_positive()) {
532 continue; }
534 if c.len() == 2 && c.iter().all(|l| !l.is_positive()) {
535 let g0 = *var_to_group.get(&c[0].var())?;
536 let g1 = *var_to_group.get(&c[1].var())?;
537 if g0 == g1 {
538 continue; }
540 }
541 if !c.iter().all(|l| !l.is_positive()) {
542 return None; }
544 let mut pairs: Vec<(usize, u64)> = Vec::new();
545 let mut seen = HashSet::new();
546 for l in c {
547 let g = *var_to_group.get(&l.var())?;
548 if !seen.insert(g) {
549 return None; }
551 pairs.push((g, pos_in_group(l.var(), g)));
552 }
553 pairs.sort_by_key(|&(g, _)| g);
554 let scope: Vec<usize> = pairs.iter().map(|&(g, _)| g).collect();
555 let tuple: Vec<u64> = pairs.iter().map(|&(_, v)| v).collect();
556 scopes.entry(scope).or_default().push(tuple);
557 }
558 if scopes.is_empty() {
559 return None;
560 }
561
562 let mut equations: Vec<ModpEquation> = Vec::new();
564 for (scope, forbidden) in &scopes {
565 let k = scope.len();
566 let total = (m as u128).checked_pow(k as u32)?;
567 if total > (1u128 << 22) {
568 return None; }
570 let forbidden_set: HashSet<Vec<u64>> = forbidden.iter().cloned().collect();
571 let mut allowed: Vec<Vec<u64>> = Vec::new();
572 for idx in 0..total {
573 let mut t = vec![0u64; k];
574 let mut x = idx;
575 for slot in t.iter_mut() {
576 *slot = (x % m as u128) as u64;
577 x /= m as u128;
578 }
579 if !forbidden_set.contains(&t) {
580 allowed.push(t);
581 }
582 }
583 let (a, c) = fit_congruence(k, &allowed, forbidden, m)?;
584 let coeffs: Vec<(usize, u64)> =
585 scope.iter().enumerate().map(|(i, &g)| (g, a[i])).filter(|&(_, ai)| ai != 0).collect();
586 if coeffs.is_empty() {
587 return None;
588 }
589 equations.push(ModpEquation::new(coeffs, c));
590 }
591
592 Some(ModpRecovery { modulus: m, num_vars: groups.len(), equations, groups })
593}
594
595pub fn gl_order_p(n: u32, p: u64) -> u128 {
600 let pn = (p as u128).pow(n);
601 (0..n).map(|i| pn - (p as u128).pow(i)).product()
602}
603
604pub fn is_invertible_modp(n: usize, p: u64, matrix: &[Vec<u64>]) -> bool {
607 let mut a: Vec<Vec<u64>> = matrix.iter().map(|r| r.iter().map(|&x| x % p).collect()).collect();
608 let mut rank = 0usize;
609 for col in 0..n {
610 if let Some(piv) = (rank..n).find(|&r| a[r][col] != 0) {
611 a.swap(rank, piv);
612 let pinv = inv(a[rank][col], p);
613 for c in 0..n {
614 a[rank][c] = mul(a[rank][c], pinv, p);
615 }
616 for r in 0..n {
617 if r != rank && a[r][col] != 0 {
618 let f = a[r][col];
619 for c in 0..n {
620 a[r][c] = sub(a[r][c], mul(f, a[rank][c], p), p);
621 }
622 }
623 }
624 rank += 1;
625 }
626 }
627 rank == n
628}
629
630pub fn invertibility_density_p(n: u32, p: u64) -> f64 {
634 (1..=n).map(|j| 1.0 - (p as f64).powi(-(j as i32))).product()
635}
636
637#[cfg(test)]
638mod tests {
639 use super::*;
640
641 fn splitmix(state: &mut u64) -> u64 {
643 *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
644 let mut z = *state;
645 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
646 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
647 z ^ (z >> 31)
648 }
649
650 fn count_invertible_modp_bruteforce(n: usize, p: u64) -> u128 {
651 let cells = (n * n) as u32;
652 let total = (p as u128).pow(cells);
653 let mut count = 0u128;
654 for idx in 0..total {
655 let mut m = vec![vec![0u64; n]; n];
656 let mut x = idx;
657 for row in m.iter_mut() {
658 for cell in row.iter_mut() {
659 *cell = (x % p as u128) as u64;
660 x /= p as u128;
661 }
662 }
663 if is_invertible_modp(n, p, &m) {
664 count += 1;
665 }
666 }
667 count
668 }
669
670 #[test]
675 fn gl_order_p_is_the_invertible_count_over_gf_p() {
676 for &(n, p) in &[(1usize, 2u64), (1, 3), (2, 2), (2, 3), (2, 5), (2, 7), (3, 2)] {
677 assert_eq!(
678 count_invertible_modp_bruteforce(n, p),
679 gl_order_p(n as u32, p),
680 "brute invertible count over GF({p}) must equal |GL({n},{p})| = Π(pⁿ−pⁱ)"
681 );
682 }
683 assert_eq!(gl_order_p(2, 2), 6, "|GL(2,2)| = 6 ≅ S₃");
684 assert_eq!(gl_order_p(2, 3), 48, "|GL(2,3)| = 48");
685 assert_eq!(gl_order_p(2, 5), 480, "|GL(2,5)| = 480");
686 assert_eq!(gl_order_p(3, 2), 168, "|GL(3,2)| = 168");
687 }
688
689 #[test]
694 fn the_field_size_is_a_symmetry_axis() {
695 for &p in &[2u64, 3, 5, 7] {
697 for n in 1..=4u32 {
698 let exact = gl_order_p(n, p) as f64 / (p as f64).powi((n * n) as i32);
699 assert!((invertibility_density_p(n, p) - exact).abs() < 1e-12, "density == |GL|/p^(n²) at n={n},p={p}");
700 }
701 }
702 let n = 6u32;
704 let dens: Vec<f64> = [2u64, 3, 5, 7, 11].iter().map(|&p| invertibility_density_p(n, p)).collect();
705 for w in dens.windows(2) {
706 assert!(w[1] > w[0], "density increases with the field size: {dens:?}");
707 }
708 assert!((invertibility_density_p(40, 2) - 0.288_788_095_1).abs() < 1e-9, "GF(2) density → φ(½)");
710 for n in 1..=10u32 {
712 assert!((invertibility_density_p(n, 2) - crate::gf2::invertibility_density(n)).abs() < 1e-12, "modp p=2 == gf2 at n={n}");
713 assert_eq!(gl_order_p(n, 2), crate::gf2::gl_order(n), "|GL(n,2)| agrees across modules at n={n}");
714 }
715 }
716
717 fn brute_force_sat(equations: &[ModpEquation], num_vars: usize, p: u64) -> bool {
718 let total = (p as u128).pow(num_vars as u32);
719 for code in 0..total {
720 let mut a = vec![0u64; num_vars];
721 let mut c = code;
722 for slot in a.iter_mut() {
723 *slot = (c % p as u128) as u64;
724 c /= p as u128;
725 }
726 if satisfies(equations, &a, p) {
727 return true;
728 }
729 }
730 false
731 }
732
733 #[test]
737 fn modp_gaussian_matches_brute_force() {
738 for &p in &[2u64, 3, 5, 7] {
739 let mut state = 0x1234_5678u64 ^ p;
740 for _ in 0..40 {
741 let num_vars = 2 + (splitmix(&mut state) % 3) as usize; let num_eqs = 1 + (splitmix(&mut state) % 5) as usize; let equations: Vec<ModpEquation> = (0..num_eqs)
744 .map(|_| {
745 let coeffs: Vec<(usize, u64)> = (0..num_vars)
746 .map(|v| (v, splitmix(&mut state) % p))
747 .filter(|&(_, a)| a != 0)
748 .collect();
749 ModpEquation::new(coeffs, splitmix(&mut state) % p)
750 })
751 .collect();
752 let brute = brute_force_sat(&equations, num_vars, p);
753 match solve(&equations, num_vars, p) {
754 ModpOutcome::Sat(a) => {
755 assert!(brute, "p={p}: solver Sat but brute force UNSAT: {equations:?}");
756 assert!(satisfies(&equations, &a, p), "p={p}: the model must satisfy: {a:?}");
757 }
758 ModpOutcome::Unsat(combo) => {
759 assert!(!brute, "p={p}: solver Unsat but a model exists: {equations:?}");
760 assert!(
761 is_refutation(&equations, num_vars, p, &combo),
762 "p={p}: the refutation must re-check: {combo:?}"
763 );
764 }
765 }
766 }
767 }
768 }
769
770 #[test]
776 fn mod3_inconsistency_is_invisible_to_gf2() {
777 let p = 3;
778 let eqs = vec![
779 ModpEquation::new(vec![(0, 1), (1, 1), (2, 1)], 0),
780 ModpEquation::new(vec![(0, 1), (1, 1), (2, 1)], 2),
781 ];
782 match solve(&eqs, 3, p) {
783 ModpOutcome::Unsat(combo) => {
784 assert!(is_refutation(&eqs, 3, p, &combo), "the mod-3 refutation re-checks: {combo:?}");
785 }
786 other => panic!("expected the mod-3 system to be refuted, got {other:?}"),
787 }
788 let gf2_rhs: Vec<u64> = eqs.iter().map(|e| e.rhs % 2).collect();
791 assert_eq!(gf2_rhs, vec![0, 0], "the GF(2) reduction has no conflict — parity is blind here");
792 }
793
794 #[test]
799 fn the_mod_p_cycle_obstruction_crushed_at_scale() {
800 for &p in &[3u64, 5, 7] {
801 for n in 2..=40 {
802 let eqs = cycle_system(n, p);
803 match solve(&eqs, n, p) {
804 ModpOutcome::Unsat(combo) => {
805 assert_ne!(n as u64 % p, 0, "p={p} n={n}: refuted ⟹ n not a multiple of p");
806 assert!(is_refutation(&eqs, n, p, &combo), "p={p} n={n}: cycle refutation re-checks");
807 }
808 ModpOutcome::Sat(a) => {
809 assert_eq!(n as u64 % p, 0, "p={p} n={n}: satisfiable ⟹ n is a multiple of p");
810 assert!(satisfies(&eqs, &a, p), "p={p} n={n}: the model must satisfy");
811 }
812 }
813 }
814 }
815 assert!(matches!(solve(&cycle_system(4, 2), 4, 2), ModpOutcome::Sat(_)), "4-cycle SAT over GF(2)");
817 assert!(matches!(solve(&cycle_system(4, 3), 4, 3), ModpOutcome::Unsat(_)), "4-cycle UNSAT over GF(3)");
818 }
819
820 #[test]
824 fn modp_over_gf2_agrees_with_xorsat() {
825 use crate::xorsat::{self, XorEquation, XorOutcome};
826 let mut state = 0x00AB_CDEFu64;
827 for _ in 0..50 {
828 let num_vars = 2 + (splitmix(&mut state) % 4) as usize;
829 let num_eqs = 1 + (splitmix(&mut state) % 5) as usize;
830 let systems: Vec<(Vec<usize>, bool)> = (0..num_eqs)
831 .map(|_| {
832 let vars: Vec<usize> =
833 (0..num_vars).filter(|_| splitmix(&mut state) % 2 == 0).collect();
834 (vars, splitmix(&mut state) % 2 == 1)
835 })
836 .collect();
837 let xor_eqs: Vec<XorEquation> =
838 systems.iter().map(|(v, r)| XorEquation::new(v.clone(), *r)).collect();
839 let modp_eqs: Vec<ModpEquation> = systems
840 .iter()
841 .map(|(v, r)| {
842 ModpEquation::new(v.iter().map(|&x| (x, 1u64)).collect::<Vec<_>>(), *r as u64)
843 })
844 .collect();
845 let xor_unsat = matches!(xorsat::solve(&xor_eqs, num_vars), XorOutcome::Unsat(_));
846 let modp_unsat = matches!(solve(&modp_eqs, num_vars, 2), ModpOutcome::Unsat(_));
847 assert_eq!(xor_unsat, modp_unsat, "modp(p=2) must match xorsat on {systems:?}");
848 }
849 }
850
851 #[test]
853 fn modp_solves_a_consistent_system() {
854 let eqs = vec![
856 ModpEquation::new(vec![(0, 1), (1, 2)], 3),
857 ModpEquation::new(vec![(1, 3)], 4),
858 ];
859 match solve(&eqs, 2, 5) {
860 ModpOutcome::Sat(a) => {
861 assert!(satisfies(&eqs, &a, 5), "model must satisfy: {a:?}");
862 assert_eq!(a, vec![2, 3], "the unique solution over GF(5)");
863 }
864 other => panic!("expected Sat, got {other:?}"),
865 }
866 }
867
868 #[test]
875 fn recover_from_cnf_is_faithful_across_primes() {
876 use crate::families::{mod_p_consistent_onehot, mod_p_tseitin_expander, ExpectedVerdict};
877 for &p in &[3u64, 5, 7] {
878 for &n in &[4usize, 6, 8] {
879 for seed in 0..3u64 {
880 for (supplied, cnf, expect) in
881 [mod_p_tseitin_expander(n, p, seed), mod_p_consistent_onehot(n, p, seed)]
882 {
883 let rec = recover_from_cnf(cnf.num_vars, &cnf.clauses).unwrap_or_else(|| {
884 panic!("p={p} n={n} seed={seed}: must recover the GF(p) system")
885 });
886 assert_eq!(rec.modulus, p, "p={p} n={n} seed={seed}: recovered the right field");
887 let rec_unsat =
888 matches!(solve(&rec.equations, rec.num_vars, p), ModpOutcome::Unsat(_));
889 let sup_unsat =
890 matches!(solve(&supplied, rec.num_vars, p), ModpOutcome::Unsat(_));
891 assert_eq!(rec_unsat, sup_unsat, "p={p} n={n} seed={seed}: recovered vs supplied");
892 assert_eq!(
893 rec_unsat,
894 matches!(expect, ExpectedVerdict::Unsat),
895 "p={p} n={n} seed={seed}: verdict matches the family's expectation"
896 );
897 }
898 }
899 }
900 }
901 }
902
903 #[test]
907 fn recover_declines_on_inputs_that_are_not_a_one_hot_encoding() {
908 use crate::cdcl::Lit;
909 let rnd = crate::families::random_3sat(20, 80, 0xF00D);
910 assert!(recover_from_cnf(rnd.num_vars, &rnd.clauses).is_none(), "random 3-SAT has no GF(p) structure");
911 let incomplete = vec![
912 vec![Lit::pos(0), Lit::pos(1), Lit::pos(2)],
913 vec![Lit::neg(0), Lit::neg(1)], ];
915 assert!(
916 recover_from_cnf(3, &incomplete).is_none(),
917 "an incomplete at-most-one must not be mistaken for a one-hot group"
918 );
919 }
920}