Skip to main content

logicaffeine_proof/
modm.rs

1//! Linear algebra over `ℤ/m` for **composite** `m`, by the Chinese Remainder Theorem — the
2//! multiplicative symmetry break. A system `Σ aᵢ·xᵢ ≡ c (mod m)` over a **squarefree** modulus
3//! `m = p₁·…·p_t` factors, through the ring isomorphism `ℤ/m ≅ GF(p₁) × … × GF(p_t)`, into one
4//! independent system over each prime field — each decided by [`crate::modp::solve`] — and the
5//! per-field solutions are recombined by CRT. The *factorization of the modulus* is the symmetry that
6//! splits the problem; the prime fields are the irreducible pieces:
7//!
8//! - **consistent mod `m`  ⟺  consistent over every prime factor `pᵢ`**, and the recombined residues
9//!   give an assignment over `ℤ/m` that re-checks ([`satisfies`]);
10//! - **inconsistent mod `m`  ⟺  some prime factor's system is inconsistent**, and that prime together
11//!   with its GF(p) refutation combination is the re-checkable witness ([`crate::modp::is_refutation`]).
12//!
13//! This is [`crate::modp`] (the prime-field cut, itself the prime generalization of the GF(2) parity
14//! cut) carried up onto the composites by multiplication. Prime-power moduli `p^k` need the residue
15//! *ring* `ℤ/p^k` (a different object — `p` is a zero divisor), a separate rung; this module is exact
16//! and complete for squarefree `m`.
17
18use 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
30/// Extended Euclid: `(g, x, y)` with `a·x + b·y = g = gcd(a,b)`.
31fn 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
40/// Inverse of `a` mod `n` (n need not be prime), assuming `gcd(a, n) = 1`; result in `0..n`.
41fn modinv(a: i128, n: i128) -> i128 {
42    let (_, x, _) = egcd(a.rem_euclid(n), n);
43    x.rem_euclid(n)
44}
45
46/// The distinct prime factors of `m`, in increasing order — or `None` if `m < 2` or `m` is not
47/// squarefree (some `p² | m`). Squarefreeness is exactly the condition under which `ℤ/m` is a product
48/// of fields, so the CRT-over-prime-fields decision procedure below is complete.
49pub 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; // p² | m ⇒ ℤ/m is not a product of fields
61            }
62            primes.push(d);
63        }
64        d += 1;
65    }
66    if x > 1 {
67        primes.push(x);
68    }
69    Some(primes)
70}
71
72/// The outcome of deciding a linear system over `ℤ/m`.
73#[derive(Clone, Debug, PartialEq, Eq)]
74pub enum ModmOutcome {
75    /// Satisfiable, with an assignment over `0..m` for each variable (re-checkable via [`satisfies`]).
76    Sat(Vec<u64>),
77    /// Unsatisfiable: inconsistent already over the quotient ring `ℤ/modulus` for a prime-power factor
78    /// `modulus = pᵏ` of `m` (a prime field when `k = 1`), witnessed by a combination of the equations
79    /// whose left side cancels mod `modulus` while the right side does not. Because `modulus | m`, any
80    /// solution mod `m` would reduce to one mod `modulus`, so this certifies UNSAT mod `m`. Re-checkable
81    /// via [`is_refutation`].
82    Unsat { modulus: u64, combo: Vec<(usize, u64)> },
83}
84
85/// Re-check a refutation over `ℤ/modulus`: the chosen combination of equations has every variable
86/// coefficient `≡ 0` and a nonzero right-hand side mod `modulus` — a solver-free certificate that the
87/// system is inconsistent over that quotient ring (and hence over any `ℤ/m` with `modulus | m`).
88pub 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
114/// Decide a linear system over `ℤ/m` for **squarefree** `m`, via CRT over the prime fields. Returns
115/// `None` only when `m` is not squarefree (the prime-power ring is a separate rung). The first prime
116/// factor whose system is inconsistent is reported as the witness — by CRT, the whole composite system
117/// is then inconsistent.
118pub 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    // Every prime field is consistent: recombine each variable's residues into a value mod m by CRT.
128    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
136/// Combine residues `(rᵢ mod mᵢ)` with pairwise-coprime moduli (primes or prime powers) into the unique
137/// value mod `∏ mᵢ`, by incremental CRT: `x ≡ acc (mod M)` and `x ≡ r (mod mᵢ)` ⟹
138/// `x = acc + M·((r−acc)·M⁻¹ mod mᵢ)`.
139fn 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
153/// The forced-value structure of a `ℤ/m` system over a **squarefree** `m`.
154pub enum ForcedM {
155    /// Inconsistent over `ℤ/m` (UNSAT) — some prime field is inconsistent.
156    Inconsistent,
157    /// `forced[g] = Some(v)` iff `x_g ≡ v (mod m)` in *every* solution, else `None`.
158    Forced(Vec<Option<u64>>),
159}
160
161/// Which variables a **squarefree** `ℤ/m` system forces to a single value, by CRT of the `GF(pᵢ)` solution
162/// spaces (`ℤ/m ≅ ∏ GF(pᵢ)`): a variable is forced mod `m` iff it is forced mod *every* prime factor
163/// ([`crate::modp::solve_space`] — its kernel never moves it), and the value is the CRT of those residues.
164/// `None` if `m` is not squarefree (the prime-power case needs Smith normal form, not handled here). This
165/// is the substrate of the composite affine SAT-side break.
166pub 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
187/// The complete solution space of a `ℤ/pᵏ` linear system, in symmetry-broken form: a particular solution
188/// plus a generating set of the kernel **submodule** (each generator is a column of the Smith transform
189/// `V` scaled by its pivot's freedom `q/gcd(dₜ,q)`). The prime-power analogue of
190/// [`crate::modp::SolutionSpaceP`] over a local ring — and the substrate of the *scalable* forced/linked
191/// break: a variable the kernel never moves is forced.
192pub 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
199/// The outcome of [`solve_space_prime_power`].
200pub enum PrimePowerSpace {
201    Inconsistent,
202    Space(SolutionSpaceM),
203}
204
205/// Solve a `ℤ/pᵏ` system for its full solution **space** via Smith normal form `U·A·V = D` — the scalable
206/// analogue of [`crate::modp::solve_space`] over a prime-power ring (where `p` is a zero-divisor, so field
207/// Gaussian fails). Returns the particular solution plus the kernel generators (so a variable the kernel
208/// never moves is forced), or `Inconsistent`. `None` only when the integer Smith reduction overflows
209/// [`GROWTH_CAP`]. `k = 1` delegates to the prime-field solver.
210pub 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    // Solve D·y ≡ ub for the particular y, recording each coordinate's freedom step hₜ = q/gcd(dₜ,q)
312    // (= q ⇒ forced; < q ⇒ the kernel can move yₜ by hₜ). Free columns (t ≥ rank) move by 1.
313    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); // a zero row with nonzero rhs ⇒ UNSAT
328        }
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    // Kernel generators: δx = hₜ·(t-th column of V), for every coordinate with genuine freedom (hₜ < q).
333    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
346/// Which variables a `ℤ/m` system forces, for **any** composite `m` — by CRT over its prime-power
347/// components (`ℤ/m ≅ ∏ ℤ/pᵢ^{kᵢ}`), each solved scalably by Smith normal form
348/// ([`solve_space_prime_power`]): a variable is forced mod `m` iff its kernel never moves it in *every*
349/// component, and the value is the CRT of the residues. This is the Smith-form generalization that
350/// replaces the old bounded brute force — no size cap; `None` only on Smith overflow. `Inconsistent` when
351/// any component is.
352pub 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
376/// The per-variable **allowed-value congruence** of a `ℤ/m` system, for any composite `m`. By CRT over the
377/// prime-power components, each variable's solution-projection is a coset `x_g ≡ residue (mod modulus)` —
378/// the *tightest* congruence the system forces on it. Generalizes forcing: `modulus = m` is a single value
379/// (fully forced), `1 < modulus < m` is **partial** (confined to a value-subset), `modulus = 1` is free.
380/// Within each prime-power component the coset modulus is `gcd(p^k, kernel column at g)` from
381/// [`solve_space_prime_power`]; the components recombine by CRT. `Inconsistent` if any component is.
382pub enum AllowedOutcome {
383    Inconsistent,
384    Allowed(Vec<(u64, u64)>),
385}
386
387/// See [`AllowedOutcome`]. `None` only on Smith overflow.
388pub 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                        // The coset modulus in this component: gcd of the ring order with the kernel's
399                        // moves of x_g (an empty kernel ⇒ gcd = q ⇒ fully pinned).
400                        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            // CRT the constrained components (their moduli are pairwise-coprime prime powers); a component
411            // that leaves x_g free (modulus 1) contributes nothing.
412            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
423/// Re-check a satisfying assignment over `ℤ/m`: every congruence holds mod `m`.
424pub 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
434/// The scalable composite obstruction: the `n`-cycle of differences `xᵢ − x_{i+1} ≡ 1 (mod m)`,
435/// inconsistent **exactly when `m ∤ n`** (summing telescopes the left to `0`, the right to `n`). The
436/// coefficient `−1` is written `m − 1`. Reuses [`crate::modp::cycle_system`]'s shape across the field.
437pub fn cycle_system(n: usize, m: u64) -> Vec<ModpEquation> {
438    modp::cycle_system(n, m)
439}
440
441/// The prime-power factorization of `m` (`m = ∏ pᵢ^{kᵢ}`), or `None` if `m < 2`.
442pub 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
466/// Integer-magnitude ceiling for the Smith diagonalization; beyond it we decline rather than risk
467/// `i128` overflow. Small prime-power components stay far below this.
468const GROWTH_CAP: i128 = 1i128 << 60;
469
470/// Decide a linear system over the residue **ring** `ℤ/pᵏ`. For `k = 1` this is the prime field, handed
471/// to the proven [`crate::modp::solve`]. For `k ≥ 2` the ring has zero divisors, so we diagonalize over
472/// the integers — `U·A·V = D` with `U, V` unimodular (Smith-style: gcd row/column reduction) — after
473/// which each `dₜ·yₜ ≡ (U·b)ₜ (mod pᵏ)` is an independent 1-D congruence, solvable iff
474/// `gcd(dₜ, pᵏ) ∣ (U·b)ₜ`. `U` yields both the satisfying assignment (`x = V·y`) and, on failure, the
475/// re-checkable refutation (a row of `U`, scaled to annihilate the coefficients mod `pᵏ`). Returns
476/// `None` only if the integer entries would exceed [`GROWTH_CAP`].
477pub 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    // Working matrix `a = U·A·V`, with U (m×m) and V (n×n) the accumulated unimodular transforms, and
497    // `b` the original right-hand side (so `U·b` is the transformed rhs). All over ℤ.
498    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            // Pivot on the minimal-magnitude nonzero entry of the active submatrix — the gcd descent.
525            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            // No solution to dₜ·yₜ ≡ rhs: scale this row by q/g to annihilate the coefficients mod q.
606            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        // A zero row of D with a nonzero transformed rhs: uₜ·A = 0 over ℤ, so uₜ is the refutation.
613        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
624/// Decide a linear system over `ℤ/m` for **any** `m ≥ 2`, closing every composite modulus. By CRT,
625/// `ℤ/m ≅ ∏ ℤ/pᵢ^{kᵢ}`: solve each prime-power component ([`solve_prime_power`]) and recombine the
626/// residues. Consistent mod `m` ⟺ consistent over every prime-power factor; the first inconsistent
627/// factor is the witness (and certifies UNSAT mod `m`, since that factor divides `m`). Returns `None`
628/// only if `m < 2`, or a component is too large / overflows the integer Smith reduction.
629pub 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; // decline an oversized prime-power component
636        }
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    /// **Squarefree factorization is exact, and rejects non-squarefree moduli.** The CRT decision
679    /// procedure is complete precisely on squarefree `m`, so the gate must be precise.
680    #[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])); // 3·5·7
687        assert_eq!(squarefree_primes(1), None);
688        assert_eq!(squarefree_primes(4), None); // 2²
689        assert_eq!(squarefree_primes(12), None); // 2²·3
690        assert_eq!(squarefree_primes(9), None); // 3²
691        assert_eq!(squarefree_primes(60), None); // 2²·3·5
692    }
693
694    /// **The composite cut, verified to the point of absurdity against brute force.** Over squarefree
695    /// moduli `m ∈ {6, 10, 15, 30}`, on a fuzz of random systems, `solve_squarefree`'s verdict always
696    /// matches exhaustive search over all `m^vars` assignments — every `Sat` recombination re-checks
697    /// mod `m`, every `Unsat` re-checks as a GF(prime) refutation at its witnessing prime.
698    #[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; // 2..3 vars
704                let num_eqs = 1 + (splitmix(&mut state) % 4) as usize; // 1..4 eqs
705                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    /// **The composite obstruction is found via a prime field GF(2) is blind to.** The mod-6 4-cycle is
735    /// inconsistent (4 is not a multiple of 6). By CRT this is invisible to the GF(2) component (4 ≡ 0
736    /// mod 2 ⟹ that field is consistent) and is caught only in the GF(3) component (4 ≡ 1 mod 3 ⟹
737    /// `0 ≡ 1`). The witness is the GF(3) refutation, and it re-checks. The multiplicative split reaches
738    /// a composite obstruction neither prime field reaches alone.
739    #[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        // The GF(2) factor on its own is perfectly consistent — the parity cut sees nothing here.
750        assert!(matches!(modp::solve(&eqs, 4, 2), ModpOutcome::Sat(_)), "GF(2) factor is consistent");
751        // And a 6-cycle (n = 6, a multiple of 6) is satisfiable mod 6, recombined across both fields.
752        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    /// The CRT recombination is genuine: a system consistent over each prime factor but with *different*
760    /// residues per factor is solved by stitching them into one value mod m. `x ≡ 1 (mod 2)` and
761    /// `x ≡ 2 (mod 3)` ⟹ `x ≡ 5 (mod 6)`.
762    #[test]
763    fn crt_recombines_distinct_residues_across_factors() {
764        // Single variable pinned to 5 mod 6 (1 mod 2, 2 mod 3).
765        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        // Direct CRT check on the primitive.
774        assert_eq!(crt(&[(1, 2), (2, 3)]), 5);
775        assert_eq!(crt(&[(2, 3), (4, 5)]), 14); // 14 ≡ 2 mod 3, ≡ 4 mod 5
776        assert_eq!(crt(&[(0, 2), (0, 3), (0, 5)]), 0);
777        // CRT over PRIME POWERS (coprime), the new closure.
778        assert_eq!(crt(&[(3, 4), (2, 9)]), 11); // 11 ≡ 3 mod 4, ≡ 2 mod 9
779    }
780
781    /// **The residue-ring solver, verified to the point of absurdity against brute force.** Over genuine
782    /// rings `ℤ/pᵏ` (`k ≥ 2`, where `p` is a zero divisor and field Gaussian does not apply), on a fuzz
783    /// of random systems the integer-Smith solver always matches exhaustive search: every `Sat` model
784    /// satisfies mod `pᵏ`, every `Unsat` witness re-checks as a refutation over `ℤ/pᵏ`.
785    #[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    /// **The full closure over every composite.** Squarefree, prime-power, and mixed moduli alike — the
823    /// general `solve` (CRT over the prime-power components) matches brute force on a fuzz of systems.
824    #[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    /// **The prime-power obstruction needs the RING, not the field.** `2x ≡ 1 (mod 4)` is unsatisfiable —
860    /// the left side is always even — yet this is a `ℤ/4` fact: the witness `2·(2x − 1) = 4x − 2 ≡ −2 ≢ 0
861    /// (mod 4)` annihilates the coefficient while leaving a nonzero residue, the residue-ring refutation.
862    /// `2x ≡ 2 (mod 4)` is satisfiable (`x = 1`). Field reasoning over GF(2) cannot tell these apart
863    /// (both reduce to `0 ≡ ·`); the local ring decides.
864    #[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}