use crate::modp::{self, ModpEquation, ModpOutcome};
fn gcd_i128(a: i128, b: i128) -> i128 {
let (mut a, mut b) = (a.abs(), b.abs());
while b != 0 {
let t = a % b;
a = b;
b = t;
}
a
}
fn egcd(a: i128, b: i128) -> (i128, i128, i128) {
if b == 0 {
(a, 1, 0)
} else {
let (g, x, y) = egcd(b, a % b);
(g, y, x - (a / b) * y)
}
}
fn modinv(a: i128, n: i128) -> i128 {
let (_, x, _) = egcd(a.rem_euclid(n), n);
x.rem_euclid(n)
}
pub fn squarefree_primes(m: u64) -> Option<Vec<u64>> {
if m < 2 {
return None;
}
let mut primes = Vec::new();
let mut x = m;
let mut d = 2u64;
while d * d <= x {
if x % d == 0 {
x /= d;
if x % d == 0 {
return None; }
primes.push(d);
}
d += 1;
}
if x > 1 {
primes.push(x);
}
Some(primes)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ModmOutcome {
Sat(Vec<u64>),
Unsat { modulus: u64, combo: Vec<(usize, u64)> },
}
pub fn is_refutation(
equations: &[ModpEquation],
num_vars: usize,
modulus: u64,
combo: &[(usize, u64)],
) -> bool {
if combo.is_empty() {
return false;
}
let mm = modulus as u128;
let mut lhs = vec![0u128; num_vars];
let mut rhs = 0u128;
for &(idx, mult) in combo {
let Some(eq) = equations.get(idx) else {
return false;
};
for &(v, a) in &eq.coeffs {
if v < num_vars {
lhs[v] = (lhs[v] + mult as u128 * a as u128) % mm;
}
}
rhs = (rhs + mult as u128 * eq.rhs as u128) % mm;
}
lhs.iter().all(|&x| x == 0) && rhs != 0
}
pub fn solve_squarefree(equations: &[ModpEquation], num_vars: usize, m: u64) -> Option<ModmOutcome> {
let primes = squarefree_primes(m)?;
let mut per_prime: Vec<(u64, Vec<u64>)> = Vec::with_capacity(primes.len());
for &p in &primes {
match modp::solve(equations, num_vars, p) {
ModpOutcome::Sat(a) => per_prime.push((p, a)),
ModpOutcome::Unsat(combo) => return Some(ModmOutcome::Unsat { modulus: p, combo }),
}
}
let mut assignment = vec![0u64; num_vars];
for (i, slot) in assignment.iter_mut().enumerate() {
let residues: Vec<(u64, u64)> = per_prime.iter().map(|(p, a)| (a[i], *p)).collect();
*slot = crt(&residues);
}
Some(ModmOutcome::Sat(assignment))
}
fn crt(residues: &[(u64, u64)]) -> u64 {
let mut acc_r = 0i128;
let mut acc_m = 1i128;
for &(r, modu) in residues {
let modu = modu as i128;
let diff = (r as i128 - acc_r).rem_euclid(modu);
let t = (diff * modinv(acc_m.rem_euclid(modu), modu)).rem_euclid(modu);
acc_r += acc_m * t;
acc_m *= modu;
acc_r = acc_r.rem_euclid(acc_m);
}
acc_r as u64
}
pub enum ForcedM {
Inconsistent,
Forced(Vec<Option<u64>>),
}
pub fn forced_values_squarefree(equations: &[ModpEquation], num_vars: usize, m: u64) -> Option<ForcedM> {
let primes = squarefree_primes(m)?;
let mut per_prime: Vec<(u64, Vec<Option<u64>>)> = Vec::with_capacity(primes.len());
for &p in &primes {
let Some(ss) = crate::modp::solve_space(equations, num_vars, p) else {
return Some(ForcedM::Inconsistent);
};
let forced_p: Vec<Option<u64>> =
(0..num_vars).map(|g| ss.kernel_basis.iter().all(|k| k[g] == 0).then(|| ss.particular[g])).collect();
per_prime.push((p, forced_p));
}
let forced: Vec<Option<u64>> = (0..num_vars)
.map(|g| {
let residues: Option<Vec<(u64, u64)>> =
per_prime.iter().map(|(p, f)| f[g].map(|v| (v, *p))).collect();
residues.map(|res| crt(&res))
})
.collect();
Some(ForcedM::Forced(forced))
}
pub struct SolutionSpaceM {
pub num_vars: usize,
pub m: u64,
pub particular: Vec<u64>,
pub kernel_basis: Vec<Vec<u64>>,
}
pub enum PrimePowerSpace {
Inconsistent,
Space(SolutionSpaceM),
}
pub fn solve_space_prime_power(equations: &[ModpEquation], num_vars: usize, p: u64, k: u32) -> Option<PrimePowerSpace> {
if k == 1 {
return Some(match crate::modp::solve_space(equations, num_vars, p) {
None => PrimePowerSpace::Inconsistent,
Some(ss) => PrimePowerSpace::Space(SolutionSpaceM {
num_vars,
m: p,
particular: ss.particular,
kernel_basis: ss.kernel_basis,
}),
});
}
let q = (p as i128).pow(k);
let (m, n) = (equations.len(), num_vars);
let mut a = vec![vec![0i128; n]; m];
let mut b = vec![0i128; m];
let mut u = vec![vec![0i128; m]; m];
let mut v = vec![vec![0i128; n]; n];
for (i, ui) in u.iter_mut().enumerate() {
ui[i] = 1;
}
for (j, vj) in v.iter_mut().enumerate() {
vj[j] = 1;
}
for (i, eq) in equations.iter().enumerate() {
for &(var, coef) in &eq.coeffs {
if var < n {
a[i][var] = (a[i][var] + coef as i128).rem_euclid(q);
}
}
b[i] = (eq.rhs as i128).rem_euclid(q);
}
let exceeds = |a: &[Vec<i128>], u: &[Vec<i128>], v: &[Vec<i128>]| {
a.iter().chain(u).chain(v).any(|r| r.iter().any(|&x| x.abs() > GROWTH_CAP))
};
let mut rank = 0usize;
for t in 0..m.min(n) {
loop {
let mut best: Option<(usize, usize, i128)> = None;
for (i, row) in a.iter().enumerate().skip(t) {
for (j, &val) in row.iter().enumerate().skip(t) {
if val != 0 && best.is_none_or(|(_, _, bv)| val.abs() < bv) {
best = Some((i, j, val.abs()));
}
}
}
let Some((pi, pj, _)) = best else { break };
if pi != t {
a.swap(pi, t);
u.swap(pi, t);
}
if pj != t {
for row in a.iter_mut() {
row.swap(pj, t);
}
for row in v.iter_mut() {
row.swap(pj, t);
}
}
let piv = a[t][t];
for i in 0..m {
if i != t && a[i][t] != 0 {
let f = a[i][t].div_euclid(piv);
if f != 0 {
for j in 0..n {
a[i][j] -= f * a[t][j];
}
for j in 0..m {
u[i][j] -= f * u[t][j];
}
}
}
}
for j in 0..n {
if j != t && a[t][j] != 0 {
let f = a[t][j].div_euclid(piv);
if f != 0 {
for row in a.iter_mut() {
row[j] -= f * row[t];
}
for row in v.iter_mut() {
row[j] -= f * row[t];
}
}
}
}
if exceeds(&a, &u, &v) {
return None;
}
if (0..m).all(|i| i == t || a[i][t] == 0) && (0..n).all(|j| j == t || a[t][j] == 0) {
break;
}
}
if a[t][t] != 0 {
rank = t + 1;
} else {
break;
}
}
let ub: Vec<i128> = (0..m).map(|i| (0..m).fold(0i128, |acc, r| acc + u[i][r] * b[r]).rem_euclid(q)).collect();
let mut y = vec![0i128; n];
let mut freedom = vec![1i128; n];
for t in 0..rank {
let d = a[t][t].rem_euclid(q);
let g = gcd_i128(d, q);
if ub[t].rem_euclid(g) != 0 {
return Some(PrimePowerSpace::Inconsistent);
}
let qg = q / g;
y[t] = if qg == 1 { 0 } else { (ub[t] / g).rem_euclid(qg) * modinv(d / g, qg) % qg };
freedom[t] = qg;
}
for &ubt in ub.iter().skip(rank) {
if ubt != 0 {
return Some(PrimePowerSpace::Inconsistent); }
}
let particular: Vec<u64> =
(0..n).map(|i| (0..n).fold(0i128, |acc, j| acc + v[i][j] * y[j]).rem_euclid(q) as u64).collect();
let mut kernel_basis: Vec<Vec<u64>> = Vec::new();
for t in 0..n {
if freedom[t] >= q {
continue;
}
let gen: Vec<u64> = (0..n).map(|i| (v[i][t] * freedom[t]).rem_euclid(q) as u64).collect();
if gen.iter().any(|&x| x != 0) {
kernel_basis.push(gen);
}
}
Some(PrimePowerSpace::Space(SolutionSpaceM { num_vars: n, m: q as u64, particular, kernel_basis }))
}
pub fn forced_values_prime_power(equations: &[ModpEquation], num_vars: usize, m: u64) -> Option<ForcedM> {
let factors = prime_power_factorize(m)?;
let mut per_component: Vec<(u64, Vec<Option<u64>>)> = Vec::with_capacity(factors.len());
for (p, k) in factors {
match solve_space_prime_power(equations, num_vars, p, k)? {
PrimePowerSpace::Inconsistent => return Some(ForcedM::Inconsistent),
PrimePowerSpace::Space(ss) => {
let forced_c: Vec<Option<u64>> = (0..num_vars)
.map(|g| ss.kernel_basis.iter().all(|kk| kk[g] == 0).then(|| ss.particular[g]))
.collect();
per_component.push((p.pow(k), forced_c));
}
}
}
let forced: Vec<Option<u64>> = (0..num_vars)
.map(|g| {
let residues: Option<Vec<(u64, u64)>> =
per_component.iter().map(|(q, f)| f[g].map(|v| (v, *q))).collect();
residues.map(|res| crt(&res))
})
.collect();
Some(ForcedM::Forced(forced))
}
pub enum AllowedOutcome {
Inconsistent,
Allowed(Vec<(u64, u64)>),
}
pub fn allowed_residues(equations: &[ModpEquation], num_vars: usize, m: u64) -> Option<AllowedOutcome> {
let factors = prime_power_factorize(m)?;
let mut per_component: Vec<Vec<(u64, u64)>> = Vec::with_capacity(factors.len());
for (p, k) in factors {
let q = p.pow(k);
match solve_space_prime_power(equations, num_vars, p, k)? {
PrimePowerSpace::Inconsistent => return Some(AllowedOutcome::Inconsistent),
PrimePowerSpace::Space(ss) => {
let pv: Vec<(u64, u64)> = (0..num_vars)
.map(|g| {
let d = ss.kernel_basis.iter().fold(q, |acc, kk| gcd_i128(acc as i128, kk[g] as i128) as u64);
(ss.particular[g] % d, d)
})
.collect();
per_component.push(pv);
}
}
}
let residues: Vec<(u64, u64)> = (0..num_vars)
.map(|g| {
let pairs: Vec<(u64, u64)> = per_component.iter().map(|pv| pv[g]).filter(|&(_, d)| d > 1).collect();
if pairs.is_empty() {
(0, 1)
} else {
(crt(&pairs), pairs.iter().map(|&(_, d)| d).product())
}
})
.collect();
Some(AllowedOutcome::Allowed(residues))
}
pub fn satisfies(equations: &[ModpEquation], assignment: &[u64], m: u64) -> bool {
let mm = m as u128;
equations.iter().all(|eq| {
let lhs = eq.coeffs.iter().fold(0u128, |acc, &(v, a)| {
(acc + a as u128 * *assignment.get(v).unwrap_or(&0) as u128) % mm
});
lhs == (eq.rhs as u128 % mm)
})
}
pub fn cycle_system(n: usize, m: u64) -> Vec<ModpEquation> {
modp::cycle_system(n, m)
}
pub fn prime_power_factorize(m: u64) -> Option<Vec<(u64, u32)>> {
if m < 2 {
return None;
}
let mut out = Vec::new();
let mut x = m;
let mut d = 2u64;
while d * d <= x {
if x % d == 0 {
let mut k = 0u32;
while x % d == 0 {
x /= d;
k += 1;
}
out.push((d, k));
}
d += 1;
}
if x > 1 {
out.push((x, 1));
}
Some(out)
}
const GROWTH_CAP: i128 = 1i128 << 60;
pub fn solve_prime_power(
equations: &[ModpEquation],
num_vars: usize,
p: u64,
k: u32,
) -> Option<ModmOutcome> {
if k == 1 {
return Some(match modp::solve(equations, num_vars, p) {
ModpOutcome::Sat(a) => ModmOutcome::Sat(a),
ModpOutcome::Unsat(combo) => ModmOutcome::Unsat { modulus: p, combo },
});
}
let q = (p as i128).pow(k);
let m = equations.len();
let n = num_vars;
if m == 0 {
return Some(ModmOutcome::Sat(vec![0u64; n]));
}
let mut a = vec![vec![0i128; n]; m];
let mut b = vec![0i128; m];
let mut u = vec![vec![0i128; m]; m];
let mut v = vec![vec![0i128; n]; n];
for (i, ui) in u.iter_mut().enumerate() {
ui[i] = 1;
}
for (j, vj) in v.iter_mut().enumerate() {
vj[j] = 1;
}
for (i, eq) in equations.iter().enumerate() {
for &(var, coef) in &eq.coeffs {
if var < n {
a[i][var] = (a[i][var] + coef as i128).rem_euclid(q);
}
}
b[i] = (eq.rhs as i128).rem_euclid(q);
}
let exceeds_cap = |a: &[Vec<i128>], u: &[Vec<i128>], v: &[Vec<i128>]| {
a.iter().chain(u).chain(v).any(|r| r.iter().any(|&x| x.abs() > GROWTH_CAP))
};
let mut rank = 0usize;
for t in 0..m.min(n) {
loop {
let mut best: Option<(usize, usize, i128)> = None;
for (i, row) in a.iter().enumerate().skip(t) {
for (j, &val) in row.iter().enumerate().skip(t) {
if val != 0 && best.is_none_or(|(_, _, bv)| val.abs() < bv) {
best = Some((i, j, val.abs()));
}
}
}
let Some((pi, pj, _)) = best else { break };
if pi != t {
a.swap(pi, t);
u.swap(pi, t);
}
if pj != t {
for row in a.iter_mut() {
row.swap(pj, t);
}
for row in v.iter_mut() {
row.swap(pj, t);
}
}
let piv = a[t][t];
for i in 0..m {
if i != t && a[i][t] != 0 {
let f = a[i][t].div_euclid(piv);
if f != 0 {
for j in 0..n {
a[i][j] -= f * a[t][j];
}
for j in 0..m {
u[i][j] -= f * u[t][j];
}
}
}
}
for j in 0..n {
if j != t && a[t][j] != 0 {
let f = a[t][j].div_euclid(piv);
if f != 0 {
for row in a.iter_mut() {
row[j] -= f * row[t];
}
for row in v.iter_mut() {
row[j] -= f * row[t];
}
}
}
}
if exceeds_cap(&a, &u, &v) {
return None;
}
let col_clean = (0..m).all(|i| i == t || a[i][t] == 0);
let row_clean = (0..n).all(|j| j == t || a[t][j] == 0);
if col_clean && row_clean {
break;
}
}
if a[t][t] != 0 {
rank = t + 1;
} else {
break;
}
}
let ub: Vec<i128> =
(0..m).map(|i| (0..m).fold(0i128, |acc, r| acc + u[i][r] * b[r]).rem_euclid(q)).collect();
let combo_from_row = |row: &[i128], lambda: i128| -> Vec<(usize, u64)> {
row.iter()
.enumerate()
.map(|(i, &c)| (i, (lambda * c).rem_euclid(q) as u64))
.filter(|&(_, mlt)| mlt != 0)
.collect()
};
let mut y = vec![0i128; n];
for t in 0..rank {
let d = a[t][t].rem_euclid(q);
let rhs = ub[t];
let g = gcd_i128(d, q);
if rhs.rem_euclid(g) != 0 {
return Some(ModmOutcome::Unsat { modulus: q as u64, combo: combo_from_row(&u[t], q / g) });
}
let qg = q / g;
y[t] = if qg == 1 { 0 } else { (rhs / g).rem_euclid(qg) * modinv(d / g, qg) % qg };
}
for (t, &ubt) in ub.iter().enumerate().skip(rank) {
if ubt != 0 {
return Some(ModmOutcome::Unsat { modulus: q as u64, combo: combo_from_row(&u[t], 1) });
}
}
let x: Vec<u64> =
(0..n).map(|i| (0..n).fold(0i128, |acc, j| acc + v[i][j] * y[j]).rem_euclid(q) as u64).collect();
debug_assert!(satisfies(equations, &x, q as u64), "the ring model must satisfy mod p^k");
Some(ModmOutcome::Sat(x))
}
pub fn solve(equations: &[ModpEquation], num_vars: usize, m: u64) -> Option<ModmOutcome> {
let factors = prime_power_factorize(m)?;
let mut per_component: Vec<(u64, Vec<u64>)> = Vec::with_capacity(factors.len());
for (p, k) in factors {
let q = p.pow(k);
if q as u128 > 1_000_000_000 {
return None; }
match solve_prime_power(equations, num_vars, p, k)? {
ModmOutcome::Sat(a) => per_component.push((q, a)),
unsat @ ModmOutcome::Unsat { .. } => return Some(unsat),
}
}
let mut assignment = vec![0u64; num_vars];
for (i, slot) in assignment.iter_mut().enumerate() {
let residues: Vec<(u64, u64)> = per_component.iter().map(|(q, a)| (a[i], *q)).collect();
*slot = crt(&residues);
}
Some(ModmOutcome::Sat(assignment))
}
#[cfg(test)]
mod tests {
use super::*;
fn splitmix(state: &mut u64) -> u64 {
*state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn brute_force_sat(equations: &[ModpEquation], num_vars: usize, m: u64) -> bool {
let total = (m as u128).pow(num_vars as u32);
for code in 0..total {
let mut a = vec![0u64; num_vars];
let mut c = code;
for slot in a.iter_mut() {
*slot = (c % m as u128) as u64;
c /= m as u128;
}
if satisfies(equations, &a, m) {
return true;
}
}
false
}
#[test]
fn squarefree_primes_is_exact() {
assert_eq!(squarefree_primes(6), Some(vec![2, 3]));
assert_eq!(squarefree_primes(30), Some(vec![2, 3, 5]));
assert_eq!(squarefree_primes(15), Some(vec![3, 5]));
assert_eq!(squarefree_primes(7), Some(vec![7]));
assert_eq!(squarefree_primes(105), Some(vec![3, 5, 7])); assert_eq!(squarefree_primes(1), None);
assert_eq!(squarefree_primes(4), None); assert_eq!(squarefree_primes(12), None); assert_eq!(squarefree_primes(9), None); assert_eq!(squarefree_primes(60), None); }
#[test]
fn solve_squarefree_matches_brute_force_over_composites() {
for &m in &[6u64, 10, 15, 30] {
let mut state = 0xC0DE_1234u64 ^ m;
for _ in 0..40 {
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)
.map(|_| {
let coeffs: Vec<(usize, u64)> = (0..num_vars)
.map(|v| (v, splitmix(&mut state) % m))
.filter(|&(_, a)| a != 0)
.collect();
ModpEquation::new(coeffs, splitmix(&mut state) % m)
})
.collect();
let brute = brute_force_sat(&equations, num_vars, m);
match solve_squarefree(&equations, num_vars, m).expect("m is squarefree") {
ModmOutcome::Sat(a) => {
assert!(brute, "m={m}: Sat but brute force UNSAT: {equations:?}");
assert!(satisfies(&equations, &a, m), "m={m}: the model must satisfy mod m: {a:?}");
assert!(a.iter().all(|&v| v < m), "m={m}: residues lie in 0..m");
}
ModmOutcome::Unsat { modulus, combo } => {
assert!(!brute, "m={m}: Unsat but a model exists: {equations:?}");
assert!(
is_refutation(&equations, num_vars, modulus, &combo),
"m={m}: the witness must re-check over ℤ/{modulus}: {combo:?}"
);
assert_eq!(m % modulus, 0, "m={m}: the witnessing modulus must divide m");
}
}
}
}
}
#[test]
fn the_mod_6_cycle_obstruction_is_caught_through_the_gf3_factor() {
let eqs = cycle_system(4, 6);
match solve_squarefree(&eqs, 4, 6).expect("6 is squarefree") {
ModmOutcome::Unsat { modulus, combo } => {
assert_eq!(modulus, 3, "the mod-6 obstruction lives in the GF(3) factor");
assert!(is_refutation(&eqs, 4, modulus, &combo), "the GF(3) refutation re-checks");
}
other => panic!("the mod-6 4-cycle must be UNSAT, got {other:?}"),
}
assert!(matches!(modp::solve(&eqs, 4, 2), ModpOutcome::Sat(_)), "GF(2) factor is consistent");
let eqs6 = cycle_system(6, 6);
match solve_squarefree(&eqs6, 6, 6).expect("6 is squarefree") {
ModmOutcome::Sat(a) => assert!(satisfies(&eqs6, &a, 6), "the 6-cycle model satisfies mod 6"),
other => panic!("the mod-6 6-cycle must be SAT, got {other:?}"),
}
}
#[test]
fn crt_recombines_distinct_residues_across_factors() {
let eqs = vec![ModpEquation::new(vec![(0, 1)], 5)];
match solve_squarefree(&eqs, 1, 6).expect("6 is squarefree") {
ModmOutcome::Sat(a) => {
assert_eq!(a, vec![5], "CRT(1 mod 2, 2 mod 3) = 5 mod 6");
assert!(satisfies(&eqs, &a, 6));
}
other => panic!("expected Sat, got {other:?}"),
}
assert_eq!(crt(&[(1, 2), (2, 3)]), 5);
assert_eq!(crt(&[(2, 3), (4, 5)]), 14); assert_eq!(crt(&[(0, 2), (0, 3), (0, 5)]), 0);
assert_eq!(crt(&[(3, 4), (2, 9)]), 11); }
#[test]
fn solve_prime_power_matches_brute_force_over_residue_rings() {
for &(p, k) in &[(2u64, 2u32), (2, 3), (2, 4), (3, 2), (3, 3), (5, 2)] {
let q = p.pow(k);
let mut state = 0xBEEF_0001u64 ^ q;
for _ in 0..40 {
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)
.map(|_| {
let coeffs: Vec<(usize, u64)> = (0..num_vars)
.map(|v| (v, splitmix(&mut state) % q))
.filter(|&(_, a)| a != 0)
.collect();
ModpEquation::new(coeffs, splitmix(&mut state) % q)
})
.collect();
let brute = brute_force_sat(&equations, num_vars, q);
match solve_prime_power(&equations, num_vars, p, k).expect("within the growth cap") {
ModmOutcome::Sat(a) => {
assert!(brute, "q={q}: Sat but brute force UNSAT: {equations:?}");
assert!(satisfies(&equations, &a, q), "q={q}: the ring model must satisfy: {a:?}");
assert!(a.iter().all(|&val| val < q), "q={q}: residues lie in 0..q");
}
ModmOutcome::Unsat { modulus, combo } => {
assert!(!brute, "q={q}: Unsat but a model exists: {equations:?}");
assert_eq!(modulus, q, "q={q}: the witness modulus is the prime power");
assert!(
is_refutation(&equations, num_vars, modulus, &combo),
"q={q}: the ring refutation must re-check: {combo:?}"
);
}
}
}
}
}
#[test]
fn solve_matches_brute_force_over_all_composites() {
for &m in &[4u64, 8, 9, 12, 18, 24, 36] {
let mut state = 0xABCD_0002u64 ^ m;
for _ in 0..30 {
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)
.map(|_| {
let coeffs: Vec<(usize, u64)> = (0..num_vars)
.map(|v| (v, splitmix(&mut state) % m))
.filter(|&(_, a)| a != 0)
.collect();
ModpEquation::new(coeffs, splitmix(&mut state) % m)
})
.collect();
let brute = brute_force_sat(&equations, num_vars, m);
match solve(&equations, num_vars, m).expect("m ≥ 2 and within the cap") {
ModmOutcome::Sat(a) => {
assert!(brute, "m={m}: Sat but brute force UNSAT: {equations:?}");
assert!(satisfies(&equations, &a, m), "m={m}: the model must satisfy mod m: {a:?}");
}
ModmOutcome::Unsat { modulus, combo } => {
assert!(!brute, "m={m}: Unsat but a model exists: {equations:?}");
assert_eq!(m % modulus, 0, "m={m}: the witnessing modulus must divide m");
assert!(
is_refutation(&equations, num_vars, modulus, &combo),
"m={m}: the witness must re-check over ℤ/{modulus}: {combo:?}"
);
}
}
}
}
}
#[test]
fn the_mod_4_obstruction_needs_the_ring_not_the_field() {
let unsat = vec![ModpEquation::new(vec![(0, 2)], 1)];
match solve(&unsat, 1, 4).unwrap() {
ModmOutcome::Unsat { modulus, combo } => {
assert_eq!(modulus, 4);
assert!(is_refutation(&unsat, 1, 4, &combo), "the ℤ/4 refutation re-checks: {combo:?}");
}
other => panic!("2x ≡ 1 (mod 4) is UNSAT, got {other:?}"),
}
let sat = vec![ModpEquation::new(vec![(0, 2)], 2)];
match solve(&sat, 1, 4).unwrap() {
ModmOutcome::Sat(a) => assert!(satisfies(&sat, &a, 4), "2x ≡ 2 (mod 4) has a model"),
other => panic!("2x ≡ 2 (mod 4) is SAT, got {other:?}"),
}
assert_eq!(prime_power_factorize(36), Some(vec![(2, 2), (3, 2)]));
assert_eq!(prime_power_factorize(8), Some(vec![(2, 3)]));
assert_eq!(prime_power_factorize(30), Some(vec![(2, 1), (3, 1), (5, 1)]));
}
}