1use std::collections::HashSet;
15
16use crate::cdcl::Lit;
17use crate::modp::{self, ModpEquation};
18
19#[derive(Clone, PartialEq, Eq, Debug)]
22pub struct AffineP {
23 pub p: u64,
24 pub n: usize,
25 pub matrix: Vec<Vec<u64>>,
26 pub translation: Vec<u64>,
27}
28
29impl AffineP {
30 pub fn identity(n: usize, p: u64) -> Self {
32 let matrix = (0..n).map(|i| (0..n).map(|j| u64::from(i == j)).collect()).collect();
33 AffineP { p, n, matrix, translation: vec![0; n] }
34 }
35
36 pub fn apply(&self, x: &[u64]) -> Vec<u64> {
38 (0..self.n)
39 .map(|i| {
40 let s = (0..self.n).fold(0u64, |a, j| (a + self.matrix[i][j] * x[j]) % self.p);
41 (s + self.translation[i]) % self.p
42 })
43 .collect()
44 }
45
46 pub fn compose(&self, other: &AffineP) -> AffineP {
48 let (p, n) = (self.p, self.n);
49 let mut matrix = vec![vec![0u64; n]; n];
50 for (i, row) in matrix.iter_mut().enumerate() {
51 for (j, cell) in row.iter_mut().enumerate() {
52 *cell = (0..n).fold(0u64, |a, k| (a + self.matrix[i][k] * other.matrix[k][j]) % p);
53 }
54 }
55 let translation = (0..n)
56 .map(|i| {
57 let av = (0..n).fold(0u64, |a, k| (a + self.matrix[i][k] * other.translation[k]) % p);
58 (av + self.translation[i]) % p
59 })
60 .collect();
61 AffineP { p, n, matrix, translation }
62 }
63
64 pub fn is_bijection(&self) -> bool {
66 modp::is_invertible_modp(self.n, self.p, &self.matrix)
67 }
68
69 pub fn is_monomial(&self) -> bool {
72 let n = self.n;
73 let rows = self.matrix.iter().all(|r| r.iter().filter(|&&x| x != 0).count() == 1);
74 let cols = (0..n).all(|j| (0..n).filter(|&i| self.matrix[i][j] != 0).count() == 1);
75 rows && cols
76 }
77}
78
79pub fn agl_p_order(n: u32, p: u64) -> u128 {
81 (p as u128).pow(n) * modp::gl_order_p(n, p)
82}
83
84pub fn all_affine_p_bijections(n: usize, p: u64) -> Vec<AffineP> {
87 assert!((p as u128).pow((n * n) as u32) <= 200_000, "exhaustive AGL(n,p) enumeration is bounded (p^{{n²}} ≤ 200k)");
88 let decode = |mut code: u64, len: usize| -> Vec<u64> {
89 (0..len)
90 .map(|_| {
91 let d = code % p;
92 code /= p;
93 d
94 })
95 .collect()
96 };
97 let mut out = Vec::new();
98 let matrices = (p).pow((n * n) as u32);
99 let translations = (p).pow(n as u32);
100 for code in 0..matrices {
101 let flat = decode(code, n * n);
102 let matrix: Vec<Vec<u64>> = (0..n).map(|i| flat[i * n..(i + 1) * n].to_vec()).collect();
103 if !modp::is_invertible_modp(n, p, &matrix) {
104 continue;
105 }
106 for tcode in 0..translations {
107 out.push(AffineP { p, n, matrix: matrix.clone(), translation: decode(tcode, n) });
108 }
109 }
110 out
111}
112
113pub fn models_p(n: usize, p: u64, equations: &[ModpEquation]) -> Vec<Vec<u64>> {
116 assert!((p as u128).pow(n as u32) <= 60_000, "model enumeration is brute force — small n");
117 let total = p.pow(n as u32);
118 (0..total)
119 .filter_map(|code| {
120 let mut c = code;
121 let x: Vec<u64> = (0..n)
122 .map(|_| {
123 let d = c % p;
124 c /= p;
125 d
126 })
127 .collect();
128 equations
129 .iter()
130 .all(|eq| eq.coeffs.iter().fold(0u64, |a, &(v, co)| (a + co * x[v]) % p) % p == eq.rhs % p)
131 .then_some(x)
132 })
133 .collect()
134}
135
136pub fn affine_p_symmetries(n: usize, p: u64, models: &[Vec<u64>]) -> Vec<AffineP> {
140 let set: HashSet<Vec<u64>> = models.iter().cloned().collect();
141 all_affine_p_bijections(n, p)
142 .into_iter()
143 .filter(|phi| set.iter().all(|m| set.contains(&phi.apply(m))))
144 .collect()
145}
146
147pub fn affine_p_refutation_drat(num_bool_vars: usize, clauses: &[Vec<Lit>]) -> Option<Vec<Vec<Lit>>> {
154 crate::xor_drat::emit_modp_drat(num_bool_vars, clauses)
155}
156
157pub enum AffinePForced {
159 Refuted(Option<Vec<Vec<Lit>>>),
161 Forced(Vec<Vec<Lit>>),
163 Unchanged,
165}
166
167fn gfp_mul(a: u64, b: u64, p: u64) -> u64 {
169 (a % p) * (b % p) % p
170}
171fn gfp_sub(a: u64, b: u64, p: u64) -> u64 {
172 (a % p + p - b % p) % p
173}
174fn gfp_inv(a: u64, p: u64) -> u64 {
175 let (mut result, mut base, mut exp) = (1u64, a % p, p - 2);
176 while exp > 0 {
177 if exp & 1 == 1 {
178 result = gfp_mul(result, base, p);
179 }
180 base = gfp_mul(base, base, p);
181 exp >>= 1;
182 }
183 result
184}
185
186pub fn affine_p_forced(num_bool_vars: usize, clauses: &[Vec<Lit>]) -> AffinePForced {
201 let Some(rec) = modp::recover_from_cnf(num_bool_vars, clauses) else {
202 return AffinePForced::Unchanged;
203 };
204 if !modp::is_prime(rec.modulus) {
205 return AffinePForced::Unchanged; }
207 let Some(ss) = modp::solve_space(&rec.equations, rec.num_vars, rec.modulus) else {
208 return AffinePForced::Refuted(crate::xor_drat::emit_modp_drat(num_bool_vars, clauses));
209 };
210 let p = rec.modulus;
211 let key = |c: &[Lit]| -> Vec<(u32, bool)> {
212 let mut k: Vec<(u32, bool)> = c.iter().map(|l| (l.var(), l.is_positive())).collect();
213 k.sort_unstable();
214 k
215 };
216 let existing: HashSet<Vec<(u32, bool)>> = clauses.iter().map(|c| key(c)).collect();
217 let mut out: Vec<Vec<Lit>> = Vec::new();
218 let mut push_new = |c: Vec<Lit>| {
219 if !existing.contains(&key(&c)) {
220 out.push(c);
221 }
222 };
223 let col = |g: usize| -> Vec<u64> { ss.kernel_basis.iter().map(|kv| kv[g]).collect() };
225
226 let mut classes: std::collections::HashMap<Vec<u64>, Vec<usize>> = std::collections::HashMap::new();
229 for g in 0..rec.num_vars {
230 let c = col(g);
231 if c.iter().all(|&x| x == 0) {
232 let val = ss.particular[g];
233 for (v, &bvar) in rec.groups[g].iter().enumerate() {
234 push_new(vec![if v as u64 == val { Lit::pos(bvar) } else { Lit::neg(bvar) }]);
235 }
236 } else {
237 let scale = gfp_inv(*c.iter().find(|&&x| x != 0).unwrap(), p); classes.entry(c.iter().map(|&x| gfp_mul(x, scale, p)).collect()).or_default().push(g);
239 }
240 }
241 let mut class_list: Vec<Vec<usize>> = classes
242 .into_values()
243 .map(|mut v| {
244 v.sort_unstable();
245 v
246 })
247 .collect();
248 class_list.sort_unstable_by_key(|c| c[0]);
249 for members in &class_list {
250 if members.len() < 2 {
251 continue; }
253 let rep = members[0];
254 let s_rep = *col(rep).iter().find(|&&x| x != 0).unwrap();
255 for &g in &members[1..] {
256 let s_g = *col(g).iter().find(|&&x| x != 0).unwrap();
257 let c_g = gfp_mul(s_g, gfp_inv(s_rep, p), p); let d_g = gfp_sub(ss.particular[g], gfp_mul(c_g, ss.particular[rep], p), p);
259 let inv_c = gfp_inv(c_g, p);
260 for v in 0..p {
261 let sv = gfp_mul(gfp_sub(v, d_g, p), inv_c, p); let (bg, br) = (rec.groups[g][v as usize], rec.groups[rep][sv as usize]);
263 push_new(vec![Lit::neg(bg), Lit::pos(br)]);
264 push_new(vec![Lit::pos(bg), Lit::neg(br)]);
265 }
266 }
267 }
268
269 if out.is_empty() {
270 AffinePForced::Unchanged
271 } else {
272 AffinePForced::Forced(out)
273 }
274}
275
276pub fn agl_m_order(n: u32, m: u64) -> Option<u128> {
279 Some(crate::modm::squarefree_primes(m)?.iter().map(|&p| agl_p_order(n, p)).product())
280}
281
282fn crt_combine(residues: &[(u64, u64)]) -> (u64, u64) {
284 let (mut acc_r, mut acc_m): (i128, i128) = (0, 1);
285 for &(r, modu) in residues {
286 let modu = modu as i128;
287 let diff = (r as i128 - acc_r).rem_euclid(modu);
288 let t = (diff * mod_inverse(acc_m.rem_euclid(modu) as u64, modu as u64) as i128).rem_euclid(modu);
289 acc_r += acc_m * t;
290 acc_m *= modu;
291 acc_r = acc_r.rem_euclid(acc_m);
292 }
293 (acc_r as u64, acc_m as u64)
294}
295
296fn mod_inverse(a: u64, m: u64) -> u64 {
298 let (mut old_r, mut r): (i128, i128) = (a as i128, m as i128);
299 let (mut old_s, mut s): (i128, i128) = (1, 0);
300 while r != 0 {
301 let q = old_r / r;
302 (old_r, r) = (r, old_r - q * r);
303 (old_s, s) = (s, old_s - q * s);
304 }
305 old_s.rem_euclid(m as i128) as u64
306}
307
308pub fn affine_m_forced(num_bool_vars: usize, clauses: &[Vec<Lit>]) -> AffinePForced {
322 let Some(rec) = modp::recover_from_cnf(num_bool_vars, clauses) else {
323 return AffinePForced::Unchanged;
324 };
325 let m = rec.modulus;
326 if modp::is_prime(m) {
327 return AffinePForced::Unchanged; }
329 let Some(primes) = crate::modm::squarefree_primes(m) else {
330 return prime_power_forced(num_bool_vars, &rec, clauses); };
332 let mut spaces: Vec<crate::modp::SolutionSpaceP> = Vec::with_capacity(primes.len());
333 for &p in &primes {
334 let Some(ss) = modp::solve_space(&rec.equations, rec.num_vars, p) else {
335 return AffinePForced::Refuted(crate::xor_drat::emit_modp_drat(num_bool_vars, clauses));
336 };
337 spaces.push(ss);
338 }
339
340 let key = |c: &[Lit]| -> Vec<(u32, bool)> {
341 let mut k: Vec<(u32, bool)> = c.iter().map(|l| (l.var(), l.is_positive())).collect();
342 k.sort_unstable();
343 k
344 };
345 let existing: HashSet<Vec<(u32, bool)>> = clauses.iter().map(|c| key(c)).collect();
346 let mut out: Vec<Vec<Lit>> = Vec::new();
347 let push_new = |c: Vec<Lit>, out: &mut Vec<Vec<Lit>>| {
348 if !existing.contains(&key(&c)) {
349 out.push(c);
350 }
351 };
352 let col = |i: usize, g: usize| -> Vec<u64> { spaces[i].kernel_basis.iter().map(|kk| kk[g]).collect() };
353 let forced_res = |i: usize, g: usize| -> Option<u64> {
354 col(i, g).iter().all(|&x| x == 0).then(|| spaces[i].particular[g])
355 };
356
357 for g in 0..rec.num_vars {
359 let constraints: Vec<(u64, u64)> =
360 primes.iter().enumerate().filter_map(|(i, &p)| forced_res(i, g).map(|v| (v, p))).collect();
361 if constraints.is_empty() {
362 continue;
363 }
364 let (res, q) = crt_combine(&constraints);
365 let fully = constraints.len() == primes.len();
366 for (v, &bvar) in rec.groups[g].iter().enumerate() {
367 if (v as u64) % q != res {
368 push_new(vec![Lit::neg(bvar)], &mut out); } else if fully {
370 push_new(vec![Lit::pos(bvar)], &mut out); }
372 }
373 }
374
375 let mut classes: std::collections::HashMap<Vec<Vec<u64>>, Vec<usize>> = std::collections::HashMap::new();
377 for g in 0..rec.num_vars {
378 if (0..primes.len()).any(|i| forced_res(i, g).is_some()) {
379 continue;
380 }
381 let sig: Vec<Vec<u64>> = (0..primes.len())
382 .map(|i| {
383 let (p, c) = (primes[i], col(i, g));
384 let s = gfp_inv(*c.iter().find(|&&x| x != 0).unwrap(), p);
385 c.iter().map(|&x| gfp_mul(x, s, p)).collect()
386 })
387 .collect();
388 classes.entry(sig).or_default().push(g);
389 }
390 let mut class_list: Vec<Vec<usize>> = classes
391 .into_values()
392 .map(|mut v| {
393 v.sort_unstable();
394 v
395 })
396 .collect();
397 class_list.sort_unstable_by_key(|c| c[0]);
398 for members in &class_list {
399 if members.len() < 2 {
400 continue;
401 }
402 let rep = members[0];
403 for &g in &members[1..] {
404 let (mut cs, mut ds): (Vec<(u64, u64)>, Vec<(u64, u64)>) = (Vec::new(), Vec::new());
405 for (i, &p) in primes.iter().enumerate() {
406 let s_rep = *col(i, rep).iter().find(|&&x| x != 0).unwrap();
407 let s_g = *col(i, g).iter().find(|&&x| x != 0).unwrap();
408 let c_i = gfp_mul(s_g, gfp_inv(s_rep, p), p);
409 let d_i = gfp_sub(spaces[i].particular[g], gfp_mul(c_i, spaces[i].particular[rep], p), p);
410 cs.push((c_i, p));
411 ds.push((d_i, p));
412 }
413 let (c, _) = crt_combine(&cs);
414 let (d, _) = crt_combine(&ds);
415 let inv_c = mod_inverse(c, m);
416 for v in 0..m {
417 let sv = ((v + m - d) % m) * inv_c % m; let (bg, br) = (rec.groups[g][v as usize], rec.groups[rep][sv as usize]);
419 push_new(vec![Lit::neg(bg), Lit::pos(br)], &mut out);
420 push_new(vec![Lit::pos(bg), Lit::neg(br)], &mut out);
421 }
422 }
423 }
424
425 if out.is_empty() {
426 AffinePForced::Unchanged
427 } else {
428 AffinePForced::Forced(out)
429 }
430}
431
432fn gcd_u64(mut a: u64, mut b: u64) -> u64 {
434 while b != 0 {
435 (a, b) = (b, a % b);
436 }
437 a
438}
439
440fn cg_mul(a: u64, b: u64, q: u64) -> u64 {
442 a % q * (b % q) % q
443}
444
445fn kcol(ss: &crate::modm::SolutionSpaceM, g: usize) -> Vec<u64> {
447 ss.kernel_basis.iter().map(|kk| kk[g]).collect()
448}
449
450struct BreakStructure {
455 residue: Vec<(u64, u64)>,
456 link: Vec<Option<(usize, u64, u64)>>,
457}
458
459enum BreakOutcome {
460 Inconsistent,
461 Structure(BreakStructure),
462}
463
464fn composite_break_structure(eqs: &[ModpEquation], num_vars: usize, m: u64) -> Option<BreakOutcome> {
466 use crate::modm::PrimePowerSpace;
467 let factors = crate::modm::prime_power_factorize(m)?;
468 let mut spaces: Vec<(u64, u64, crate::modm::SolutionSpaceM)> = Vec::with_capacity(factors.len());
469 for (p, k) in factors {
470 match crate::modm::solve_space_prime_power(eqs, num_vars, p, k) {
471 None => return None,
472 Some(PrimePowerSpace::Inconsistent) => return Some(BreakOutcome::Inconsistent),
473 Some(PrimePowerSpace::Space(ss)) => spaces.push((p, p.pow(k), ss)),
474 }
475 }
476
477 let residue: Vec<(u64, u64)> = (0..num_vars)
479 .map(|g| {
480 let pairs: Vec<(u64, u64)> = spaces
481 .iter()
482 .filter_map(|(_, q, ss)| {
483 let d = kcol(ss, g).iter().fold(*q, |acc, &x| gcd_u64(acc, x));
484 (d > 1).then(|| (ss.particular[g] % d, d))
485 })
486 .collect();
487 if pairs.is_empty() {
488 (0, 1)
489 } else {
490 (crt_combine(&pairs).0, pairs.iter().map(|&(_, d)| d).product())
491 }
492 })
493 .collect();
494
495 let mut classes: std::collections::HashMap<Vec<Vec<u64>>, Vec<usize>> = std::collections::HashMap::new();
497 'g: for g in 0..num_vars {
498 let mut sig: Vec<Vec<u64>> = Vec::with_capacity(spaces.len());
499 for (p, q, ss) in &spaces {
500 let c = kcol(ss, g);
501 let Some(piv) = c.iter().position(|&x| x % p != 0) else {
502 continue 'g;
503 };
504 let inv = mod_inverse(c[piv], *q);
505 sig.push(c.iter().map(|&x| x * inv % q).collect());
506 }
507 classes.entry(sig).or_default().push(g);
508 }
509 let mut link = vec![None; num_vars];
510 let mut class_list: Vec<Vec<usize>> = classes.into_values().map(|mut v| { v.sort_unstable(); v }).collect();
511 class_list.sort_unstable_by_key(|c| c[0]);
512 for members in &class_list {
513 if members.len() < 2 {
514 continue;
515 }
516 let rep = members[0];
517 for &g in &members[1..] {
518 let (mut cs, mut ds): (Vec<(u64, u64)>, Vec<(u64, u64)>) = (Vec::new(), Vec::new());
519 for (p, q, ss) in &spaces {
520 let (cg, cr) = (kcol(ss, g), kcol(ss, rep));
521 let piv = cr.iter().position(|&x| x % p != 0).unwrap();
522 let c_c = cg[piv] * mod_inverse(cr[piv], *q) % q;
523 let d_c = (ss.particular[g] + q - cg_mul(c_c, ss.particular[rep], *q)) % q;
524 cs.push((c_c, *q));
525 ds.push((d_c, *q));
526 }
527 link[g] = Some((rep, crt_combine(&cs).0, crt_combine(&ds).0));
528 }
529 }
530
531 Some(BreakOutcome::Structure(BreakStructure { residue, link }))
532}
533
534fn prime_power_forced(num_bool_vars: usize, rec: &modp::ModpRecovery, clauses: &[Vec<Lit>]) -> AffinePForced {
539 let m = rec.modulus;
540 let st = match composite_break_structure(&rec.equations, rec.num_vars, m) {
541 None => return AffinePForced::Unchanged,
542 Some(BreakOutcome::Inconsistent) => {
543 return AffinePForced::Refuted(crate::xor_drat::emit_modp_drat(num_bool_vars, clauses));
544 }
545 Some(BreakOutcome::Structure(s)) => s,
546 };
547 let key = |c: &[Lit]| -> Vec<(u32, bool)> {
548 let mut k: Vec<(u32, bool)> = c.iter().map(|l| (l.var(), l.is_positive())).collect();
549 k.sort_unstable();
550 k
551 };
552 let existing: HashSet<Vec<(u32, bool)>> = clauses.iter().map(|c| key(c)).collect();
553 let mut out: Vec<Vec<Lit>> = Vec::new();
554 let push_new = |c: Vec<Lit>, out: &mut Vec<Vec<Lit>>| {
555 if !existing.contains(&key(&c)) {
556 out.push(c);
557 }
558 };
559 for (g, &(res, modu)) in st.residue.iter().enumerate() {
560 if modu <= 1 {
561 continue;
562 }
563 for (v, &bvar) in rec.groups[g].iter().enumerate() {
564 if (v as u64) % modu != res {
565 push_new(vec![Lit::neg(bvar)], &mut out);
566 } else if modu == m {
567 push_new(vec![Lit::pos(bvar)], &mut out);
568 }
569 }
570 }
571 for g in 0..rec.num_vars {
572 if let Some((rep, c, d)) = st.link[g] {
573 let inv_c = mod_inverse(c, m);
574 for v in 0..m {
575 let sv = (v + m - d) % m * inv_c % m;
576 let (bg, br) = (rec.groups[g][v as usize], rec.groups[rep][sv as usize]);
577 push_new(vec![Lit::neg(bg), Lit::pos(br)], &mut out);
578 push_new(vec![Lit::pos(bg), Lit::neg(br)], &mut out);
579 }
580 }
581 }
582 if out.is_empty() {
583 AffinePForced::Unchanged
584 } else {
585 AffinePForced::Forced(out)
586 }
587}
588
589#[derive(Clone, Debug)]
591enum BoolSub {
592 Const(bool),
594 Survive(u32),
596 Alias(u32),
599}
600
601#[derive(Clone, Debug)]
603pub struct AffinePCanonical {
604 pub num_vars: usize,
605 pub clauses: Vec<Vec<Lit>>,
606 sub: Vec<BoolSub>,
607}
608
609impl AffinePCanonical {
610 pub fn lift(&self, reduced_model: &[bool]) -> Vec<bool> {
612 self.sub
613 .iter()
614 .map(|s| match *s {
615 BoolSub::Const(c) => c,
616 BoolSub::Survive(ni) | BoolSub::Alias(ni) => reduced_model[ni as usize],
617 })
618 .collect()
619 }
620}
621
622pub enum AffinePCanon {
624 Refuted(Option<Vec<Vec<Lit>>>),
625 Canonical(AffinePCanonical),
626 Unchanged,
627}
628
629pub fn affine_p_canonicalize(num_bool_vars: usize, clauses: &[Vec<Lit>]) -> AffinePCanon {
637 let Some(rec) = modp::recover_from_cnf(num_bool_vars, clauses) else {
638 return AffinePCanon::Unchanged;
639 };
640 let p = rec.modulus;
641 if !modp::is_prime(p) {
642 return AffinePCanon::Unchanged;
643 }
644 let Some(ss) = modp::solve_space(&rec.equations, rec.num_vars, p) else {
645 return AffinePCanon::Refuted(crate::xor_drat::emit_modp_drat(num_bool_vars, clauses));
646 };
647
648 enum Role {
650 Forced(u64),
651 Survive,
652 Linked { rep: usize, c: u64, d: u64 },
653 }
654 let col = |g: usize| -> Vec<u64> { ss.kernel_basis.iter().map(|k| k[g]).collect() };
655 let mut role: Vec<Role> = Vec::with_capacity(rec.num_vars);
656 let mut classes: std::collections::HashMap<Vec<u64>, Vec<usize>> = std::collections::HashMap::new();
657 for g in 0..rec.num_vars {
658 let c = col(g);
659 if c.iter().all(|&x| x == 0) {
660 role.push(Role::Forced(ss.particular[g]));
661 } else {
662 let s = gfp_inv(*c.iter().find(|&&x| x != 0).unwrap(), p);
663 classes.entry(c.iter().map(|&x| gfp_mul(x, s, p)).collect()).or_default().push(g);
664 role.push(Role::Survive); }
666 }
667 for members in classes.values() {
668 let mut ms = members.clone();
669 ms.sort_unstable();
670 let rep = ms[0];
671 let s_rep = *col(rep).iter().find(|&&x| x != 0).unwrap();
672 for &g in &ms[1..] {
673 let s_g = *col(g).iter().find(|&&x| x != 0).unwrap();
674 let c = gfp_mul(s_g, gfp_inv(s_rep, p), p);
675 let d = gfp_sub(ss.particular[g], gfp_mul(c, ss.particular[rep], p), p);
676 role[g] = Role::Linked { rep, c, d };
677 }
678 }
679
680 let mut in_group = vec![false; num_bool_vars];
682 for grp in &rec.groups {
683 for &b in grp {
684 in_group[b as usize] = true;
685 }
686 }
687 let mut new_idx: Vec<Option<u32>> = vec![None; num_bool_vars];
688 let mut next = 0u32;
689 for g in 0..rec.num_vars {
690 if matches!(role[g], Role::Survive) {
691 for &b in &rec.groups[g] {
692 new_idx[b as usize] = Some(next);
693 next += 1;
694 }
695 }
696 }
697 for (b, slot) in new_idx.iter_mut().enumerate() {
698 if !in_group[b] {
699 *slot = Some(next);
700 next += 1;
701 }
702 }
703 let reduced_nv = next as usize;
704
705 let mut sub = vec![BoolSub::Const(false); num_bool_vars];
707 for (b, slot) in new_idx.iter().enumerate() {
708 if let Some(ni) = slot {
709 sub[b] = BoolSub::Survive(*ni); }
711 }
712 for g in 0..rec.num_vars {
713 match role[g] {
714 Role::Forced(val) => {
715 for (v, &b) in rec.groups[g].iter().enumerate() {
716 sub[b as usize] = BoolSub::Const(v as u64 == val);
717 }
718 }
719 Role::Survive => {} Role::Linked { rep, c, d } => {
721 let inv_c = gfp_inv(c, p);
722 for (v, &b) in rec.groups[g].iter().enumerate() {
723 let sv = gfp_mul(gfp_sub(v as u64, d, p), inv_c, p); let rep_b = rec.groups[rep][sv as usize];
725 sub[b as usize] = BoolSub::Alias(new_idx[rep_b as usize].unwrap());
726 }
727 }
728 }
729 }
730 if !sub.iter().any(|s| matches!(s, BoolSub::Const(_) | BoolSub::Alias(_))) {
731 return AffinePCanon::Unchanged; }
733
734 let mut out: Vec<Vec<Lit>> = Vec::new();
736 'clause: for c in clauses {
737 let mut seen: std::collections::HashMap<u32, bool> = std::collections::HashMap::new();
738 let mut lits: Vec<Lit> = Vec::new();
739 for l in c {
740 let (ni, pol) = match sub[l.var() as usize] {
741 BoolSub::Const(cst) => {
742 if cst == l.is_positive() {
743 continue 'clause; }
745 continue; }
747 BoolSub::Survive(ni) | BoolSub::Alias(ni) => (ni, l.is_positive()),
748 };
749 match seen.get(&ni) {
750 Some(&prev) if prev != pol => continue 'clause, Some(_) => continue, None => {
753 seen.insert(ni, pol);
754 lits.push(Lit::new(ni, pol));
755 }
756 }
757 }
758 out.push(lits);
759 }
760 AffinePCanon::Canonical(AffinePCanonical { num_vars: reduced_nv, clauses: out, sub })
761}
762
763pub fn affine_m_canonicalize(num_bool_vars: usize, clauses: &[Vec<Lit>]) -> AffinePCanon {
773 let Some(rec) = modp::recover_from_cnf(num_bool_vars, clauses) else {
774 return AffinePCanon::Unchanged;
775 };
776 let m = rec.modulus;
777 if modp::is_prime(m) {
778 return AffinePCanon::Unchanged; }
780 let st = match composite_break_structure(&rec.equations, rec.num_vars, m) {
781 None => return AffinePCanon::Unchanged,
782 Some(BreakOutcome::Inconsistent) => {
783 return AffinePCanon::Refuted(crate::xor_drat::emit_modp_drat(num_bool_vars, clauses));
784 }
785 Some(BreakOutcome::Structure(s)) => s,
786 };
787
788 enum Role {
789 Forced(u64),
790 Partial { res: u64, modu: u64 },
791 Survive,
792 Linked { rep: usize, c: u64, d: u64 },
793 }
794 let role: Vec<Role> = (0..rec.num_vars)
795 .map(|g| {
796 let (res, modu) = st.residue[g];
797 if modu == m {
798 Role::Forced(res)
799 } else if modu > 1 {
800 Role::Partial { res, modu }
801 } else if let Some((rep, c, d)) = st.link[g] {
802 Role::Linked { rep, c, d }
803 } else {
804 Role::Survive
805 }
806 })
807 .collect();
808
809 let mut in_group = vec![false; num_bool_vars];
812 for grp in &rec.groups {
813 for &b in grp {
814 in_group[b as usize] = true;
815 }
816 }
817 let mut new_idx: Vec<Option<u32>> = vec![None; num_bool_vars];
818 let mut next = 0u32;
819 for g in 0..rec.num_vars {
820 match role[g] {
821 Role::Survive => {
822 for &b in &rec.groups[g] {
823 new_idx[b as usize] = Some(next);
824 next += 1;
825 }
826 }
827 Role::Partial { res, modu } => {
828 for (v, &b) in rec.groups[g].iter().enumerate() {
829 if (v as u64) % modu == res {
830 new_idx[b as usize] = Some(next);
831 next += 1;
832 }
833 }
834 }
835 _ => {}
836 }
837 }
838 for (b, slot) in new_idx.iter_mut().enumerate() {
839 if !in_group[b] {
840 *slot = Some(next);
841 next += 1;
842 }
843 }
844 let reduced_nv = next as usize;
845
846 let mut sub = vec![BoolSub::Const(false); num_bool_vars];
849 for (b, slot) in new_idx.iter().enumerate() {
850 if let Some(ni) = slot {
851 sub[b] = BoolSub::Survive(*ni);
852 }
853 }
854 for g in 0..rec.num_vars {
855 match role[g] {
856 Role::Forced(val) => {
857 for (v, &b) in rec.groups[g].iter().enumerate() {
858 sub[b as usize] = BoolSub::Const(v as u64 == val);
859 }
860 }
861 Role::Partial { .. } | Role::Survive => {} Role::Linked { rep, c, d } => {
863 let inv_c = mod_inverse(c, m);
864 for (v, &b) in rec.groups[g].iter().enumerate() {
865 let sv = (v as u64 + m - d) % m * inv_c % m; let rep_b = rec.groups[rep][sv as usize];
867 sub[b as usize] = BoolSub::Alias(new_idx[rep_b as usize].unwrap());
868 }
869 }
870 }
871 }
872 if !sub.iter().any(|s| matches!(s, BoolSub::Const(_) | BoolSub::Alias(_))) {
873 return AffinePCanon::Unchanged; }
875
876 let mut out: Vec<Vec<Lit>> = Vec::new();
878 'clause: for c in clauses {
879 let mut seen: std::collections::HashMap<u32, bool> = std::collections::HashMap::new();
880 let mut lits: Vec<Lit> = Vec::new();
881 for l in c {
882 let (ni, pol) = match sub[l.var() as usize] {
883 BoolSub::Const(cst) => {
884 if cst == l.is_positive() {
885 continue 'clause; }
887 continue; }
889 BoolSub::Survive(ni) | BoolSub::Alias(ni) => (ni, l.is_positive()),
890 };
891 match seen.get(&ni) {
892 Some(&prev) if prev != pol => continue 'clause, Some(_) => continue, None => {
895 seen.insert(ni, pol);
896 lits.push(Lit::new(ni, pol));
897 }
898 }
899 }
900 out.push(lits);
901 }
902 AffinePCanon::Canonical(AffinePCanonical { num_vars: reduced_nv, clauses: out, sub })
903}
904
905#[cfg(test)]
906mod tests {
907 use super::*;
908 use crate::families;
909
910 #[test]
912 fn agl_p_order_matches_the_enumeration() {
913 for (n, p) in [(1usize, 2u64), (1, 3), (2, 2), (2, 3), (2, 5), (3, 2), (3, 3)] {
914 assert_eq!(
915 all_affine_p_bijections(n, p).len() as u128,
916 agl_p_order(n as u32, p),
917 "n={n}, p={p}: enumerated affine bijections must equal |AGL(n,p)| = pⁿ·|GL(n,p)|"
918 );
919 }
920 assert_eq!(agl_p_order(2, 3), 432);
922 assert_eq!(agl_p_order(3, 2), 1344);
923 }
924
925 #[test]
927 fn affine_p_maps_compose_and_act_correctly() {
928 let p = 3u64;
929 let id = AffineP::identity(2, p);
930 for phi in all_affine_p_bijections(2, p) {
931 assert!(phi.is_bijection());
932 assert_eq!(phi.compose(&id), phi);
933 assert_eq!(id.compose(&phi), phi);
934 let sq = phi.compose(&phi);
935 for a in 0..p {
936 for b in 0..p {
937 let x = vec![a, b];
938 assert_eq!(sq.apply(&x), phi.apply(&phi.apply(&x)), "composition must match double application");
939 }
940 }
941 }
942 }
943
944 #[test]
950 fn gfp_affine_symmetry_strictly_exceeds_monomial_on_mod_p_parity() {
951 let eq = ModpEquation::new(vec![(0usize, 1u64), (1, 1), (2, 1)], 0); let models = models_p(3, 3, std::slice::from_ref(&eq));
953 assert_eq!(models.len(), 9, "the mod-3 hyperplane has 3² = 9 points");
954
955 let sym = affine_p_symmetries(3, 3, &models);
956 assert_eq!(sym.len(), 7776, "AGL(3,3) stabilizer of the mod-3 hyperplane = 864 linear × 9 translations");
957
958 let monomial = sym.iter().filter(|a| a.is_monomial()).count();
959 assert!(sym.len() > monomial, "affine symmetry ({}) must strictly exceed the monomial part ({monomial})", sym.len());
960 assert!(
961 sym.iter().any(|a| !a.is_monomial()),
962 "a non-monomial GF(3) affine symmetry (a shear) must exist — invisible to the monomial breakers"
963 );
964 }
965
966 #[test]
971 fn gfp_affine_refutation_is_certified_via_modp_drat_bridge() {
972 for (n, p) in [(4usize, 3u64), (6, 3), (4, 5)] {
973 let (_, cnf, _) = families::mod_p_tseitin_expander(n, p, 1);
974 let Some(proof) = affine_p_refutation_drat(cnf.num_vars, &cnf.clauses) else {
975 eprintln!("[modp n={n} p={p}] resolution route over budget — skipped");
976 continue;
977 };
978 assert!(proof.last().is_some_and(|c| c.is_empty()), "proof ends in the empty clause (n={n}, p={p})");
979 assert!(
980 crate::rup::check_refutation(cnf.num_vars, &cnf.clauses, &proof),
981 "the GF({p}) affine refutation must RUP-refute the original CNF (n={n})"
982 );
983 }
984 }
985
986 #[test]
990 fn solve_space_matches_brute_force() {
991 use crate::modp::solve_space;
992 let mut state = 0xC0FFEE_77u64;
993 let mut rng = || {
994 state ^= state << 13;
995 state ^= state >> 7;
996 state ^= state << 17;
997 state
998 };
999 for &p in &[3u64, 5] {
1000 for _ in 0..200 {
1001 let n = 2 + (rng() % 3) as usize; let m = 1 + (rng() % 4) as usize;
1003 let eqs: Vec<ModpEquation> = (0..m)
1004 .map(|_| {
1005 let k = 1 + (rng() % n as u64) as usize;
1006 let mut vars: Vec<usize> = Vec::new();
1007 while vars.len() < k {
1008 let v = (rng() % n as u64) as usize;
1009 if !vars.contains(&v) {
1010 vars.push(v);
1011 }
1012 }
1013 let coeffs: Vec<(usize, u64)> = vars.iter().map(|&v| (v, 1 + rng() % (p - 1))).collect();
1014 ModpEquation::new(coeffs, rng() % p)
1015 })
1016 .collect();
1017 let brute: HashSet<Vec<u64>> = models_p(n, p, &eqs).into_iter().collect();
1018 match solve_space(&eqs, n, p) {
1019 None => assert!(brute.is_empty(), "None ⇒ no GF({p}) solutions"),
1020 Some(ss) => {
1021 assert_eq!(ss.count() as usize, brute.len(), "count must equal the brute-force solution count");
1022 let got: HashSet<Vec<u64>> = ss.enumerate().into_iter().collect();
1023 assert_eq!(got, brute, "particular + kernel must enumerate exactly the GF({p}) solutions");
1024 }
1025 }
1026 }
1027 }
1028 }
1029
1030 fn onehot_cnf(p: u64, num_edges: usize, equations: &[ModpEquation]) -> (usize, Vec<Vec<Lit>>) {
1033 let bvar = |e: usize, val: u64| (e * p as usize + val as usize) as u32;
1034 let mut clauses: Vec<Vec<Lit>> = Vec::new();
1035 for e in 0..num_edges {
1036 clauses.push((0..p).map(|v| Lit::pos(bvar(e, v))).collect());
1037 for v1 in 0..p {
1038 for v2 in (v1 + 1)..p {
1039 clauses.push(vec![Lit::neg(bvar(e, v1)), Lit::neg(bvar(e, v2))]);
1040 }
1041 }
1042 }
1043 for eq in equations {
1044 let d = eq.coeffs.len();
1045 for idx in 0..p.pow(d as u32) {
1046 let mut x = idx;
1047 let combo: Vec<u64> = (0..d)
1048 .map(|_| {
1049 let v = x % p;
1050 x /= p;
1051 v
1052 })
1053 .collect();
1054 let sum = eq.coeffs.iter().zip(&combo).fold(0u64, |a, (&(_, co), &val)| (a + co * val) % p);
1055 if sum % p != eq.rhs % p {
1056 clauses.push(eq.coeffs.iter().zip(&combo).map(|(&(v, _), &val)| Lit::neg(bvar(v, val))).collect());
1057 }
1058 }
1059 }
1060 (num_edges * p as usize, clauses)
1061 }
1062
1063 #[test]
1067 fn affine_p_forced_pins_determined_edges_and_is_sound() {
1068 let eqs = vec![
1070 ModpEquation::new(vec![(0usize, 1u64), (1, 1)], 0),
1071 ModpEquation::new(vec![(1usize, 1u64), (2, 1)], 0),
1072 ModpEquation::new(vec![(0usize, 1u64), (2, 1)], 2),
1073 ];
1074 let (nbv, clauses) = onehot_cnf(3, 3, &eqs);
1075 match affine_p_forced(nbv, &clauses) {
1076 AffinePForced::Forced(units) => {
1077 for &(e, v) in &[(0usize, 1u64), (1, 2), (2, 1)] {
1079 let bvar = (e * 3 + v as usize) as u32;
1080 assert!(units.contains(&vec![Lit::pos(bvar)]), "must pin b(edge{e}={v}) = var {bvar}; got {units:?}");
1081 }
1082 let models = crate::affine::models_of(nbv, &clauses);
1084 assert!(!models.is_empty(), "the crafted determined system is satisfiable");
1085 for u in &units {
1086 let l = u[0];
1087 assert!(
1088 models.iter().all(|&m| ((m >> l.var()) & 1 == 1) == l.is_positive()),
1089 "forced unit on var {} must hold in every Boolean model",
1090 l.var()
1091 );
1092 }
1093 }
1094 _ => panic!("the determined edges must produce forced one-hot units"),
1095 }
1096 }
1097
1098 #[test]
1103 fn gfp_link_lifts_to_value_permuted_bit_equivalences() {
1104 let p = 3u64;
1105 let eqs = vec![ModpEquation::new(vec![(0usize, 1u64), (1, 1)], 0)];
1106
1107 let ss = crate::modp::solve_space(&eqs, 2, p).expect("consistent system");
1109 assert_eq!(ss.kernel_basis.len(), 1, "one free variable ⇒ a single kernel direction");
1110 let kv = &ss.kernel_basis[0];
1111 assert!(kv[0] != 0 && kv[1] != 0, "neither variable is forced");
1112 assert_eq!(kv[0], (2 * kv[1]) % p, "x0's kernel entry is 2× x1's — the GF(3) scalar link");
1113
1114 let (nbv, clauses) = onehot_cnf(p, 2, &eqs);
1116 let AffinePForced::Forced(extra) = affine_p_forced(nbv, &clauses) else {
1117 panic!("the link must fire as forced consequences");
1118 };
1119 assert!(extra.iter().any(|c| c.len() == 2), "must emit bit-equivalences (2-literal clauses)");
1120 let models = crate::affine::models_of(nbv, &clauses);
1121 for c in &extra {
1122 assert!(
1123 models.iter().all(|&m| c.iter().any(|l| ((m >> l.var()) & 1 == 1) == l.is_positive())),
1124 "every emitted equivalence must hold in every Boolean model"
1125 );
1126 }
1127 let rec = crate::modp::recover_from_cnf(nbv, &clauses).expect("recovers");
1129 let pos_of = |bv: u32| {
1130 rec.groups.iter().enumerate().find_map(|(g, grp)| grp.iter().position(|&b| b == bv).map(|v| (g, v)))
1131 };
1132 let value_permuted = extra.iter().filter(|c| c.len() == 2).any(|c| {
1133 matches!((pos_of(c[0].var()), pos_of(c[1].var())), (Some((g0, v0)), Some((g1, v1))) if g0 != g1 && v0 != v1)
1134 });
1135 assert!(value_permuted, "a genuine GF(3) link must produce a value-permuted bit-equivalence");
1136 }
1137
1138 #[test]
1142 fn affine_p_break_is_sound_against_brute_force() {
1143 let mut state = 0x5A7_AFF1_9E37u64;
1144 let mut rng = || {
1145 state ^= state << 13;
1146 state ^= state >> 7;
1147 state ^= state << 17;
1148 state
1149 };
1150 for &p in &[3u64, 5] {
1151 for _ in 0..200 {
1152 let m = 1 + (rng() % 3) as usize;
1153 let eqs: Vec<ModpEquation> = (0..m)
1154 .map(|_| {
1155 ModpEquation::new(vec![(0usize, 1 + rng() % (p - 1)), (1, 1 + rng() % (p - 1))], rng() % p)
1157 })
1158 .collect();
1159 let (nbv, clauses) = onehot_cnf(p, 2, &eqs);
1160 let models = crate::affine::models_of(nbv, &clauses);
1161 match affine_p_forced(nbv, &clauses) {
1162 AffinePForced::Refuted(_) => assert!(models.is_empty(), "Refuted ⇒ the Boolean CNF must be UNSAT"),
1163 AffinePForced::Forced(extra) => {
1164 for c in &extra {
1165 assert!(
1166 models.iter().all(|&m| c.iter().any(|l| ((m >> l.var()) & 1 == 1) == l.is_positive())),
1167 "every forced/linked consequence must hold in every Boolean model"
1168 );
1169 }
1170 }
1171 AffinePForced::Unchanged => {}
1172 }
1173 }
1174 }
1175 }
1176
1177 #[test]
1180 fn agl_m_order_factors_by_crt() {
1181 assert_eq!(agl_m_order(2, 6), Some(24 * 432));
1182 assert_eq!(agl_m_order(2, 30), Some(24 * 432 * 12000)); assert_eq!(agl_m_order(2, 4), None, "4 is not squarefree");
1184 assert_eq!(agl_m_order(2, 12), None, "12 = 2²·3 is not squarefree");
1185 }
1186
1187 fn models_m(n: usize, m: u64, equations: &[ModpEquation]) -> Vec<Vec<u64>> {
1189 (0..m.pow(n as u32))
1190 .filter_map(|code| {
1191 let mut c = code;
1192 let x: Vec<u64> = (0..n)
1193 .map(|_| {
1194 let v = c % m;
1195 c /= m;
1196 v
1197 })
1198 .collect();
1199 equations
1200 .iter()
1201 .all(|eq| eq.coeffs.iter().fold(0u64, |a, &(v, co)| (a + co * x[v]) % m) % m == eq.rhs % m)
1202 .then_some(x)
1203 })
1204 .collect()
1205 }
1206
1207 #[test]
1211 fn forced_values_squarefree_matches_brute_force() {
1212 use crate::modm::{forced_values_squarefree, ForcedM};
1213 let mut state = 0xC0DE_B0D5u64;
1214 let mut rng = || {
1215 state ^= state << 13;
1216 state ^= state >> 7;
1217 state ^= state << 17;
1218 state
1219 };
1220 for &m in &[6u64, 10, 15] {
1221 for _ in 0..150 {
1222 let eqs: Vec<ModpEquation> = (0..(1 + rng() % 3))
1223 .map(|_| ModpEquation::new(vec![(0usize, rng() % m), (1, rng() % m)], rng() % m))
1224 .collect();
1225 let models = models_m(2, m, &eqs);
1226 let brute: Vec<Option<u64>> = (0..2)
1227 .map(|g| {
1228 let vals: HashSet<u64> = models.iter().map(|x| x[g]).collect();
1229 if vals.len() == 1 { vals.into_iter().next() } else { None }
1230 })
1231 .collect();
1232 match forced_values_squarefree(&eqs, 2, m) {
1233 None => panic!("m={m} is squarefree"),
1234 Some(ForcedM::Inconsistent) => assert!(models.is_empty(), "Inconsistent ⇒ no ℤ/{m} solutions"),
1235 Some(ForcedM::Forced(f)) => assert_eq!(f, brute, "forced values must match brute force (m={m})"),
1236 }
1237 }
1238 }
1239 }
1240
1241 #[test]
1245 fn affine_m_forced_pins_composite_modulus_and_is_sound() {
1246 let m = 6u64;
1247 let eqs = vec![
1248 ModpEquation::new(vec![(0usize, 1u64)], 5),
1249 ModpEquation::new(vec![(1usize, 1u64)], 2),
1250 ];
1251 let (nbv, clauses) = onehot_cnf(m, 2, &eqs);
1252 match affine_m_forced(nbv, &clauses) {
1253 AffinePForced::Forced(units) => {
1254 assert!(units.contains(&vec![Lit::pos(5)]), "must pin b(edge0=5); got {units:?}"); assert!(units.contains(&vec![Lit::pos(8)]), "must pin b(edge1=2)"); let models = crate::affine::models_of(nbv, &clauses);
1257 assert!(!models.is_empty(), "the crafted ℤ/6 system is satisfiable");
1258 for u in &units {
1259 let l = u[0];
1260 assert!(
1261 models.iter().all(|&mm| ((mm >> l.var()) & 1 == 1) == l.is_positive()),
1262 "forced unit on var {} must hold in every Boolean model",
1263 l.var()
1264 );
1265 }
1266 }
1267 _ => panic!("the determined ℤ/6 edges must produce forced one-hot units"),
1268 }
1269 }
1270
1271 #[test]
1275 fn affine_m_forced_refutes_a_composite_zero_divisor_obstruction() {
1276 let m = 6u64;
1277 let eqs = vec![
1278 ModpEquation::new(vec![(0usize, 1u64), (1, 5)], 0), ModpEquation::new(vec![(1usize, 1u64), (2, 5)], 0), ModpEquation::new(vec![(0usize, 1u64), (2, 1)], 3), ];
1282 assert!(models_m(3, m, &eqs).is_empty(), "the ℤ/6 system is genuinely inconsistent");
1283 let (nbv, clauses) = onehot_cnf(m, 3, &eqs);
1284 assert!(matches!(affine_m_forced(nbv, &clauses), AffinePForced::Refuted(_)), "must refute the ℤ/6 obstruction");
1285 }
1286
1287 #[test]
1292 fn affine_m_link_lifts_to_composite_value_permuted_equivalences() {
1293 let m = 6u64;
1294 let eqs = vec![ModpEquation::new(vec![(0usize, 1u64), (1, 1)], 0)];
1295 let (nbv, clauses) = onehot_cnf(m, 2, &eqs);
1296 let AffinePForced::Forced(extra) = affine_m_forced(nbv, &clauses) else {
1297 panic!("the ℤ/6 link must fire");
1298 };
1299 assert!(extra.iter().any(|c| c.len() == 2), "must emit value-permuted bit-equivalences (2-literal clauses)");
1300 let models = crate::affine::models_of(nbv, &clauses);
1301 for c in &extra {
1302 assert!(
1303 models.iter().all(|&mm| c.iter().any(|l| ((mm >> l.var()) & 1 == 1) == l.is_positive())),
1304 "every emitted equivalence must hold in every Boolean model"
1305 );
1306 }
1307 let rec = crate::modp::recover_from_cnf(nbv, &clauses).expect("recovers");
1308 let pos_of = |bv: u32| {
1309 rec.groups.iter().enumerate().find_map(|(g, grp)| grp.iter().position(|&b| b == bv).map(|v| (g, v)))
1310 };
1311 assert!(
1312 extra.iter().filter(|c| c.len() == 2).any(|c| matches!(
1313 (pos_of(c[0].var()), pos_of(c[1].var())),
1314 (Some((g0, v0)), Some((g1, v1))) if g0 != g1 && v0 != v1
1315 )),
1316 "a genuine ℤ/6 link must produce a value-permuted equivalence"
1317 );
1318 }
1319
1320 #[test]
1325 fn affine_m_break_is_sound_against_brute_force() {
1326 let mut state = 0x0BAD_F00D_77u64;
1327 let mut rng = || {
1328 state ^= state << 13;
1329 state ^= state >> 7;
1330 state ^= state << 17;
1331 state
1332 };
1333 for &m in &[6u64] {
1337 for _ in 0..300 {
1338 let mut eqs: Vec<ModpEquation> = Vec::new();
1339 if rng() & 1 == 0 {
1340 eqs.push(ModpEquation::new(vec![(0usize, 1 + rng() % (m - 1))], rng() % m));
1341 }
1342 if rng() & 1 == 0 {
1343 eqs.push(ModpEquation::new(vec![(1usize, 1 + rng() % (m - 1))], rng() % m));
1344 }
1345 if rng() & 1 == 0 {
1346 eqs.push(ModpEquation::new(vec![(0usize, 1 + rng() % (m - 1)), (1, 1 + rng() % (m - 1))], rng() % m));
1347 }
1348 if eqs.is_empty() {
1349 continue;
1350 }
1351 let (nbv, clauses) = onehot_cnf(m, 2, &eqs);
1352 let models = crate::affine::models_of(nbv, &clauses);
1353 match affine_m_forced(nbv, &clauses) {
1354 AffinePForced::Refuted(_) => assert!(models.is_empty(), "Refuted ⇒ the Boolean CNF must be UNSAT (m={m})"),
1355 AffinePForced::Forced(extra) => {
1356 for c in &extra {
1357 assert!(
1358 models.iter().all(|&mm| c.iter().any(|l| ((mm >> l.var()) & 1 == 1) == l.is_positive())),
1359 "every composite consequence must hold in every Boolean model (m={m})"
1360 );
1361 }
1362 }
1363 AffinePForced::Unchanged => {}
1364 }
1365 }
1366 }
1367 }
1368
1369 #[test]
1374 fn affine_p_canonicalize_eliminates_and_lifts_soundly() {
1375 let eqs = vec![
1376 ModpEquation::new(vec![(0usize, 1u64), (1, 1)], 0),
1377 ModpEquation::new(vec![(2usize, 1u64)], 1),
1378 ];
1379 let (nbv, clauses) = onehot_cnf(3, 3, &eqs);
1380 match affine_p_canonicalize(nbv, &clauses) {
1381 AffinePCanon::Canonical(canon) => {
1382 assert!(canon.num_vars < nbv, "elimination must shrink the bit count ({} < {nbv})", canon.num_vars);
1383 let orig = crate::affine::models_of(nbv, &clauses);
1384 let red = crate::affine::models_of(canon.num_vars, &canon.clauses);
1385 assert_eq!(red.is_empty(), orig.is_empty(), "reduction must preserve satisfiability");
1386 for &rm_bits in &red {
1387 let rm: Vec<bool> = (0..canon.num_vars).map(|i| (rm_bits >> i) & 1 == 1).collect();
1388 let lifted = canon.lift(&rm);
1389 assert!(
1390 clauses.iter().all(|c| c.iter().any(|l| lifted[l.var() as usize] == l.is_positive())),
1391 "every lifted reduced model must satisfy the original formula"
1392 );
1393 }
1394 }
1395 _ => panic!("forced + linked groups must canonicalize"),
1396 }
1397 }
1398
1399 #[test]
1403 fn affine_p_canonicalize_is_sound_against_brute_force() {
1404 let mut state = 0xE11_3110u64;
1405 let mut rng = || {
1406 state ^= state << 13;
1407 state ^= state >> 7;
1408 state ^= state << 17;
1409 state
1410 };
1411 for &p in &[3u64, 5] {
1412 for _ in 0..120 {
1413 let mut eqs: Vec<ModpEquation> = Vec::new();
1414 if rng() & 1 == 0 {
1415 eqs.push(ModpEquation::new(vec![(0usize, 1 + rng() % (p - 1))], rng() % p));
1416 }
1417 if rng() & 1 == 0 {
1418 eqs.push(ModpEquation::new(vec![(1usize, 1 + rng() % (p - 1))], rng() % p));
1419 }
1420 if rng() & 1 == 0 {
1421 eqs.push(ModpEquation::new(vec![(0usize, 1 + rng() % (p - 1)), (1, 1 + rng() % (p - 1))], rng() % p));
1422 }
1423 if eqs.is_empty() {
1424 continue;
1425 }
1426 let (nbv, clauses) = onehot_cnf(p, 2, &eqs);
1427 let orig = crate::affine::models_of(nbv, &clauses);
1428 match affine_p_canonicalize(nbv, &clauses) {
1429 AffinePCanon::Refuted(_) => assert!(orig.is_empty(), "Refuted ⇒ original UNSAT (p={p})"),
1430 AffinePCanon::Canonical(canon) => {
1431 let red = crate::affine::models_of(canon.num_vars, &canon.clauses);
1432 assert_eq!(red.is_empty(), orig.is_empty(), "elimination preserves satisfiability (p={p})");
1433 for &rm_bits in &red {
1434 let rm: Vec<bool> = (0..canon.num_vars).map(|i| (rm_bits >> i) & 1 == 1).collect();
1435 let lifted = canon.lift(&rm);
1436 assert!(
1437 clauses.iter().all(|c| c.iter().any(|l| lifted[l.var() as usize] == l.is_positive())),
1438 "every lifted reduced model must satisfy the original (p={p})"
1439 );
1440 }
1441 }
1442 AffinePCanon::Unchanged => {}
1443 }
1444 }
1445 }
1446 }
1447
1448 #[test]
1451 fn forced_values_prime_power_is_exact() {
1452 use crate::modm::{forced_values_prime_power, ForcedM};
1453 let eqs = vec![
1454 ModpEquation::new(vec![(0usize, 2u64)], 4), ModpEquation::new(vec![(1usize, 1u64)], 5), ];
1457 match forced_values_prime_power(&eqs, 2, 8) {
1458 Some(ForcedM::Forced(f)) => {
1459 assert_eq!(f[0], None, "x0 is not forced — 2·x0 ≡ 4 has two solutions mod 8");
1460 assert_eq!(f[1], Some(5), "x1 is forced to 5");
1461 }
1462 other => panic!("expected a forced structure, got something else: {}", matches!(other, Some(ForcedM::Inconsistent))),
1463 }
1464 let unsolvable = vec![ModpEquation::new(vec![(0usize, 3u64)], 1)]; assert!(
1466 matches!(forced_values_prime_power(&unsolvable, 1, 9), Some(ForcedM::Inconsistent)),
1467 "3·x0 ≡ 1 (mod 9) is unsolvable"
1468 );
1469 }
1470
1471 #[test]
1475 fn affine_m_forced_handles_prime_power_modulus() {
1476 let m = 4u64;
1477 let eqs = vec![ModpEquation::new(vec![(0usize, 1u64)], 3)];
1478 let (nbv, clauses) = onehot_cnf(m, 1, &eqs);
1479 match affine_m_forced(nbv, &clauses) {
1480 AffinePForced::Forced(units) => {
1481 assert!(units.contains(&vec![Lit::pos(3)]), "must pin b(x0=3) over ℤ/4; got {units:?}");
1482 let models = crate::affine::models_of(nbv, &clauses);
1483 assert!(!models.is_empty(), "the ℤ/4 system is satisfiable");
1484 for u in &units {
1485 let l = u[0];
1486 assert!(
1487 models.iter().all(|&mm| ((mm >> l.var()) & 1 == 1) == l.is_positive()),
1488 "forced unit on var {} must hold in every Boolean model",
1489 l.var()
1490 );
1491 }
1492 }
1493 _ => panic!("the ℤ/4 forced variable must produce one-hot units"),
1494 }
1495 }
1496
1497 #[test]
1502 fn solve_space_prime_power_matches_brute() {
1503 use crate::modm::{solve_space_prime_power, PrimePowerSpace};
1504 let mut state = 0x511700D5u64;
1505 let mut rng = || {
1506 state ^= state << 13;
1507 state ^= state >> 7;
1508 state ^= state << 17;
1509 state
1510 };
1511 for &(p, k) in &[(2u64, 2u32), (2, 3), (3, 2)] {
1512 let q = p.pow(k);
1513 for _ in 0..200 {
1514 let nv = 1 + (rng() % 3) as usize;
1515 let eqs: Vec<ModpEquation> = (0..(1 + rng() % 3))
1516 .map(|_| {
1517 let kk = 1 + (rng() % nv as u64) as usize;
1518 let mut vars: Vec<usize> = Vec::new();
1519 while vars.len() < kk {
1520 let vv = (rng() % nv as u64) as usize;
1521 if !vars.contains(&vv) {
1522 vars.push(vv);
1523 }
1524 }
1525 ModpEquation::new(vars.iter().map(|&vv| (vv, rng() % q)).collect::<Vec<_>>(), rng() % q)
1526 })
1527 .collect();
1528 let total = (q as u128).pow(nv as u32) as u64;
1529 let mut sols: Vec<Vec<u64>> = Vec::new();
1530 for code in 0..total {
1531 let mut c = code;
1532 let x: Vec<u64> = (0..nv)
1533 .map(|_| {
1534 let vv = c % q;
1535 c /= q;
1536 vv
1537 })
1538 .collect();
1539 if eqs.iter().all(|eq| eq.coeffs.iter().fold(0u64, |a, &(vv, co)| (a + co * x[vv]) % q) % q == eq.rhs % q) {
1540 sols.push(x);
1541 }
1542 }
1543 match solve_space_prime_power(&eqs, nv, p, k).expect("no Smith overflow at this size") {
1544 PrimePowerSpace::Inconsistent => assert!(sols.is_empty(), "Inconsistent ⇒ no ℤ/{q} solution"),
1545 PrimePowerSpace::Space(ss) => {
1546 assert!(!sols.is_empty(), "a Space ⇒ at least one solution (ℤ/{q})");
1547 assert!(
1548 eqs.iter().all(|eq| eq.coeffs.iter().fold(0u64, |a, &(vv, co)| (a + co * ss.particular[vv]) % q) % q == eq.rhs % q),
1549 "the particular solution must satisfy (ℤ/{q})"
1550 );
1551 for g in 0..nv {
1552 let smith = ss.kernel_basis.iter().all(|kk| kk[g] == 0);
1553 let vals: HashSet<u64> = sols.iter().map(|s| s[g]).collect();
1554 assert_eq!(smith, vals.len() == 1, "forced(var {g}): Smith vs brute (ℤ/{q})");
1555 if smith {
1556 assert_eq!(ss.particular[g], *vals.iter().next().unwrap(), "forced value matches brute (ℤ/{q})");
1557 }
1558 }
1559 }
1560 }
1561 }
1562 }
1563 }
1564
1565 #[test]
1569 fn forced_values_prime_power_scales_past_the_brute_cap() {
1570 use crate::modm::{forced_values_prime_power, ForcedM};
1571 let m = 16u64;
1572 let eqs: Vec<ModpEquation> = (0..6usize).map(|i| ModpEquation::new(vec![(i, 1u64)], i as u64 + 1)).collect();
1573 match forced_values_prime_power(&eqs, 6, m) {
1574 Some(ForcedM::Forced(f)) => {
1575 for i in 0..6 {
1576 assert_eq!(f[i], Some(i as u64 + 1), "x{i} forced to {} over ℤ/16 (2²⁴ tuples, Smith only)", i + 1);
1577 }
1578 }
1579 _ => panic!("ℤ/16 forcing over 6 vars must succeed via the scalable Smith path"),
1580 }
1581 }
1582
1583 #[test]
1587 fn allowed_residues_matches_brute() {
1588 use crate::modm::{allowed_residues, AllowedOutcome};
1589 let mut state = 0xA110_0ED5u64;
1590 let mut rng = || {
1591 state ^= state << 13;
1592 state ^= state >> 7;
1593 state ^= state << 17;
1594 state
1595 };
1596 for &m in &[4u64, 8, 9, 6, 12] {
1597 for _ in 0..150 {
1598 let nv = 1 + (rng() % 2) as usize;
1599 let eqs: Vec<ModpEquation> = (0..(1 + rng() % 2))
1600 .map(|_| {
1601 let kk = 1 + (rng() % nv as u64) as usize;
1602 let mut vars: Vec<usize> = Vec::new();
1603 while vars.len() < kk {
1604 let vv = (rng() % nv as u64) as usize;
1605 if !vars.contains(&vv) {
1606 vars.push(vv);
1607 }
1608 }
1609 ModpEquation::new(vars.iter().map(|&vv| (vv, rng() % m)).collect::<Vec<_>>(), rng() % m)
1610 })
1611 .collect();
1612 let total = (m as u128).pow(nv as u32) as u64;
1613 let mut sols: Vec<Vec<u64>> = Vec::new();
1614 for code in 0..total {
1615 let mut c = code;
1616 let x: Vec<u64> = (0..nv)
1617 .map(|_| {
1618 let vv = c % m;
1619 c /= m;
1620 vv
1621 })
1622 .collect();
1623 if eqs.iter().all(|eq| eq.coeffs.iter().fold(0u64, |a, &(vv, co)| (a + co * x[vv]) % m) % m == eq.rhs % m) {
1624 sols.push(x);
1625 }
1626 }
1627 match allowed_residues(&eqs, nv, m).expect("no Smith overflow") {
1628 AllowedOutcome::Inconsistent => assert!(sols.is_empty(), "Inconsistent ⇒ no ℤ/{m} solution"),
1629 AllowedOutcome::Allowed(residues) => {
1630 assert!(!sols.is_empty());
1631 for g in 0..nv {
1632 let (res, modu) = residues[g];
1633 let vals: HashSet<u64> = sols.iter().map(|s| s[g]).collect();
1634 for v in 0..m {
1635 assert_eq!(vals.contains(&v), v % modu == res, "var {g} value {v} allowed? (m={m}, res={res} mod {modu})");
1636 }
1637 }
1638 }
1639 }
1640 }
1641 }
1642 }
1643
1644 #[test]
1650 fn affine_m_forced_partially_forces_a_prime_power_value_subset() {
1651 let m = 8u64;
1652 let eqs = vec![
1653 ModpEquation::new(vec![(0usize, 1u64), (1, 1)], 0), ModpEquation::new(vec![(1usize, 2u64)], 0), ];
1656 let (nbv, clauses) = onehot_cnf(m, 2, &eqs);
1657 match affine_m_forced(nbv, &clauses) {
1658 AffinePForced::Forced(units) => {
1659 for v in [1u32, 2, 3, 5, 6, 7] {
1661 assert!(units.contains(&vec![Lit::neg(v)]), "must derive-forbid b(x0={v}) (off ≡0 mod 4)");
1662 }
1663 let models = crate::affine::models_of(nbv, &clauses);
1664 assert!(!models.is_empty(), "the ℤ/8 system is satisfiable");
1665 for u in &units {
1666 assert!(
1667 models.iter().all(|&mm| u.iter().any(|l| ((mm >> l.var()) & 1 == 1) == l.is_positive())),
1668 "emitted clause {u:?} must hold in every Boolean model"
1669 );
1670 }
1671 }
1672 _ => panic!("ℤ/8 derived partial forcing must fire"),
1673 }
1674 }
1675
1676 #[test]
1681 fn affine_m_forced_links_over_a_prime_power_ring() {
1682 let m = 8u64;
1683 let eqs = vec![ModpEquation::new(vec![(0usize, 1u64), (1, 5)], 0)];
1684 let (nbv, clauses) = onehot_cnf(m, 2, &eqs);
1685 let AffinePForced::Forced(extra) = affine_m_forced(nbv, &clauses) else {
1686 panic!("the ℤ/8 ring link must fire");
1687 };
1688 assert!(extra.iter().any(|c| c.len() == 2), "must emit value-permuted bit-equivalences");
1689 let models = crate::affine::models_of(nbv, &clauses);
1690 for c in &extra {
1691 assert!(
1692 models.iter().all(|&mm| c.iter().any(|l| ((mm >> l.var()) & 1 == 1) == l.is_positive())),
1693 "every emitted equivalence must hold in every Boolean model"
1694 );
1695 }
1696 let rec = crate::modp::recover_from_cnf(nbv, &clauses).expect("recovers");
1697 let pos_of = |bv: u32| {
1698 rec.groups.iter().enumerate().find_map(|(g, grp)| grp.iter().position(|&b| b == bv).map(|v| (g, v)))
1699 };
1700 assert!(
1701 extra.iter().filter(|c| c.len() == 2).any(|c| matches!(
1702 (pos_of(c[0].var()), pos_of(c[1].var())),
1703 (Some((g0, v0)), Some((g1, v1))) if g0 != g1 && v0 != v1
1704 )),
1705 "a genuine ℤ/8 ring link must produce a value-permuted equivalence"
1706 );
1707 }
1708
1709 #[test]
1713 fn affine_m_break_is_sound_over_a_prime_power_ring() {
1714 let m = 8u64;
1715 let mut state = 0x21B6_0FF5u64;
1716 let mut rng = || {
1717 state ^= state << 13;
1718 state ^= state >> 7;
1719 state ^= state << 17;
1720 state
1721 };
1722 for _ in 0..120 {
1723 let mut eqs: Vec<ModpEquation> = Vec::new();
1724 if rng() & 1 == 0 {
1725 eqs.push(ModpEquation::new(vec![(0usize, 1 + rng() % (m - 1))], rng() % m));
1726 }
1727 if rng() & 1 == 0 {
1728 eqs.push(ModpEquation::new(vec![(1usize, 1 + rng() % (m - 1))], rng() % m));
1729 }
1730 if rng() & 1 == 0 {
1731 eqs.push(ModpEquation::new(vec![(0usize, 1 + rng() % (m - 1)), (1, 1 + rng() % (m - 1))], rng() % m));
1732 }
1733 if eqs.is_empty() {
1734 continue;
1735 }
1736 let (nbv, clauses) = onehot_cnf(m, 2, &eqs);
1737 let models = crate::affine::models_of(nbv, &clauses);
1738 match affine_m_forced(nbv, &clauses) {
1739 AffinePForced::Refuted(_) => assert!(models.is_empty(), "Refuted ⇒ the ℤ/8 CNF must be UNSAT"),
1740 AffinePForced::Forced(extra) => {
1741 for c in &extra {
1742 assert!(
1743 models.iter().all(|&mm| c.iter().any(|l| ((mm >> l.var()) & 1 == 1) == l.is_positive())),
1744 "every ℤ/8 consequence must hold in every Boolean model"
1745 );
1746 }
1747 }
1748 AffinePForced::Unchanged => {}
1749 }
1750 }
1751 }
1752
1753 #[test]
1757 fn affine_m_canonicalize_eliminates_and_lifts_soundly() {
1758 let m = 4u64;
1759 let eqs = vec![
1760 ModpEquation::new(vec![(0usize, 1u64)], 3),
1761 ModpEquation::new(vec![(1usize, 1u64), (2, 1)], 0),
1762 ];
1763 let (nbv, clauses) = onehot_cnf(m, 3, &eqs);
1764 match affine_m_canonicalize(nbv, &clauses) {
1765 AffinePCanon::Canonical(canon) => {
1766 assert!(canon.num_vars < nbv, "elimination must shrink the bit count ({} < {nbv})", canon.num_vars);
1767 let orig = crate::affine::models_of(nbv, &clauses);
1768 let red = crate::affine::models_of(canon.num_vars, &canon.clauses);
1769 assert!(!orig.is_empty(), "the ℤ/4 system is satisfiable");
1770 assert_eq!(red.is_empty(), orig.is_empty(), "reduction must preserve satisfiability");
1771 for &rm_bits in &red {
1772 let rm: Vec<bool> = (0..canon.num_vars).map(|i| (rm_bits >> i) & 1 == 1).collect();
1773 let lifted = canon.lift(&rm);
1774 assert!(
1775 clauses.iter().all(|c| c.iter().any(|l| lifted[l.var() as usize] == l.is_positive())),
1776 "every lifted reduced model must satisfy the original formula"
1777 );
1778 }
1779 }
1780 _ => panic!("the forced + linked ℤ/4 groups must canonicalize"),
1781 }
1782 }
1783
1784 #[test]
1789 fn affine_m_canonicalize_is_sound_against_brute_force() {
1790 let mut state = 0x5EED_3110u64;
1791 let mut rng = || {
1792 state ^= state << 13;
1793 state ^= state >> 7;
1794 state ^= state << 17;
1795 state
1796 };
1797 for &m in &[4u64, 6, 8] {
1798 for _ in 0..150 {
1799 let mut eqs: Vec<ModpEquation> = Vec::new();
1800 if rng() % 3 != 0 {
1801 eqs.push(ModpEquation::new(vec![(0usize, 1 + rng() % (m - 1))], rng() % m));
1802 }
1803 if rng() % 3 != 0 {
1804 eqs.push(ModpEquation::new(vec![(1usize, 1 + rng() % (m - 1))], rng() % m));
1805 }
1806 if rng() % 3 != 0 {
1807 eqs.push(ModpEquation::new(vec![(0usize, 1 + rng() % (m - 1)), (1, 1 + rng() % (m - 1))], rng() % m));
1808 }
1809 if eqs.is_empty() {
1810 continue;
1811 }
1812 let (nbv, clauses) = onehot_cnf(m, 2, &eqs);
1813 let orig = crate::affine::models_of(nbv, &clauses);
1814 match affine_m_canonicalize(nbv, &clauses) {
1815 AffinePCanon::Refuted(_) => assert!(orig.is_empty(), "Refuted ⇒ original UNSAT (m={m})"),
1816 AffinePCanon::Canonical(canon) => {
1817 let red = crate::affine::models_of(canon.num_vars, &canon.clauses);
1818 assert_eq!(red.is_empty(), orig.is_empty(), "elimination preserves satisfiability (m={m})");
1819 for &rm_bits in &red {
1820 let rm: Vec<bool> = (0..canon.num_vars).map(|i| (rm_bits >> i) & 1 == 1).collect();
1821 let lifted = canon.lift(&rm);
1822 assert!(
1823 clauses.iter().all(|c| c.iter().any(|l| lifted[l.var() as usize] == l.is_positive())),
1824 "every lifted reduced model must satisfy the original (m={m})"
1825 );
1826 }
1827 }
1828 AffinePCanon::Unchanged => {}
1829 }
1830 }
1831 }
1832 }
1833}