1use crate::modp::{self, ModpEquation, ModpOutcome};
19
20fn gcd_i128(a: i128, b: i128) -> i128 {
21 let (mut a, mut b) = (a.abs(), b.abs());
22 while b != 0 {
23 let t = a % b;
24 a = b;
25 b = t;
26 }
27 a
28}
29
30fn egcd(a: i128, b: i128) -> (i128, i128, i128) {
32 if b == 0 {
33 (a, 1, 0)
34 } else {
35 let (g, x, y) = egcd(b, a % b);
36 (g, y, x - (a / b) * y)
37 }
38}
39
40fn modinv(a: i128, n: i128) -> i128 {
42 let (_, x, _) = egcd(a.rem_euclid(n), n);
43 x.rem_euclid(n)
44}
45
46pub fn squarefree_primes(m: u64) -> Option<Vec<u64>> {
50 if m < 2 {
51 return None;
52 }
53 let mut primes = Vec::new();
54 let mut x = m;
55 let mut d = 2u64;
56 while d * d <= x {
57 if x % d == 0 {
58 x /= d;
59 if x % d == 0 {
60 return None; }
62 primes.push(d);
63 }
64 d += 1;
65 }
66 if x > 1 {
67 primes.push(x);
68 }
69 Some(primes)
70}
71
72#[derive(Clone, Debug, PartialEq, Eq)]
74pub enum ModmOutcome {
75 Sat(Vec<u64>),
77 Unsat { modulus: u64, combo: Vec<(usize, u64)> },
83}
84
85pub fn is_refutation(
89 equations: &[ModpEquation],
90 num_vars: usize,
91 modulus: u64,
92 combo: &[(usize, u64)],
93) -> bool {
94 if combo.is_empty() {
95 return false;
96 }
97 let mm = modulus as u128;
98 let mut lhs = vec![0u128; num_vars];
99 let mut rhs = 0u128;
100 for &(idx, mult) in combo {
101 let Some(eq) = equations.get(idx) else {
102 return false;
103 };
104 for &(v, a) in &eq.coeffs {
105 if v < num_vars {
106 lhs[v] = (lhs[v] + mult as u128 * a as u128) % mm;
107 }
108 }
109 rhs = (rhs + mult as u128 * eq.rhs as u128) % mm;
110 }
111 lhs.iter().all(|&x| x == 0) && rhs != 0
112}
113
114pub fn solve_squarefree(equations: &[ModpEquation], num_vars: usize, m: u64) -> Option<ModmOutcome> {
119 let primes = squarefree_primes(m)?;
120 let mut per_prime: Vec<(u64, Vec<u64>)> = Vec::with_capacity(primes.len());
121 for &p in &primes {
122 match modp::solve(equations, num_vars, p) {
123 ModpOutcome::Sat(a) => per_prime.push((p, a)),
124 ModpOutcome::Unsat(combo) => return Some(ModmOutcome::Unsat { modulus: p, combo }),
125 }
126 }
127 let mut assignment = vec![0u64; num_vars];
129 for (i, slot) in assignment.iter_mut().enumerate() {
130 let residues: Vec<(u64, u64)> = per_prime.iter().map(|(p, a)| (a[i], *p)).collect();
131 *slot = crt(&residues);
132 }
133 Some(ModmOutcome::Sat(assignment))
134}
135
136fn crt(residues: &[(u64, u64)]) -> u64 {
140 let mut acc_r = 0i128;
141 let mut acc_m = 1i128;
142 for &(r, modu) in residues {
143 let modu = modu as i128;
144 let diff = (r as i128 - acc_r).rem_euclid(modu);
145 let t = (diff * modinv(acc_m.rem_euclid(modu), modu)).rem_euclid(modu);
146 acc_r += acc_m * t;
147 acc_m *= modu;
148 acc_r = acc_r.rem_euclid(acc_m);
149 }
150 acc_r as u64
151}
152
153pub enum ForcedM {
155 Inconsistent,
157 Forced(Vec<Option<u64>>),
159}
160
161pub fn forced_values_squarefree(equations: &[ModpEquation], num_vars: usize, m: u64) -> Option<ForcedM> {
167 let primes = squarefree_primes(m)?;
168 let mut per_prime: Vec<(u64, Vec<Option<u64>>)> = Vec::with_capacity(primes.len());
169 for &p in &primes {
170 let Some(ss) = crate::modp::solve_space(equations, num_vars, p) else {
171 return Some(ForcedM::Inconsistent);
172 };
173 let forced_p: Vec<Option<u64>> =
174 (0..num_vars).map(|g| ss.kernel_basis.iter().all(|k| k[g] == 0).then(|| ss.particular[g])).collect();
175 per_prime.push((p, forced_p));
176 }
177 let forced: Vec<Option<u64>> = (0..num_vars)
178 .map(|g| {
179 let residues: Option<Vec<(u64, u64)>> =
180 per_prime.iter().map(|(p, f)| f[g].map(|v| (v, *p))).collect();
181 residues.map(|res| crt(&res))
182 })
183 .collect();
184 Some(ForcedM::Forced(forced))
185}
186
187pub struct SolutionSpaceM {
193 pub num_vars: usize,
194 pub m: u64,
195 pub particular: Vec<u64>,
196 pub kernel_basis: Vec<Vec<u64>>,
197}
198
199pub enum PrimePowerSpace {
201 Inconsistent,
202 Space(SolutionSpaceM),
203}
204
205pub fn solve_space_prime_power(equations: &[ModpEquation], num_vars: usize, p: u64, k: u32) -> Option<PrimePowerSpace> {
211 if k == 1 {
212 return Some(match crate::modp::solve_space(equations, num_vars, p) {
213 None => PrimePowerSpace::Inconsistent,
214 Some(ss) => PrimePowerSpace::Space(SolutionSpaceM {
215 num_vars,
216 m: p,
217 particular: ss.particular,
218 kernel_basis: ss.kernel_basis,
219 }),
220 });
221 }
222 let q = (p as i128).pow(k);
223 let (m, n) = (equations.len(), num_vars);
224 let mut a = vec![vec![0i128; n]; m];
225 let mut b = vec![0i128; m];
226 let mut u = vec![vec![0i128; m]; m];
227 let mut v = vec![vec![0i128; n]; n];
228 for (i, ui) in u.iter_mut().enumerate() {
229 ui[i] = 1;
230 }
231 for (j, vj) in v.iter_mut().enumerate() {
232 vj[j] = 1;
233 }
234 for (i, eq) in equations.iter().enumerate() {
235 for &(var, coef) in &eq.coeffs {
236 if var < n {
237 a[i][var] = (a[i][var] + coef as i128).rem_euclid(q);
238 }
239 }
240 b[i] = (eq.rhs as i128).rem_euclid(q);
241 }
242 let exceeds = |a: &[Vec<i128>], u: &[Vec<i128>], v: &[Vec<i128>]| {
243 a.iter().chain(u).chain(v).any(|r| r.iter().any(|&x| x.abs() > GROWTH_CAP))
244 };
245 let mut rank = 0usize;
246 for t in 0..m.min(n) {
247 loop {
248 let mut best: Option<(usize, usize, i128)> = None;
249 for (i, row) in a.iter().enumerate().skip(t) {
250 for (j, &val) in row.iter().enumerate().skip(t) {
251 if val != 0 && best.is_none_or(|(_, _, bv)| val.abs() < bv) {
252 best = Some((i, j, val.abs()));
253 }
254 }
255 }
256 let Some((pi, pj, _)) = best else { break };
257 if pi != t {
258 a.swap(pi, t);
259 u.swap(pi, t);
260 }
261 if pj != t {
262 for row in a.iter_mut() {
263 row.swap(pj, t);
264 }
265 for row in v.iter_mut() {
266 row.swap(pj, t);
267 }
268 }
269 let piv = a[t][t];
270 for i in 0..m {
271 if i != t && a[i][t] != 0 {
272 let f = a[i][t].div_euclid(piv);
273 if f != 0 {
274 for j in 0..n {
275 a[i][j] -= f * a[t][j];
276 }
277 for j in 0..m {
278 u[i][j] -= f * u[t][j];
279 }
280 }
281 }
282 }
283 for j in 0..n {
284 if j != t && a[t][j] != 0 {
285 let f = a[t][j].div_euclid(piv);
286 if f != 0 {
287 for row in a.iter_mut() {
288 row[j] -= f * row[t];
289 }
290 for row in v.iter_mut() {
291 row[j] -= f * row[t];
292 }
293 }
294 }
295 }
296 if exceeds(&a, &u, &v) {
297 return None;
298 }
299 if (0..m).all(|i| i == t || a[i][t] == 0) && (0..n).all(|j| j == t || a[t][j] == 0) {
300 break;
301 }
302 }
303 if a[t][t] != 0 {
304 rank = t + 1;
305 } else {
306 break;
307 }
308 }
309 let ub: Vec<i128> = (0..m).map(|i| (0..m).fold(0i128, |acc, r| acc + u[i][r] * b[r]).rem_euclid(q)).collect();
310
311 let mut y = vec![0i128; n];
314 let mut freedom = vec![1i128; n];
315 for t in 0..rank {
316 let d = a[t][t].rem_euclid(q);
317 let g = gcd_i128(d, q);
318 if ub[t].rem_euclid(g) != 0 {
319 return Some(PrimePowerSpace::Inconsistent);
320 }
321 let qg = q / g;
322 y[t] = if qg == 1 { 0 } else { (ub[t] / g).rem_euclid(qg) * modinv(d / g, qg) % qg };
323 freedom[t] = qg;
324 }
325 for &ubt in ub.iter().skip(rank) {
326 if ubt != 0 {
327 return Some(PrimePowerSpace::Inconsistent); }
329 }
330 let particular: Vec<u64> =
331 (0..n).map(|i| (0..n).fold(0i128, |acc, j| acc + v[i][j] * y[j]).rem_euclid(q) as u64).collect();
332 let mut kernel_basis: Vec<Vec<u64>> = Vec::new();
334 for t in 0..n {
335 if freedom[t] >= q {
336 continue;
337 }
338 let gen: Vec<u64> = (0..n).map(|i| (v[i][t] * freedom[t]).rem_euclid(q) as u64).collect();
339 if gen.iter().any(|&x| x != 0) {
340 kernel_basis.push(gen);
341 }
342 }
343 Some(PrimePowerSpace::Space(SolutionSpaceM { num_vars: n, m: q as u64, particular, kernel_basis }))
344}
345
346pub fn forced_values_prime_power(equations: &[ModpEquation], num_vars: usize, m: u64) -> Option<ForcedM> {
353 let factors = prime_power_factorize(m)?;
354 let mut per_component: Vec<(u64, Vec<Option<u64>>)> = Vec::with_capacity(factors.len());
355 for (p, k) in factors {
356 match solve_space_prime_power(equations, num_vars, p, k)? {
357 PrimePowerSpace::Inconsistent => return Some(ForcedM::Inconsistent),
358 PrimePowerSpace::Space(ss) => {
359 let forced_c: Vec<Option<u64>> = (0..num_vars)
360 .map(|g| ss.kernel_basis.iter().all(|kk| kk[g] == 0).then(|| ss.particular[g]))
361 .collect();
362 per_component.push((p.pow(k), forced_c));
363 }
364 }
365 }
366 let forced: Vec<Option<u64>> = (0..num_vars)
367 .map(|g| {
368 let residues: Option<Vec<(u64, u64)>> =
369 per_component.iter().map(|(q, f)| f[g].map(|v| (v, *q))).collect();
370 residues.map(|res| crt(&res))
371 })
372 .collect();
373 Some(ForcedM::Forced(forced))
374}
375
376pub enum AllowedOutcome {
383 Inconsistent,
384 Allowed(Vec<(u64, u64)>),
385}
386
387pub fn allowed_residues(equations: &[ModpEquation], num_vars: usize, m: u64) -> Option<AllowedOutcome> {
389 let factors = prime_power_factorize(m)?;
390 let mut per_component: Vec<Vec<(u64, u64)>> = Vec::with_capacity(factors.len());
391 for (p, k) in factors {
392 let q = p.pow(k);
393 match solve_space_prime_power(equations, num_vars, p, k)? {
394 PrimePowerSpace::Inconsistent => return Some(AllowedOutcome::Inconsistent),
395 PrimePowerSpace::Space(ss) => {
396 let pv: Vec<(u64, u64)> = (0..num_vars)
397 .map(|g| {
398 let d = ss.kernel_basis.iter().fold(q, |acc, kk| gcd_i128(acc as i128, kk[g] as i128) as u64);
401 (ss.particular[g] % d, d)
402 })
403 .collect();
404 per_component.push(pv);
405 }
406 }
407 }
408 let residues: Vec<(u64, u64)> = (0..num_vars)
409 .map(|g| {
410 let pairs: Vec<(u64, u64)> = per_component.iter().map(|pv| pv[g]).filter(|&(_, d)| d > 1).collect();
413 if pairs.is_empty() {
414 (0, 1)
415 } else {
416 (crt(&pairs), pairs.iter().map(|&(_, d)| d).product())
417 }
418 })
419 .collect();
420 Some(AllowedOutcome::Allowed(residues))
421}
422
423pub fn satisfies(equations: &[ModpEquation], assignment: &[u64], m: u64) -> bool {
425 let mm = m as u128;
426 equations.iter().all(|eq| {
427 let lhs = eq.coeffs.iter().fold(0u128, |acc, &(v, a)| {
428 (acc + a as u128 * *assignment.get(v).unwrap_or(&0) as u128) % mm
429 });
430 lhs == (eq.rhs as u128 % mm)
431 })
432}
433
434pub fn cycle_system(n: usize, m: u64) -> Vec<ModpEquation> {
438 modp::cycle_system(n, m)
439}
440
441pub fn prime_power_factorize(m: u64) -> Option<Vec<(u64, u32)>> {
443 if m < 2 {
444 return None;
445 }
446 let mut out = Vec::new();
447 let mut x = m;
448 let mut d = 2u64;
449 while d * d <= x {
450 if x % d == 0 {
451 let mut k = 0u32;
452 while x % d == 0 {
453 x /= d;
454 k += 1;
455 }
456 out.push((d, k));
457 }
458 d += 1;
459 }
460 if x > 1 {
461 out.push((x, 1));
462 }
463 Some(out)
464}
465
466const GROWTH_CAP: i128 = 1i128 << 60;
469
470pub fn solve_prime_power(
478 equations: &[ModpEquation],
479 num_vars: usize,
480 p: u64,
481 k: u32,
482) -> Option<ModmOutcome> {
483 if k == 1 {
484 return Some(match modp::solve(equations, num_vars, p) {
485 ModpOutcome::Sat(a) => ModmOutcome::Sat(a),
486 ModpOutcome::Unsat(combo) => ModmOutcome::Unsat { modulus: p, combo },
487 });
488 }
489 let q = (p as i128).pow(k);
490 let m = equations.len();
491 let n = num_vars;
492 if m == 0 {
493 return Some(ModmOutcome::Sat(vec![0u64; n]));
494 }
495
496 let mut a = vec![vec![0i128; n]; m];
499 let mut b = vec![0i128; m];
500 let mut u = vec![vec![0i128; m]; m];
501 let mut v = vec![vec![0i128; n]; n];
502 for (i, ui) in u.iter_mut().enumerate() {
503 ui[i] = 1;
504 }
505 for (j, vj) in v.iter_mut().enumerate() {
506 vj[j] = 1;
507 }
508 for (i, eq) in equations.iter().enumerate() {
509 for &(var, coef) in &eq.coeffs {
510 if var < n {
511 a[i][var] = (a[i][var] + coef as i128).rem_euclid(q);
512 }
513 }
514 b[i] = (eq.rhs as i128).rem_euclid(q);
515 }
516
517 let exceeds_cap = |a: &[Vec<i128>], u: &[Vec<i128>], v: &[Vec<i128>]| {
518 a.iter().chain(u).chain(v).any(|r| r.iter().any(|&x| x.abs() > GROWTH_CAP))
519 };
520
521 let mut rank = 0usize;
522 for t in 0..m.min(n) {
523 loop {
524 let mut best: Option<(usize, usize, i128)> = None;
526 for (i, row) in a.iter().enumerate().skip(t) {
527 for (j, &val) in row.iter().enumerate().skip(t) {
528 if val != 0 && best.is_none_or(|(_, _, bv)| val.abs() < bv) {
529 best = Some((i, j, val.abs()));
530 }
531 }
532 }
533 let Some((pi, pj, _)) = best else { break };
534 if pi != t {
535 a.swap(pi, t);
536 u.swap(pi, t);
537 }
538 if pj != t {
539 for row in a.iter_mut() {
540 row.swap(pj, t);
541 }
542 for row in v.iter_mut() {
543 row.swap(pj, t);
544 }
545 }
546 let piv = a[t][t];
547 for i in 0..m {
548 if i != t && a[i][t] != 0 {
549 let f = a[i][t].div_euclid(piv);
550 if f != 0 {
551 for j in 0..n {
552 a[i][j] -= f * a[t][j];
553 }
554 for j in 0..m {
555 u[i][j] -= f * u[t][j];
556 }
557 }
558 }
559 }
560 for j in 0..n {
561 if j != t && a[t][j] != 0 {
562 let f = a[t][j].div_euclid(piv);
563 if f != 0 {
564 for row in a.iter_mut() {
565 row[j] -= f * row[t];
566 }
567 for row in v.iter_mut() {
568 row[j] -= f * row[t];
569 }
570 }
571 }
572 }
573 if exceeds_cap(&a, &u, &v) {
574 return None;
575 }
576 let col_clean = (0..m).all(|i| i == t || a[i][t] == 0);
577 let row_clean = (0..n).all(|j| j == t || a[t][j] == 0);
578 if col_clean && row_clean {
579 break;
580 }
581 }
582 if a[t][t] != 0 {
583 rank = t + 1;
584 } else {
585 break;
586 }
587 }
588
589 let ub: Vec<i128> =
590 (0..m).map(|i| (0..m).fold(0i128, |acc, r| acc + u[i][r] * b[r]).rem_euclid(q)).collect();
591 let combo_from_row = |row: &[i128], lambda: i128| -> Vec<(usize, u64)> {
592 row.iter()
593 .enumerate()
594 .map(|(i, &c)| (i, (lambda * c).rem_euclid(q) as u64))
595 .filter(|&(_, mlt)| mlt != 0)
596 .collect()
597 };
598
599 let mut y = vec![0i128; n];
600 for t in 0..rank {
601 let d = a[t][t].rem_euclid(q);
602 let rhs = ub[t];
603 let g = gcd_i128(d, q);
604 if rhs.rem_euclid(g) != 0 {
605 return Some(ModmOutcome::Unsat { modulus: q as u64, combo: combo_from_row(&u[t], q / g) });
607 }
608 let qg = q / g;
609 y[t] = if qg == 1 { 0 } else { (rhs / g).rem_euclid(qg) * modinv(d / g, qg) % qg };
610 }
611 for (t, &ubt) in ub.iter().enumerate().skip(rank) {
612 if ubt != 0 {
614 return Some(ModmOutcome::Unsat { modulus: q as u64, combo: combo_from_row(&u[t], 1) });
615 }
616 }
617
618 let x: Vec<u64> =
619 (0..n).map(|i| (0..n).fold(0i128, |acc, j| acc + v[i][j] * y[j]).rem_euclid(q) as u64).collect();
620 debug_assert!(satisfies(equations, &x, q as u64), "the ring model must satisfy mod p^k");
621 Some(ModmOutcome::Sat(x))
622}
623
624pub fn solve(equations: &[ModpEquation], num_vars: usize, m: u64) -> Option<ModmOutcome> {
630 let factors = prime_power_factorize(m)?;
631 let mut per_component: Vec<(u64, Vec<u64>)> = Vec::with_capacity(factors.len());
632 for (p, k) in factors {
633 let q = p.pow(k);
634 if q as u128 > 1_000_000_000 {
635 return None; }
637 match solve_prime_power(equations, num_vars, p, k)? {
638 ModmOutcome::Sat(a) => per_component.push((q, a)),
639 unsat @ ModmOutcome::Unsat { .. } => return Some(unsat),
640 }
641 }
642 let mut assignment = vec![0u64; num_vars];
643 for (i, slot) in assignment.iter_mut().enumerate() {
644 let residues: Vec<(u64, u64)> = per_component.iter().map(|(q, a)| (a[i], *q)).collect();
645 *slot = crt(&residues);
646 }
647 Some(ModmOutcome::Sat(assignment))
648}
649
650#[cfg(test)]
651mod tests {
652 use super::*;
653
654 fn splitmix(state: &mut u64) -> u64 {
655 *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
656 let mut z = *state;
657 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
658 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
659 z ^ (z >> 31)
660 }
661
662 fn brute_force_sat(equations: &[ModpEquation], num_vars: usize, m: u64) -> bool {
663 let total = (m as u128).pow(num_vars as u32);
664 for code in 0..total {
665 let mut a = vec![0u64; num_vars];
666 let mut c = code;
667 for slot in a.iter_mut() {
668 *slot = (c % m as u128) as u64;
669 c /= m as u128;
670 }
671 if satisfies(equations, &a, m) {
672 return true;
673 }
674 }
675 false
676 }
677
678 #[test]
681 fn squarefree_primes_is_exact() {
682 assert_eq!(squarefree_primes(6), Some(vec![2, 3]));
683 assert_eq!(squarefree_primes(30), Some(vec![2, 3, 5]));
684 assert_eq!(squarefree_primes(15), Some(vec![3, 5]));
685 assert_eq!(squarefree_primes(7), Some(vec![7]));
686 assert_eq!(squarefree_primes(105), Some(vec![3, 5, 7])); assert_eq!(squarefree_primes(1), None);
688 assert_eq!(squarefree_primes(4), None); assert_eq!(squarefree_primes(12), None); assert_eq!(squarefree_primes(9), None); assert_eq!(squarefree_primes(60), None); }
693
694 #[test]
699 fn solve_squarefree_matches_brute_force_over_composites() {
700 for &m in &[6u64, 10, 15, 30] {
701 let mut state = 0xC0DE_1234u64 ^ m;
702 for _ in 0..40 {
703 let num_vars = 2 + (splitmix(&mut state) % 2) as usize; let num_eqs = 1 + (splitmix(&mut state) % 4) as usize; let equations: Vec<ModpEquation> = (0..num_eqs)
706 .map(|_| {
707 let coeffs: Vec<(usize, u64)> = (0..num_vars)
708 .map(|v| (v, splitmix(&mut state) % m))
709 .filter(|&(_, a)| a != 0)
710 .collect();
711 ModpEquation::new(coeffs, splitmix(&mut state) % m)
712 })
713 .collect();
714 let brute = brute_force_sat(&equations, num_vars, m);
715 match solve_squarefree(&equations, num_vars, m).expect("m is squarefree") {
716 ModmOutcome::Sat(a) => {
717 assert!(brute, "m={m}: Sat but brute force UNSAT: {equations:?}");
718 assert!(satisfies(&equations, &a, m), "m={m}: the model must satisfy mod m: {a:?}");
719 assert!(a.iter().all(|&v| v < m), "m={m}: residues lie in 0..m");
720 }
721 ModmOutcome::Unsat { modulus, combo } => {
722 assert!(!brute, "m={m}: Unsat but a model exists: {equations:?}");
723 assert!(
724 is_refutation(&equations, num_vars, modulus, &combo),
725 "m={m}: the witness must re-check over ℤ/{modulus}: {combo:?}"
726 );
727 assert_eq!(m % modulus, 0, "m={m}: the witnessing modulus must divide m");
728 }
729 }
730 }
731 }
732 }
733
734 #[test]
740 fn the_mod_6_cycle_obstruction_is_caught_through_the_gf3_factor() {
741 let eqs = cycle_system(4, 6);
742 match solve_squarefree(&eqs, 4, 6).expect("6 is squarefree") {
743 ModmOutcome::Unsat { modulus, combo } => {
744 assert_eq!(modulus, 3, "the mod-6 obstruction lives in the GF(3) factor");
745 assert!(is_refutation(&eqs, 4, modulus, &combo), "the GF(3) refutation re-checks");
746 }
747 other => panic!("the mod-6 4-cycle must be UNSAT, got {other:?}"),
748 }
749 assert!(matches!(modp::solve(&eqs, 4, 2), ModpOutcome::Sat(_)), "GF(2) factor is consistent");
751 let eqs6 = cycle_system(6, 6);
753 match solve_squarefree(&eqs6, 6, 6).expect("6 is squarefree") {
754 ModmOutcome::Sat(a) => assert!(satisfies(&eqs6, &a, 6), "the 6-cycle model satisfies mod 6"),
755 other => panic!("the mod-6 6-cycle must be SAT, got {other:?}"),
756 }
757 }
758
759 #[test]
763 fn crt_recombines_distinct_residues_across_factors() {
764 let eqs = vec![ModpEquation::new(vec![(0, 1)], 5)];
766 match solve_squarefree(&eqs, 1, 6).expect("6 is squarefree") {
767 ModmOutcome::Sat(a) => {
768 assert_eq!(a, vec![5], "CRT(1 mod 2, 2 mod 3) = 5 mod 6");
769 assert!(satisfies(&eqs, &a, 6));
770 }
771 other => panic!("expected Sat, got {other:?}"),
772 }
773 assert_eq!(crt(&[(1, 2), (2, 3)]), 5);
775 assert_eq!(crt(&[(2, 3), (4, 5)]), 14); assert_eq!(crt(&[(0, 2), (0, 3), (0, 5)]), 0);
777 assert_eq!(crt(&[(3, 4), (2, 9)]), 11); }
780
781 #[test]
786 fn solve_prime_power_matches_brute_force_over_residue_rings() {
787 for &(p, k) in &[(2u64, 2u32), (2, 3), (2, 4), (3, 2), (3, 3), (5, 2)] {
788 let q = p.pow(k);
789 let mut state = 0xBEEF_0001u64 ^ q;
790 for _ in 0..40 {
791 let num_vars = 2 + (splitmix(&mut state) % 2) as usize;
792 let num_eqs = 1 + (splitmix(&mut state) % 4) as usize;
793 let equations: Vec<ModpEquation> = (0..num_eqs)
794 .map(|_| {
795 let coeffs: Vec<(usize, u64)> = (0..num_vars)
796 .map(|v| (v, splitmix(&mut state) % q))
797 .filter(|&(_, a)| a != 0)
798 .collect();
799 ModpEquation::new(coeffs, splitmix(&mut state) % q)
800 })
801 .collect();
802 let brute = brute_force_sat(&equations, num_vars, q);
803 match solve_prime_power(&equations, num_vars, p, k).expect("within the growth cap") {
804 ModmOutcome::Sat(a) => {
805 assert!(brute, "q={q}: Sat but brute force UNSAT: {equations:?}");
806 assert!(satisfies(&equations, &a, q), "q={q}: the ring model must satisfy: {a:?}");
807 assert!(a.iter().all(|&val| val < q), "q={q}: residues lie in 0..q");
808 }
809 ModmOutcome::Unsat { modulus, combo } => {
810 assert!(!brute, "q={q}: Unsat but a model exists: {equations:?}");
811 assert_eq!(modulus, q, "q={q}: the witness modulus is the prime power");
812 assert!(
813 is_refutation(&equations, num_vars, modulus, &combo),
814 "q={q}: the ring refutation must re-check: {combo:?}"
815 );
816 }
817 }
818 }
819 }
820 }
821
822 #[test]
825 fn solve_matches_brute_force_over_all_composites() {
826 for &m in &[4u64, 8, 9, 12, 18, 24, 36] {
827 let mut state = 0xABCD_0002u64 ^ m;
828 for _ in 0..30 {
829 let num_vars = 2 + (splitmix(&mut state) % 2) as usize;
830 let num_eqs = 1 + (splitmix(&mut state) % 4) as usize;
831 let equations: Vec<ModpEquation> = (0..num_eqs)
832 .map(|_| {
833 let coeffs: Vec<(usize, u64)> = (0..num_vars)
834 .map(|v| (v, splitmix(&mut state) % m))
835 .filter(|&(_, a)| a != 0)
836 .collect();
837 ModpEquation::new(coeffs, splitmix(&mut state) % m)
838 })
839 .collect();
840 let brute = brute_force_sat(&equations, num_vars, m);
841 match solve(&equations, num_vars, m).expect("m ≥ 2 and within the cap") {
842 ModmOutcome::Sat(a) => {
843 assert!(brute, "m={m}: Sat but brute force UNSAT: {equations:?}");
844 assert!(satisfies(&equations, &a, m), "m={m}: the model must satisfy mod m: {a:?}");
845 }
846 ModmOutcome::Unsat { modulus, combo } => {
847 assert!(!brute, "m={m}: Unsat but a model exists: {equations:?}");
848 assert_eq!(m % modulus, 0, "m={m}: the witnessing modulus must divide m");
849 assert!(
850 is_refutation(&equations, num_vars, modulus, &combo),
851 "m={m}: the witness must re-check over ℤ/{modulus}: {combo:?}"
852 );
853 }
854 }
855 }
856 }
857 }
858
859 #[test]
865 fn the_mod_4_obstruction_needs_the_ring_not_the_field() {
866 let unsat = vec![ModpEquation::new(vec![(0, 2)], 1)];
867 match solve(&unsat, 1, 4).unwrap() {
868 ModmOutcome::Unsat { modulus, combo } => {
869 assert_eq!(modulus, 4);
870 assert!(is_refutation(&unsat, 1, 4, &combo), "the ℤ/4 refutation re-checks: {combo:?}");
871 }
872 other => panic!("2x ≡ 1 (mod 4) is UNSAT, got {other:?}"),
873 }
874 let sat = vec![ModpEquation::new(vec![(0, 2)], 2)];
875 match solve(&sat, 1, 4).unwrap() {
876 ModmOutcome::Sat(a) => assert!(satisfies(&sat, &a, 4), "2x ≡ 2 (mod 4) has a model"),
877 other => panic!("2x ≡ 2 (mod 4) is SAT, got {other:?}"),
878 }
879 assert_eq!(prime_power_factorize(36), Some(vec![(2, 2), (3, 2)]));
880 assert_eq!(prime_power_factorize(8), Some(vec![(2, 3)]));
881 assert_eq!(prime_power_factorize(30), Some(vec![(2, 1), (3, 1), (5, 1)]));
882 }
883}