1use crate::cdcl::Lit;
7use crate::dimacs::DimacsCnf;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum ExpectedVerdict {
12 Sat,
13 Unsat,
14}
15
16pub fn php(n: usize) -> (DimacsCnf, ExpectedVerdict) {
21 let holes = n.saturating_sub(1);
22 let num_vars = n * holes;
23 let var = |p: usize, h: usize| Lit::pos((p * holes + h) as u32);
24 let mut clauses: Vec<Vec<Lit>> = Vec::new();
25 for p in 0..n {
27 clauses.push((0..holes).map(|h| var(p, h)).collect());
28 }
29 for h in 0..holes {
31 for p in 0..n {
32 for q in (p + 1)..n {
33 clauses.push(vec![var(p, h).negated(), var(q, h).negated()]);
34 }
35 }
36 }
37 (DimacsCnf { num_vars, clauses }, ExpectedVerdict::Unsat)
38}
39
40pub fn parity_exactly_one(n: usize) -> (DimacsCnf, ExpectedVerdict) {
48 assert!(n >= 2, "need at least two selectors");
49 let x = |i: usize| i as u32; let s = |i: usize| (n + i) as u32; let mut clauses: Vec<Vec<Lit>> = Vec::new();
52 clauses.push((0..n).map(|i| Lit::pos(x(i))).collect());
54 for i in 0..n {
55 for j in (i + 1)..n {
56 clauses.push(vec![Lit::neg(x(i)), Lit::neg(x(j))]);
57 }
58 }
59 let gadget = |vars: &[u32], out: &mut Vec<Vec<Lit>>| {
61 let k = vars.len();
62 for mask in 0u32..(1 << k) {
63 if mask.count_ones() % 2 == 1 {
64 out.push((0..k).map(|t| Lit::new(vars[t], (mask >> t) & 1 == 0)).collect());
65 }
66 }
67 };
68 gadget(&[s(0), x(0)], &mut clauses);
69 for i in 1..n {
70 gadget(&[s(i), s(i - 1), x(i)], &mut clauses);
71 }
72 clauses.push(vec![Lit::neg(s(n - 1))]);
73 (DimacsCnf { num_vars: 2 * n, clauses }, ExpectedVerdict::Unsat)
74}
75
76pub fn clique_coloring(n: usize, k: usize) -> (DimacsCnf, ExpectedVerdict) {
81 let num_vars = n * k;
82 let var = |v: usize, c: usize| Lit::pos((v * k + c) as u32);
83 let mut clauses: Vec<Vec<Lit>> = Vec::new();
84 for v in 0..n {
86 clauses.push((0..k).map(|c| var(v, c)).collect());
87 }
88 for u in 0..n {
90 for w in (u + 1)..n {
91 for c in 0..k {
92 clauses.push(vec![var(u, c).negated(), var(w, c).negated()]);
93 }
94 }
95 }
96 let verdict = if k < n { ExpectedVerdict::Unsat } else { ExpectedVerdict::Sat };
97 (DimacsCnf { num_vars, clauses }, verdict)
98}
99
100pub fn mutilated_chessboard(n: usize) -> (DimacsCnf, ExpectedVerdict) {
108 assert!(n >= 4 && n % 2 == 0, "the parity argument needs an even board ≥ 4");
109 let removed = |r: usize, c: usize| (r == 0 && c == 0) || (r == n - 1 && c == n - 1);
110 let sq = |r: usize, c: usize| r * n + c;
111 let mut edges: Vec<(usize, usize)> = Vec::new();
113 for r in 0..n {
114 for c in 0..n {
115 if removed(r, c) {
116 continue;
117 }
118 if c + 1 < n && !removed(r, c + 1) {
119 edges.push((sq(r, c), sq(r, c + 1)));
120 }
121 if r + 1 < n && !removed(r + 1, c) {
122 edges.push((sq(r, c), sq(r + 1, c)));
123 }
124 }
125 }
126 let num_vars = edges.len();
127 let mut incident: std::collections::HashMap<usize, Vec<usize>> = std::collections::HashMap::new();
128 for (e, &(a, b)) in edges.iter().enumerate() {
129 incident.entry(a).or_default().push(e);
130 incident.entry(b).or_default().push(e);
131 }
132 let mut clauses: Vec<Vec<Lit>> = Vec::new();
133 for r in 0..n {
134 for c in 0..n {
135 if removed(r, c) {
136 continue;
137 }
138 let inc = incident.get(&sq(r, c)).cloned().unwrap_or_default();
139 clauses.push(inc.iter().map(|&e| Lit::pos(e as u32)).collect()); for i in 0..inc.len() {
141 for j in (i + 1)..inc.len() {
142 clauses.push(vec![Lit::pos(inc[i] as u32).negated(), Lit::pos(inc[j] as u32).negated()]); }
144 }
145 }
146 }
147 (DimacsCnf { num_vars, clauses }, ExpectedVerdict::Unsat)
148}
149
150pub fn ordering_principle(n: usize) -> (DimacsCnf, ExpectedVerdict) {
156 assert!(n >= 2);
157 let var = |i: usize, j: usize| Lit::pos((i * n + j) as u32);
158 let num_vars = n * n;
159 let mut clauses: Vec<Vec<Lit>> = Vec::new();
160 for i in 0..n {
161 for j in (i + 1)..n {
162 clauses.push(vec![var(i, j), var(j, i)]); clauses.push(vec![var(i, j).negated(), var(j, i).negated()]); }
165 }
166 for i in 0..n {
167 for j in 0..n {
168 for k in 0..n {
169 if i != j && j != k && i != k {
170 clauses.push(vec![var(i, j).negated(), var(j, k).negated(), var(i, k)]); }
172 }
173 }
174 }
175 for i in 0..n {
176 clauses.push((0..n).filter(|&j| j != i).map(|j| var(i, j)).collect()); }
178 (DimacsCnf { num_vars, clauses }, ExpectedVerdict::Unsat)
179}
180
181pub fn random_3sat(vars: usize, num_clauses: usize, seed: u64) -> DimacsCnf {
186 let mut state = seed;
187 let mut next = move || {
188 state = state.wrapping_add(0x9E3779B97F4A7C15);
189 let mut z = state;
190 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
191 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
192 z ^ (z >> 31)
193 };
194 let mut clauses: Vec<Vec<Lit>> = Vec::with_capacity(num_clauses);
195 while clauses.len() < num_clauses {
196 let mut vs: Vec<u32> = Vec::with_capacity(3);
197 while vs.len() < 3 {
198 let v = (next() as usize % vars) as u32;
199 if !vs.contains(&v) {
200 vs.push(v);
201 }
202 }
203 clauses.push(vs.iter().map(|&v| Lit::new(v, next() & 1 == 0)).collect());
204 }
205 DimacsCnf { num_vars: vars, clauses }
206}
207
208pub fn random_ksat(k: usize, vars: usize, num_clauses: usize, seed: u64) -> DimacsCnf {
213 let mut state = seed;
214 let mut next = move || {
215 state = state.wrapping_add(0x9E3779B97F4A7C15);
216 let mut z = state;
217 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
218 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
219 z ^ (z >> 31)
220 };
221 let mut clauses: Vec<Vec<Lit>> = Vec::with_capacity(num_clauses);
222 while clauses.len() < num_clauses {
223 let mut vs: Vec<u32> = Vec::with_capacity(k);
224 while vs.len() < k {
225 let v = (next() as usize % vars) as u32;
226 if !vs.contains(&v) {
227 vs.push(v);
228 }
229 }
230 clauses.push(vs.iter().map(|&v| Lit::new(v, next() & 1 == 0)).collect());
231 }
232 DimacsCnf { num_vars: vars, clauses }
233}
234
235fn xor_clauses(vs: &[u32], rhs: bool) -> Vec<Vec<Lit>> {
238 let mut out = Vec::new();
239 for mask in 0u32..(1 << vs.len()) {
240 let odd = mask.count_ones() % 2 == 1;
241 if odd != rhs {
242 out.push(vs.iter().enumerate().map(|(i, &v)| Lit::new(v, (mask >> i) & 1 == 0)).collect());
244 }
245 }
246 out
247}
248
249fn gf2_absorb(basis: &mut std::collections::HashMap<u32, Vec<u32>>, vars: &[u32]) -> bool {
254 let mut row: std::collections::BTreeSet<u32> = vars.iter().copied().collect();
255 while let Some(&pivot) = row.iter().next() {
256 match basis.get(&pivot) {
257 Some(b) => {
258 for &v in b {
259 if !row.remove(&v) {
260 row.insert(v); }
262 }
263 }
264 None => {
265 basis.insert(pivot, row.iter().copied().collect());
266 return true;
267 }
268 }
269 }
270 false
271}
272
273pub fn random_kxor(k: usize, n: usize, m: usize, seed: u64) -> (Vec<crate::xorsat::XorEquation>, DimacsCnf) {
290 assert!((1..=n).contains(&k) && m >= 1, "need 1 ≤ k ≤ n and m ≥ 1");
291 let mut state = seed;
292 let mut next = move || {
293 state = state.wrapping_add(0x9E3779B97F4A7C15);
294 let mut z = state;
295 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
296 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
297 z ^ (z >> 31)
298 };
299 let planted: Vec<bool> = (0..n).map(|_| next() & 1 == 0).collect();
300 let mut draw = || {
301 let mut vs: Vec<u32> = Vec::with_capacity(k);
302 while vs.len() < k {
303 let v = (next() as usize % n) as u32;
304 if !vs.contains(&v) {
305 vs.push(v);
306 }
307 }
308 vs
309 };
310 let consistent_rhs = |vs: &[u32]| vs.iter().fold(false, |a, &v| a ^ planted[v as usize]);
311
312 let target_rank = if k % 2 == 1 { n } else { n.saturating_sub(1) };
314 let cap = 8 * n + 64; let mut basis: std::collections::HashMap<u32, Vec<u32>> = std::collections::HashMap::new();
316 let mut rows: Vec<(Vec<u32>, bool)> = Vec::new();
317 let mut rank = 0usize;
318 while (rank < target_rank || rows.len() < m) && rows.len() < cap {
319 let vs = draw();
320 if gf2_absorb(&mut basis, &vs) {
321 rank += 1;
322 }
323 let rhs = consistent_rhs(&vs);
324 rows.push((vs, rhs));
325 }
326 let vs = draw();
329 let rhs = !consistent_rhs(&vs);
330 rows.push((vs, rhs));
331
332 let mut eqs = Vec::new();
333 let mut clauses = Vec::new();
334 for (vs, rhs) in &rows {
335 eqs.push(crate::xorsat::XorEquation::new(vs.iter().map(|&v| v as usize).collect::<Vec<_>>(), *rhs));
336 clauses.extend(xor_clauses(vs, *rhs));
337 }
338 (eqs, DimacsCnf { num_vars: n, clauses })
339}
340
341pub fn parity_unsat(n: usize, m: usize, seed: u64) -> (Vec<crate::xorsat::XorEquation>, DimacsCnf) {
349 random_kxor(3, n, m, seed)
350}
351
352fn random_3regular(n: usize, seed: u64) -> Vec<(usize, usize)> {
356 let mut state = seed ^ 0x9E3779B97F4A7C15;
357 let mut next = move || {
358 state = state.wrapping_add(0x9E3779B97F4A7C15);
359 let mut z = state;
360 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
361 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
362 z ^ (z >> 31)
363 };
364 for _ in 0..4000 {
365 let mut stubs: Vec<usize> = (0..n).flat_map(|v| [v, v, v]).collect();
366 for i in (1..stubs.len()).rev() {
367 let j = (next() as usize) % (i + 1);
368 stubs.swap(i, j);
369 }
370 let mut edges = Vec::new();
371 let mut seen = std::collections::HashSet::new();
372 let mut ok = true;
373 for c in stubs.chunks(2) {
374 let (a, b) = (c[0].min(c[1]), c[0].max(c[1]));
375 if a == b || !seen.insert((a, b)) {
376 ok = false;
377 break;
378 }
379 edges.push((a, b));
380 }
381 if ok {
382 return edges;
383 }
384 }
385 panic!("could not build a simple 3-regular graph on {n} vertices");
386}
387
388pub fn tseitin_expander(n: usize, seed: u64) -> (Vec<crate::xorsat::XorEquation>, DimacsCnf, ExpectedVerdict) {
395 assert!(n % 2 == 0 && n >= 4, "a 3-regular graph needs an even vertex count ≥ 4");
396 let edges = random_3regular(n, seed);
397 let m = edges.len();
398 let mut incident: Vec<Vec<usize>> = vec![Vec::new(); n];
399 for (e, &(a, b)) in edges.iter().enumerate() {
400 incident[a].push(e);
401 incident[b].push(e);
402 }
403 let mut eqs = Vec::new();
404 let mut clauses = Vec::new();
405 for v in 0..n {
406 let inc = &incident[v];
407 let r = v == 0; eqs.push(crate::xorsat::XorEquation::new(inc.clone(), r));
409 let d = inc.len();
410 for mask in 0u32..(1u32 << d) {
411 if ((mask.count_ones() % 2) == 1) != r {
412 clauses.push((0..d).map(|i| Lit::new(inc[i] as u32, (mask >> i) & 1 == 0)).collect());
413 }
414 }
415 }
416 (eqs, DimacsCnf { num_vars: m, clauses }, ExpectedVerdict::Unsat)
417}
418
419pub fn grid_tseitin(w: usize, n: usize) -> (Vec<crate::xorsat::XorEquation>, DimacsCnf, ExpectedVerdict) {
428 assert!(w >= 2 && n >= 2, "a grid needs both dimensions ≥ 2");
429 let vid = |i: usize, j: usize| i * n + j;
430 let mut incident: Vec<Vec<usize>> = vec![Vec::new(); w * n];
431 let mut num_edges = 0usize;
432 for i in 0..w {
433 for j in 0..n {
434 let v = vid(i, j);
435 if j + 1 < n {
436 let e = num_edges;
437 num_edges += 1;
438 incident[v].push(e);
439 incident[vid(i, j + 1)].push(e);
440 }
441 if i + 1 < w {
442 let e = num_edges;
443 num_edges += 1;
444 incident[v].push(e);
445 incident[vid(i + 1, j)].push(e);
446 }
447 }
448 }
449 let mut eqs = Vec::new();
450 let mut clauses = Vec::new();
451 for v in 0..(w * n) {
452 let inc = &incident[v];
453 let r = v == 0; eqs.push(crate::xorsat::XorEquation::new(inc.clone(), r));
455 let d = inc.len();
456 for mask in 0u32..(1u32 << d) {
457 if ((mask.count_ones() % 2) == 1) != r {
458 clauses.push((0..d).map(|i| Lit::new(inc[i] as u32, (mask >> i) & 1 == 0)).collect());
459 }
460 }
461 }
462 (eqs, DimacsCnf { num_vars: num_edges, clauses }, ExpectedVerdict::Unsat)
463}
464
465pub fn mod_p_tseitin_expander(
475 n: usize,
476 p: u64,
477 seed: u64,
478) -> (Vec<crate::modp::ModpEquation>, DimacsCnf, ExpectedVerdict) {
479 assert!(p >= 3, "the mod-p obstruction needs an odd prime (p=2 is the parity case, consistent here)");
480 mod_tseitin_expander_core(n, p, seed)
481}
482
483pub fn mod_m_tseitin_expander(
491 n: usize,
492 m: u64,
493 seed: u64,
494) -> (Vec<crate::modp::ModpEquation>, DimacsCnf, ExpectedVerdict) {
495 assert!(m >= 3, "the composite obstruction needs a modulus ≥ 3 (m=2 is the parity case, consistent here)");
496 mod_tseitin_expander_core(n, m, seed)
497}
498
499fn mod_tseitin_expander_core(
506 n: usize,
507 modulus: u64,
508 seed: u64,
509) -> (Vec<crate::modp::ModpEquation>, DimacsCnf, ExpectedVerdict) {
510 use crate::cdcl::Lit;
511 use crate::modp::ModpEquation;
512 assert!(n % 2 == 0 && n >= 4, "a 3-regular graph needs an even vertex count ≥ 4");
513 let edges = random_3regular(n, seed); let ne = edges.len();
515 let charge = |v: usize| u64::from(v == 0 || v == 1); let mut eqs: Vec<ModpEquation> = Vec::new();
519 for v in 0..n {
520 let mut coeffs: Vec<(usize, u64)> = Vec::new();
521 for (e, &(a, b)) in edges.iter().enumerate() {
522 if a == v {
523 coeffs.push((e, 1));
524 } else if b == v {
525 coeffs.push((e, modulus - 1));
526 }
527 }
528 eqs.push(ModpEquation::new(coeffs, charge(v)));
529 }
530
531 let bvar = |e: usize, val: u64| (e * modulus as usize + val as usize) as u32;
533 let mut clauses: Vec<Vec<Lit>> = Vec::new();
534 for e in 0..ne {
535 clauses.push((0..modulus).map(|val| Lit::pos(bvar(e, val))).collect()); for v1 in 0..modulus {
537 for v2 in (v1 + 1)..modulus {
538 clauses.push(vec![Lit::neg(bvar(e, v1)), Lit::neg(bvar(e, v2))]); }
540 }
541 }
542 for v in 0..n {
544 let incident: Vec<(usize, i64)> = edges
545 .iter()
546 .enumerate()
547 .filter_map(|(e, &(a, b))| {
548 if a == v {
549 Some((e, 1i64))
550 } else if b == v {
551 Some((e, -1i64))
552 } else {
553 None
554 }
555 })
556 .collect();
557 let d = incident.len();
558 let want = charge(v) as i64;
559 let pi = modulus as i64;
560 for idx in 0..modulus.pow(d as u32) {
561 let mut x = idx;
562 let mut combo = vec![0u64; d];
563 for slot in combo.iter_mut() {
564 *slot = x % modulus;
565 x /= modulus;
566 }
567 let s = incident.iter().zip(&combo).fold(0i64, |acc, (&(_, sign), &val)| acc + sign * val as i64);
568 if (s.rem_euclid(pi)) != (want.rem_euclid(pi)) {
569 clauses.push(
570 incident.iter().zip(&combo).map(|(&(e, _), &val)| Lit::neg(bvar(e, val))).collect(),
571 );
572 }
573 }
574 }
575
576 (eqs, DimacsCnf { num_vars: ne * modulus as usize, clauses }, ExpectedVerdict::Unsat)
577}
578
579pub fn mod_p_consistent_onehot(
585 n: usize,
586 p: u64,
587 seed: u64,
588) -> (Vec<crate::modp::ModpEquation>, DimacsCnf, ExpectedVerdict) {
589 use crate::cdcl::Lit;
590 use crate::modp::ModpEquation;
591 assert!(n % 2 == 0 && n >= 4, "a 3-regular graph needs an even vertex count ≥ 4");
592 assert!(p >= 3, "the mod-p one-hot encoding needs an odd prime");
593 let edges = random_3regular(n, seed);
594 let ne = edges.len();
595 let charge = |v: usize| -> u64 {
596 match v {
597 0 => 1,
598 1 => p - 1,
599 _ => 0,
600 }
601 }; let mut eqs: Vec<ModpEquation> = Vec::new();
604 for v in 0..n {
605 let mut coeffs: Vec<(usize, u64)> = Vec::new();
606 for (e, &(a, b)) in edges.iter().enumerate() {
607 if a == v {
608 coeffs.push((e, 1));
609 } else if b == v {
610 coeffs.push((e, p - 1));
611 }
612 }
613 eqs.push(ModpEquation::new(coeffs, charge(v)));
614 }
615
616 let bvar = |e: usize, val: u64| (e * p as usize + val as usize) as u32;
617 let mut clauses: Vec<Vec<Lit>> = Vec::new();
618 for e in 0..ne {
619 clauses.push((0..p).map(|val| Lit::pos(bvar(e, val))).collect());
620 for v1 in 0..p {
621 for v2 in (v1 + 1)..p {
622 clauses.push(vec![Lit::neg(bvar(e, v1)), Lit::neg(bvar(e, v2))]);
623 }
624 }
625 }
626 for v in 0..n {
627 let incident: Vec<(usize, i64)> = edges
628 .iter()
629 .enumerate()
630 .filter_map(|(e, &(a, b))| {
631 if a == v {
632 Some((e, 1i64))
633 } else if b == v {
634 Some((e, -1i64))
635 } else {
636 None
637 }
638 })
639 .collect();
640 let d = incident.len();
641 let want = charge(v) as i64;
642 let pi = p as i64;
643 for idx in 0..p.pow(d as u32) {
644 let mut x = idx;
645 let mut combo = vec![0u64; d];
646 for slot in combo.iter_mut() {
647 *slot = x % p;
648 x /= p;
649 }
650 let s = incident.iter().zip(&combo).fold(0i64, |acc, (&(_, sign), &val)| acc + sign * val as i64);
651 if (s.rem_euclid(pi)) != (want.rem_euclid(pi)) {
652 clauses.push(
653 incident.iter().zip(&combo).map(|(&(e, _), &val)| Lit::neg(bvar(e, val))).collect(),
654 );
655 }
656 }
657 }
658
659 (eqs, DimacsCnf { num_vars: ne * p as usize, clauses }, ExpectedVerdict::Sat)
660}
661
662pub fn ksat_threshold_first_moment_upper(k: u32) -> f64 {
671 let pow = (1u64 << k) as f64;
672 std::f64::consts::LN_2 / (pow / (pow - 1.0)).ln()
673}
674
675fn combinations(n: usize, q: usize) -> Vec<Vec<usize>> {
679 let mut out = Vec::new();
680 if q == 0 || q > n {
681 return out;
682 }
683 let mut idx: Vec<usize> = (0..q).collect();
684 loop {
685 out.push(idx.clone());
686 let mut i = q;
687 loop {
688 if i == 0 {
689 return out;
690 }
691 i -= 1;
692 if idx[i] != i + n - q {
693 break;
694 }
695 }
696 idx[i] += 1;
697 for j in (i + 1)..q {
698 idx[j] = idx[j - 1] + 1;
699 }
700 }
701}
702
703pub fn weak_php(pigeons: usize, holes: usize) -> (DimacsCnf, ExpectedVerdict) {
710 let num_vars = pigeons * holes;
711 let var = |p: usize, h: usize| Lit::pos((p * holes + h) as u32);
712 let mut clauses: Vec<Vec<Lit>> = Vec::new();
713 for p in 0..pigeons {
714 clauses.push((0..holes).map(|h| var(p, h)).collect()); }
716 for h in 0..holes {
717 for p in 0..pigeons {
718 for q in (p + 1)..pigeons {
719 clauses.push(vec![var(p, h).negated(), var(q, h).negated()]); }
721 }
722 }
723 let verdict = if pigeons > holes { ExpectedVerdict::Unsat } else { ExpectedVerdict::Sat };
724 (DimacsCnf { num_vars, clauses }, verdict)
725}
726
727pub fn functional_php(n: usize) -> (DimacsCnf, ExpectedVerdict) {
732 let holes = n.saturating_sub(1);
733 let (mut cnf, _) = php(n);
734 let var = |p: usize, h: usize| Lit::pos((p * holes + h) as u32);
735 for p in 0..n {
736 for h1 in 0..holes {
737 for h2 in (h1 + 1)..holes {
738 cnf.clauses.push(vec![var(p, h1).negated(), var(p, h2).negated()]); }
740 }
741 }
742 (cnf, ExpectedVerdict::Unsat)
743}
744
745pub fn onto_php(n: usize) -> (DimacsCnf, ExpectedVerdict) {
750 let holes = n.saturating_sub(1);
751 let (mut cnf, _) = functional_php(n);
752 let var = |p: usize, h: usize| Lit::pos((p * holes + h) as u32);
753 for h in 0..holes {
754 cnf.clauses.push((0..n).map(|p| var(p, h)).collect()); }
756 (cnf, ExpectedVerdict::Unsat)
757}
758
759pub fn mod_counting(n: usize, q: usize) -> (DimacsCnf, ExpectedVerdict) {
768 assert!(q >= 2 && n >= q, "need a block size q ≥ 2 with n ≥ q");
769 let edges = combinations(n, q);
770 let num_vars = edges.len();
771 let mut incident: Vec<Vec<usize>> = vec![Vec::new(); n];
772 for (e, edge) in edges.iter().enumerate() {
773 for &v in edge {
774 incident[v].push(e);
775 }
776 }
777 let mut clauses: Vec<Vec<Lit>> = Vec::new();
778 for inc in &incident {
779 clauses.push(inc.iter().map(|&e| Lit::pos(e as u32)).collect()); }
781 for e in 0..edges.len() {
782 for f in (e + 1)..edges.len() {
783 if edges[e].iter().any(|v| edges[f].contains(v)) {
784 clauses.push(vec![Lit::neg(e as u32), Lit::neg(f as u32)]); }
786 }
787 }
788 let verdict = if n % q == 0 { ExpectedVerdict::Sat } else { ExpectedVerdict::Unsat };
789 (DimacsCnf { num_vars, clauses }, verdict)
790}
791
792pub fn mod_counting_groups(n: usize, q: usize) -> Vec<Vec<u32>> {
797 let (cnf, _) = mod_counting(n, q);
798 cnf.clauses
799 .iter()
800 .filter(|c| c.iter().all(|l| l.is_positive()))
801 .map(|c| c.iter().map(|l| l.var()).collect())
802 .collect()
803}
804
805pub fn mod_counting_edges(n: usize, q: usize) -> Vec<Vec<usize>> {
809 combinations(n, q)
810}
811
812pub fn ramsey_number(s: usize, t: usize) -> Option<usize> {
816 let (a, b) = (s.min(t), s.max(t));
817 match (a, b) {
818 (1, _) => Some(1),
819 (2, b) => Some(b),
820 (3, 3) => Some(6),
821 (3, 4) => Some(9),
822 (3, 5) => Some(14),
823 (3, 6) => Some(18),
824 (3, 7) => Some(23),
825 (3, 8) => Some(28),
826 (3, 9) => Some(36),
827 (4, 4) => Some(18),
828 (4, 5) => Some(25),
829 _ => None,
830 }
831}
832
833pub fn ramsey(s: usize, t: usize, n: usize) -> (DimacsCnf, ExpectedVerdict) {
841 assert!(s >= 2 && t >= 2 && n >= 2);
842 let r = ramsey_number(s, t).expect("Ramsey number R(s,t) must be known to pin the verdict");
843 let mut edge_id = std::collections::HashMap::new();
844 for (e, pair) in combinations(n, 2).iter().enumerate() {
845 edge_id.insert((pair[0], pair[1]), e as u32);
846 }
847 let num_vars = edge_id.len();
848 let mut clauses: Vec<Vec<Lit>> = Vec::new();
849 let clique_edges = |clique: &[usize]| -> Vec<u32> {
850 combinations(clique.len(), 2).iter().map(|p| edge_id[&(clique[p[0]], clique[p[1]])]).collect()
851 };
852 for clique in combinations(n, s) {
853 clauses.push(clique_edges(&clique).into_iter().map(Lit::neg).collect()); }
855 for clique in combinations(n, t) {
856 clauses.push(clique_edges(&clique).into_iter().map(Lit::pos).collect()); }
858 let verdict = if n >= r { ExpectedVerdict::Unsat } else { ExpectedVerdict::Sat };
859 (DimacsCnf { num_vars, clauses }, verdict)
860}
861
862pub fn pebbling_pyramid(height: usize) -> (DimacsCnf, ExpectedVerdict) {
871 let node = |r: usize, i: usize| (r * (r + 1) / 2 + i) as u32; let num_vars = (height + 1) * (height + 2) / 2;
873 let mut clauses: Vec<Vec<Lit>> = Vec::new();
874 for i in 0..=height {
875 clauses.push(vec![Lit::pos(node(height, i))]); }
877 for r in 0..height {
878 for i in 0..=r {
879 clauses.push(vec![
880 Lit::neg(node(r + 1, i)),
881 Lit::neg(node(r + 1, i + 1)),
882 Lit::pos(node(r, i)),
883 ]); }
885 }
886 clauses.push(vec![Lit::neg(node(0, 0))]); (DimacsCnf { num_vars, clauses }, ExpectedVerdict::Unsat)
888}
889
890#[cfg(test)]
891mod tests {
892 use super::*;
893 use crate::cdcl::SolveResult;
894
895 #[test]
901 fn the_ksat_threshold_sequence_first_moment_upper_bound() {
902 let ln2 = std::f64::consts::LN_2;
903 let ub = ksat_threshold_first_moment_upper;
904
905 let known = [(2u32, 2.40942), (3, 5.19089), (4, 10.73970), (5, 21.83239)];
907 for (k, want) in known {
908 assert!((ub(k) - want).abs() < 5e-3, "α*({k}) = {} want {want}", ub(k));
909 }
910
911 for k in 2..=20 {
913 assert!(ub(k + 1) > ub(k), "increasing at k={k}");
914 }
915 for k in 4..=20 {
916 let ratio = ub(k + 1) / ub(k);
917 assert!((1.9..=2.1).contains(&ratio), "ratio ≈ 2 at k={k}: {ratio}");
918 }
919
920 for k in 2..=20 {
922 let lead = (1u64 << k) as f64 * ln2;
923 assert!(ub(k) < lead, "below the leading term 2ᵏln2 at k={k}");
924 assert!((ub(k) - (lead - ln2 / 2.0)).abs() < 0.05, "→ 2ᵏln2 − ½ln2 at k={k}");
925 }
926
927 for k in 2..=20 {
930 let p = 1.0 - 2f64.powi(-(k as i32));
931 let base = |a: f64| 2.0 * p.powf(a);
932 assert!((base(ub(k)) - 1.0).abs() < 1e-9, "α*(k={k}) is exactly base = 1");
933 assert!(base(ub(k) + 0.5) < 1.0, "above α*(k={k}): E[X] → 0");
934 assert!(base(ub(k) - 0.5) > 1.0, "below α*(k={k}): E[X] → ∞");
935 }
936
937 for (k, sharp) in [(2u32, 1.0), (3, 4.267), (4, 9.931), (5, 21.117)] {
939 assert!(ub(k) > sharp, "first-moment bound α*({k})={} exceeds the true threshold {sharp}", ub(k));
940 }
941 }
942
943 #[test]
955 fn reciprocal_threshold_sums_telescope_to_euler_and_gf2_constants() {
956 let ln2 = std::f64::consts::LN_2;
957 let recip = |k: u32| 1.0 / ksat_threshold_first_moment_upper(k);
958 let (mut s, mut p, mut alt, mut palt) = (0.0f64, 1.0f64, 0.0f64, 1.0f64);
959 for k in 1..=50u32 {
960 let term = 1.0 - 2f64.powi(-(k as i32));
961 s += recip(k);
962 p *= term;
963 assert!((s + p.ln() / ln2).abs() < 1e-9, "Σ1/α* telescopes to −log₂Π(1−2⁻ᵏ) at k={k}");
965 let sign = if k % 2 == 1 { 1.0 } else { -1.0 };
966 alt += sign * recip(k);
967 palt *= term.powf(if k % 2 == 1 { -1.0 } else { 1.0 });
968 assert!((alt - palt.ln() / ln2).abs() < 1e-9, "alternating sum telescopes to log₂Π(1−2⁻ᵏ)^((−1)ᵏ) at k={k}");
969 }
970 let phi_half = 0.288_788_095_1;
972 assert!((p - phi_half).abs() < 1e-9, "Π(1−2⁻ᵏ) → φ(½) = the GF(2) invertibility constant 0.28879");
973 assert!((s - 1.791_916_824_7).abs() < 1e-6, "Σ 1/α*_k ≈ 1.79192");
974 assert!((s + phi_half.ln() / ln2).abs() < 1e-6, "and it equals −log₂ φ(½) exactly");
975 assert!((alt - 0.715_131_251_2).abs() < 1e-6, "Σ(−1)ᵏ⁺¹/α*_k ≈ 0.71513");
977 }
978
979 #[test]
980 fn mutilated_chessboard_and_ordering_are_correctly_unsat() {
981 for n in [4, 6, 8] {
985 let (cnf, v) = mutilated_chessboard(n);
986 assert_eq!(v, ExpectedVerdict::Unsat);
987 assert!(cnf.clauses.iter().all(|c| !c.is_empty()), "chessboard {n} has no empty clause");
988 let solved = crate::solve::solve_structured(cnf.num_vars, &cnf.clauses);
989 assert!(matches!(solved.answer, crate::solve::Answer::Unsat), "mutilated chessboard {n} must be UNSAT");
990 }
991 for n in [3, 4, 5, 6] {
992 let (cnf, v) = ordering_principle(n);
993 assert_eq!(v, ExpectedVerdict::Unsat);
994 let solved = crate::solve::solve_structured(cnf.num_vars, &cnf.clauses);
995 assert!(matches!(solved.answer, crate::solve::Answer::Unsat), "ordering principle GT({n}) must be UNSAT");
996 }
997 }
998
999 #[test]
1000 fn tseitin_expander_is_unsat_and_gaussian_refutes_it() {
1001 for seed in [1u64, 7, 42] {
1005 let (eqs, cnf, verdict) = tseitin_expander(10, seed);
1006 assert_eq!(verdict, ExpectedVerdict::Unsat);
1007 match crate::xorsat::solve(&eqs, cnf.num_vars) {
1008 crate::xorsat::XorOutcome::Unsat(refutation) => {
1009 assert!(
1010 crate::xorsat::is_refutation(&eqs, cnf.num_vars, &refutation),
1011 "the Gaussian refutation must independently check"
1012 );
1013 }
1014 crate::xorsat::XorOutcome::Sat(_) => panic!("expander-Tseitin must be UNSAT"),
1015 }
1016 assert_eq!(cnf.into_solver().solve(), SolveResult::Unsat, "CNF encoding must be UNSAT too");
1018 }
1019 }
1020
1021 #[test]
1022 fn mod_p_tseitin_is_refuted_over_gf_p_with_a_checkable_certificate() {
1023 for &p in &[3u64, 5, 7] {
1027 for seed in [1u64, 7] {
1028 let (eqs, cnf, verdict) = mod_p_tseitin_expander(6, p, seed);
1029 assert_eq!(verdict, ExpectedVerdict::Unsat);
1030 let edges = cnf.num_vars / p as usize; match crate::modp::solve(&eqs, edges, p) {
1032 crate::modp::ModpOutcome::Unsat(combo) => assert!(
1033 crate::modp::is_refutation(&eqs, edges, p, &combo),
1034 "the GF({p}) refutation must independently re-check"
1035 ),
1036 crate::modp::ModpOutcome::Sat(_) => panic!("mod-{p} obstruction must be UNSAT over GF({p})"),
1037 }
1038 assert_eq!(cnf.into_solver().solve(), SolveResult::Unsat, "the Boolean CNF must be UNSAT");
1039 }
1040 }
1041 }
1042
1043 #[test]
1044 fn mod_p_obstruction_is_invisible_to_gf2() {
1045 let p = 3u64;
1049 let (eqs, cnf, _) = mod_p_tseitin_expander(6, p, 7);
1050 let edges = cnf.num_vars / p as usize;
1051 let gf2: Vec<crate::xorsat::XorEquation> = eqs
1052 .iter()
1053 .map(|eq| {
1054 let vars: Vec<usize> = eq.coeffs.iter().map(|&(v, _)| v).collect();
1055 crate::xorsat::XorEquation::new(vars, eq.rhs % 2 == 1)
1056 })
1057 .collect();
1058 assert!(
1059 matches!(crate::xorsat::solve(&gf2, edges), crate::xorsat::XorOutcome::Sat(_)),
1060 "over GF(2) the even-charge obstruction is satisfiable — the parity engine is blind to it"
1061 );
1062 }
1063
1064 #[test]
1065 fn mod_m_tseitin_is_refuted_over_the_ring_with_a_checkable_certificate() {
1066 for &m in &[6u64, 15] {
1070 for seed in [1u64, 7] {
1071 let (eqs, cnf, verdict) = mod_m_tseitin_expander(6, m, seed);
1072 assert_eq!(verdict, ExpectedVerdict::Unsat);
1073 let vars = cnf.num_vars / m as usize; match crate::modm::solve(&eqs, vars, m) {
1075 Some(crate::modm::ModmOutcome::Unsat { modulus, combo }) => assert!(
1076 crate::modm::is_refutation(&eqs, vars, modulus, &combo),
1077 "the ℤ/{m} refutation must independently re-check (via its GF({modulus}) factor)"
1078 ),
1079 other => panic!("mod-{m} obstruction must be UNSAT over ℤ/{m}, got {other:?}"),
1080 }
1081 assert_eq!(cnf.into_solver().solve(), SolveResult::Unsat, "the Boolean CNF must be UNSAT");
1082 }
1083 }
1084 }
1085
1086 #[test]
1093 #[ignore = "heavy: CDCL on Tseitin is exponential — that's the wall. Charts it vs the Gaussian collapse."]
1094 fn tseitin_parity_wall_collapses_under_gaussian() {
1095 use crate::xorsat::{is_refutation, solve, XorOutcome};
1096 let seed = 42u64;
1097 let mut rows = vec![
1098 " n | edges | CDCL: conflicts / time | Gaussian | certified".to_string(),
1099 "----+-------+--------------------------+--------------+----------".to_string(),
1100 ];
1101 for n in [16usize, 24, 32, 40, 48, 56] {
1102 let (eqs, cnf, verdict) = tseitin_expander(n, seed);
1103 assert_eq!(verdict, ExpectedVerdict::Unsat);
1104
1105 let mut solver = cnf.into_solver();
1106 let ct = std::time::Instant::now();
1107 assert_eq!(solver.solve(), SolveResult::Unsat, "Tseitin CNF is UNSAT");
1108 let cdcl_time = ct.elapsed();
1109 let conflicts = solver.conflicts();
1110
1111 let t = std::time::Instant::now();
1112 let out = solve(&eqs, cnf.num_vars);
1113 let gauss = t.elapsed();
1114 let certified = match &out {
1115 XorOutcome::Unsat(r) => is_refutation(&eqs, cnf.num_vars, r),
1116 XorOutcome::Sat(_) => false,
1117 };
1118 assert!(certified, "Gaussian gives a checkable refutation for n={n}");
1119 rows.push(format!("{n:3} | {:5} | {conflicts:10} {cdcl_time:>10?} | {gauss:>11?} | yes", cnf.num_vars));
1120 }
1121 let chart = rows.join("\n");
1122 eprintln!("\nTSEITIN PARITY WALL — resolution (CDCL) exponential vs Gaussian polynomial+certified\n{chart}\n");
1123 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../logs/derived_facts");
1124 if std::fs::create_dir_all(&dir).is_ok() {
1125 let _ = std::fs::write(dir.join("tseitin_parity_wall.txt"), format!("TSEITIN PARITY WALL — CDCL (resolution, 2^Ω(n)) vs Gaussian (poly, certified)\n\n{chart}\n"));
1126 }
1127 }
1128
1129
1130
1131 #[test]
1150 fn the_two_geometries_of_hardness() {
1151 let half_point_satisfies = |cnf: &DimacsCnf| cnf.clauses.iter().all(|c| c.len() >= 2);
1153
1154 let (php_cnf, _) = php(8);
1158 let (eqs, tse_cnf, _) = tseitin_expander(12, 7);
1159 assert!(half_point_satisfies(&php_cnf), "x≡½ ∈ the PHP clause-polytope");
1160 assert!(half_point_satisfies(&tse_cnf), "x≡½ ∈ the Tseitin clause-polytope");
1161
1162 assert!(crate::pigeonhole::certify_pigeonhole_unsat(8, 7).is_some(), "convex/Farkas counting hyperplane refutes PHP(8)");
1167
1168 match crate::xorsat::solve(&eqs, tse_cnf.num_vars) {
1173 crate::xorsat::XorOutcome::Unsat(r) => assert!(crate::xorsat::is_refutation(&eqs, tse_cnf.num_vars, &r), "GF(2) geometry refutes parity, certified"),
1174 crate::xorsat::XorOutcome::Sat(_) => panic!("Tseitin must be UNSAT"),
1175 }
1176 }
1179
1180 #[test]
1191 fn counting_is_provably_blind_two_covers_same_counts_opposite_answers() {
1192 use crate::xorsat::{solve, XorEquation, XorOutcome};
1193 let build = |edges: &[(usize, usize)], n: usize, charges: &[bool]| -> (Vec<XorEquation>, DimacsCnf) {
1195 let mut incident = vec![Vec::new(); n];
1196 for (e, &(a, b)) in edges.iter().enumerate() {
1197 incident[a].push(e);
1198 incident[b].push(e);
1199 }
1200 let (mut eqs, mut clauses) = (Vec::new(), Vec::new());
1201 for v in 0..n {
1202 let (inc, r, d) = (&incident[v], charges[v], incident[v].len());
1203 eqs.push(XorEquation::new(inc.clone(), r));
1204 for mask in 0u32..(1u32 << d) {
1205 if ((mask.count_ones() % 2) == 1) != r {
1206 clauses.push((0..d).map(|i| Lit::new(inc[i] as u32, (mask >> i) & 1 == 0)).collect());
1207 }
1208 }
1209 }
1210 (eqs, DimacsCnf { num_vars: edges.len(), clauses })
1211 };
1212
1213 let edges = super::random_3regular(12, 7);
1214 let (eqs_sat, cnf_sat) = build(&edges, 12, &vec![false; 12]); let mut odd = vec![false; 12];
1216 odd[0] = true; let (eqs_uns, cnf_uns) = build(&edges, 12, &odd);
1218
1219 let profile = |cnf: &DimacsCnf| {
1221 let mut lens: Vec<usize> = cnf.clauses.iter().map(|c| c.len()).collect();
1222 lens.sort_unstable();
1223 let mut occ = vec![0usize; cnf.num_vars];
1224 for c in &cnf.clauses {
1225 for l in c {
1226 occ[l.var() as usize] += 1;
1227 }
1228 }
1229 occ.sort_unstable();
1230 (cnf.clauses.len(), lens, occ)
1231 };
1232 assert_eq!(profile(&cnf_sat), profile(&cnf_uns), "IDENTICAL counting profile — counting cannot tell them apart");
1233
1234 assert!(matches!(solve(&eqs_sat, cnf_sat.num_vars), XorOutcome::Sat(_)), "even total parity ⇒ SAT");
1236 match solve(&eqs_uns, cnf_uns.num_vars) {
1237 XorOutcome::Unsat(r) => assert!(crate::xorsat::is_refutation(&eqs_uns, cnf_uns.num_vars, &r), "odd total parity ⇒ UNSAT, certified by GF(2)"),
1238 XorOutcome::Sat(_) => panic!("odd-parity Tseitin must be UNSAT"),
1239 }
1240 }
1241
1242 #[test]
1243 fn php_has_the_expected_shape() {
1244 let (cnf, verdict) = php(4);
1245 assert_eq!(verdict, ExpectedVerdict::Unsat);
1246 assert_eq!(cnf.num_vars, 4 * 3);
1247 assert_eq!(cnf.clauses.len(), 4 + 3 * 6);
1249 }
1250
1251 #[test]
1252 fn php_is_unsatisfiable_for_small_n() {
1253 for n in 1..=5 {
1254 let (cnf, _) = php(n);
1255 assert_eq!(
1256 cnf.into_solver().solve(),
1257 SolveResult::Unsat,
1258 "PHP({n}) must be unsatisfiable"
1259 );
1260 }
1261 }
1262
1263 #[test]
1264 #[ignore = "core benchmark on hard random 3-SAT — the general-instance (non-symmetric) baseline"]
1265 fn bench_core_on_random_3sat() {
1266 use std::time::Instant;
1267 for &(vars, ratio) in &[(50usize, 4.26), (75, 4.26), (100, 4.26), (125, 4.26)] {
1268 let nc = (vars as f64 * ratio) as usize;
1269 let cnf = random_3sat(vars, nc, 0xC0FFEE_1234);
1270 let mut s = cnf.into_solver();
1271 let t = Instant::now();
1272 let res = s.solve();
1273 let ms = t.elapsed().as_secs_f64() * 1e3;
1274 println!(
1275 "rand3sat(v={vars}, c={nc}): {res:?} in {ms:.1}ms — {} conflicts, {} learned clauses (unbounded!)",
1276 s.conflicts(),
1277 s.learned().len()
1278 );
1279 }
1280 }
1281
1282 #[test]
1283 #[ignore = "A/B benchmark: LBD clause deletion ON vs OFF on hard random 3-SAT"]
1284 fn bench_lbd_reduction_ab() {
1285 use std::time::Instant;
1286 for &v in &[140usize, 160, 180, 200] {
1287 let nc = (v as f64 * 4.26) as usize;
1288 let cnf = random_3sat(v, nc, 0xBADC0DE_99);
1289 for on in [false, true] {
1290 let mut s = cnf.into_solver();
1291 s.set_reduce(on);
1292 let t = Instant::now();
1293 let res = s.solve();
1294 let ms = t.elapsed().as_secs_f64() * 1e3;
1295 println!(
1296 "v={v} reduce={on:5}: {} in {ms:7.0}ms — {} conflicts, {} LIVE learned clauses",
1297 if matches!(res, SolveResult::Sat(_)) { "SAT " } else { "UNSAT" },
1298 s.conflicts(),
1299 s.live_learned()
1300 );
1301 }
1302 }
1303 }
1304
1305 #[test]
1306 #[ignore = "parity grave-dance: dump expander-XOR CNF + time Gaussian (xorsat), pairs with Kissat loop"]
1307 fn dump_parity_and_time_xorsat() {
1308 use std::time::Instant;
1309 for n in [40usize, 60, 80, 100, 120] {
1310 let m = (n as f64 * 1.1) as usize;
1311 let (eqs, cnf) = parity_unsat(n, m, 0x9A2173_5C);
1312 std::fs::write(format!("/tmp/parity_{n}.cnf"), crate::dimacs::print(&cnf)).unwrap();
1313 let t = Instant::now();
1315 let outcome = crate::xorsat::solve(&eqs, n);
1316 let us = t.elapsed().as_secs_f64() * 1e6;
1317 assert!(matches!(outcome, crate::xorsat::XorOutcome::Unsat(_)), "parity(n={n}) must be UNSAT");
1318 println!(
1319 "XORSAT parity(n={n}, m={m}): UNSAT in {us:.1}µs via Gaussian elimination | {} CNF clauses dumped for Kissat",
1320 cnf.clauses.len()
1321 );
1322 }
1323 }
1324
1325 #[test]
1326 fn parity_unsat_is_genuinely_unsat() {
1327 let (eqs, cnf) = parity_unsat(12, 14, 0x9A2173_5C);
1329 assert!(matches!(crate::xorsat::solve(&eqs, 12), crate::xorsat::XorOutcome::Unsat(_)));
1330 assert_eq!(cnf.into_solver().solve(), SolveResult::Unsat, "CNF encoding is UNSAT too");
1331 }
1332
1333 #[test]
1334 fn clique_coloring_verdict_tracks_colors_vs_clique() {
1335 for n in 2..=4 {
1337 let (unsat, v_unsat) = clique_coloring(n, n - 1);
1338 assert_eq!(v_unsat, ExpectedVerdict::Unsat);
1339 assert_eq!(unsat.into_solver().solve(), SolveResult::Unsat, "K_{n} with {} colors", n - 1);
1340 let (sat, v_sat) = clique_coloring(n, n);
1341 assert_eq!(v_sat, ExpectedVerdict::Sat);
1342 assert!(matches!(sat.into_solver().solve(), SolveResult::Sat(_)), "K_{n} with {n} colors");
1343 }
1344 }
1345
1346 #[test]
1347 fn clique_coloring_exposes_color_permutation_symmetry() {
1348 let (cnf, _) = clique_coloring(3, 2);
1351 let gens = crate::symmetry_detect::find_generators(cnf.num_vars, &cnf.clauses);
1352 assert!(!gens.iter().all(|g| g.is_identity()), "color symmetry must be detected");
1353 for g in &gens {
1354 assert!(crate::symmetry_detect::perm_is_automorphism(&cnf.clauses, g));
1355 }
1356 }
1357
1358 #[test]
1359 fn clique_coloring_is_refuted_with_certified_symmetry_breaking() {
1360 for (n, k) in [(3usize, 2usize), (4, 3)] {
1361 let (cnf, _) = clique_coloring(n, k);
1362 let r = crate::sym_certify::certified_unsat_auto(cnf.num_vars, &cnf.clauses);
1363 assert!(r.refuted, "K_{n} / {k} colors refuted");
1364 assert!(r.sbp_clauses >= 1, "color symmetry certified-broken");
1365 assert!(crate::pr::check_pr_refutation(cnf.num_vars, &cnf.clauses, &r.steps));
1366 }
1367 }
1368
1369 fn decides(cnf: &DimacsCnf, verdict: ExpectedVerdict) {
1373 let solved = crate::solve::solve_structured(cnf.num_vars, &cnf.clauses);
1374 match verdict {
1375 ExpectedVerdict::Unsat => {
1376 assert!(matches!(solved.answer, crate::solve::Answer::Unsat), "expected UNSAT, solver said SAT");
1377 }
1378 ExpectedVerdict::Sat => match &solved.answer {
1379 crate::solve::Answer::Sat(model) => assert!(
1380 cnf.clauses.iter().all(|c| c.iter().any(|l| model[l.var() as usize] == l.is_positive())),
1381 "the returned SAT model must satisfy every clause"
1382 ),
1383 crate::solve::Answer::Unsat => panic!("expected SAT, solver said UNSAT"),
1384 },
1385 }
1386 }
1387
1388 #[test]
1389 fn weak_php_generalizes_php_and_tracks_the_pigeon_hole_ratio() {
1390 for n in 1..=5 {
1392 let (tight, tv) = weak_php(n, n.saturating_sub(1));
1393 let (base, bv) = php(n);
1394 assert_eq!(tight.num_vars, base.num_vars, "weak_php(n,n-1) shares PHP's variable count");
1395 assert_eq!(tight.clauses, base.clauses, "weak_php(n,n-1) is byte-identical to php(n)");
1396 assert_eq!(tv, bv);
1397 }
1398 for &(p, h) in &[(3usize, 2usize), (5, 4), (4, 7), (2, 3), (3, 3), (6, 2)] {
1400 let (cnf, v) = weak_php(p, h);
1401 assert_eq!(v, if p > h { ExpectedVerdict::Unsat } else { ExpectedVerdict::Sat }, "PHP^{h}_{p} verdict");
1402 decides(&cnf, v);
1403 }
1404 }
1405
1406 #[test]
1407 fn functional_and_onto_php_are_unsat_strengthenings_of_php() {
1408 for n in 2..=5 {
1409 let (base, _) = php(n);
1410 let (func, fv) = functional_php(n);
1411 let (onto, ov) = onto_php(n);
1412 assert_eq!(fv, ExpectedVerdict::Unsat);
1413 assert_eq!(ov, ExpectedVerdict::Unsat);
1414 assert!(func.clauses.starts_with(&base.clauses), "FPHP({n}) ⊇ PHP({n})");
1416 assert!(onto.clauses.starts_with(&func.clauses), "onto-FPHP({n}) ⊇ FPHP({n})");
1417 decides(&func, ExpectedVerdict::Unsat);
1418 decides(&onto, ExpectedVerdict::Unsat);
1419 }
1420 for n in 3..=5 {
1422 let (base, _) = php(n);
1423 let (func, _) = functional_php(n);
1424 let (onto, _) = onto_php(n);
1425 assert!(func.clauses.len() > base.clauses.len(), "FPHP adds functional clauses at n={n}");
1426 assert!(onto.clauses.len() > func.clauses.len(), "onto-FPHP adds surjectivity clauses at n={n}");
1427 }
1428 }
1429
1430 #[test]
1431 fn mod_counting_is_unsat_exactly_when_q_does_not_divide_n() {
1432 for &(n, q) in &[(3usize, 2usize), (4, 2), (5, 2), (6, 2), (4, 3), (6, 3), (7, 3), (8, 4)] {
1434 let (cnf, v) = mod_counting(n, q);
1435 assert_eq!(v, if n % q == 0 { ExpectedVerdict::Sat } else { ExpectedVerdict::Unsat }, "Count_{q}({n})");
1436 assert!(cnf.clauses.iter().all(|c| !c.is_empty()), "Count_{q}({n}) has no empty clause");
1437 decides(&cnf, v);
1438 }
1439 let (c52, _) = mod_counting(5, 2);
1441 assert_eq!(c52.num_vars, 10, "C(5,2) = 10 edges of K_5");
1442 }
1443
1444 #[test]
1445 fn ramsey_tracks_the_known_ramsey_numbers() {
1446 for &(s, t, n) in &[(3usize, 3usize, 5usize), (3, 3, 6), (3, 4, 8), (3, 4, 9)] {
1449 let (cnf, v) = ramsey(s, t, n);
1450 let r = ramsey_number(s, t).unwrap();
1451 assert_eq!(v, if n >= r { ExpectedVerdict::Unsat } else { ExpectedVerdict::Sat }, "Ramsey({s},{t};{n}) vs R={r}");
1452 decides(&cnf, v);
1453 }
1454 let (cnf, _) = ramsey(3, 3, 6);
1457 let key = |c: &Vec<Lit>, flip: bool| {
1458 let mut k: Vec<(u32, bool)> = c.iter().map(|l| (l.var(), l.is_positive() ^ flip)).collect();
1459 k.sort_unstable();
1460 k
1461 };
1462 let set: std::collections::HashSet<Vec<(u32, bool)>> = cnf.clauses.iter().map(|c| key(c, false)).collect();
1463 for c in &cnf.clauses {
1464 assert!(set.contains(&key(c, true)), "global colour-flip is an automorphism of diagonal Ramsey");
1465 }
1466 }
1467
1468 #[test]
1469 fn random_kxor_is_guaranteed_unsat_for_every_seed_arity_and_size() {
1470 use crate::cdcl::SolveResult;
1474 for k in [2usize, 3, 4, 5] {
1475 for n in [8usize, 12, 16] {
1476 for seed in 0..24u64 {
1477 let (eqs, cnf) = random_kxor(k, n, n, seed.wrapping_mul(0x1000193) ^ k as u64);
1478 assert!(
1479 matches!(crate::xorsat::solve(&eqs, cnf.num_vars), crate::xorsat::XorOutcome::Unsat(_)),
1480 "k={k} n={n} seed={seed}: maximal-rank k-XOR must be UNSAT over GF(2)"
1481 );
1482 assert_eq!(cnf.into_solver().solve(), SolveResult::Unsat, "k={k} n={n} seed={seed}: CNF must be UNSAT");
1483 }
1484 }
1485 }
1486 }
1487
1488 #[test]
1489 fn random_kxor_generalizes_parity_and_gaussian_refutes_every_arity() {
1490 for seed in [1u64, 7, 0x9A2173_5C] {
1492 let (_, a) = parity_unsat(12, 14, seed);
1493 let (_, b) = random_kxor(3, 12, 14, seed);
1494 assert_eq!(a.clauses, b.clauses, "parity_unsat is exactly random_kxor(k=3)");
1495 }
1496 for k in [2usize, 4, 5] {
1498 let (eqs, cnf) = random_kxor(k, 14, 16, 0xC0FFEE ^ k as u64);
1499 match crate::xorsat::solve(&eqs, cnf.num_vars) {
1500 crate::xorsat::XorOutcome::Unsat(r) => assert!(
1501 crate::xorsat::is_refutation(&eqs, cnf.num_vars, &r),
1502 "{k}-XOR Gaussian refutation must re-check"
1503 ),
1504 crate::xorsat::XorOutcome::Sat(_) => panic!("planted-then-flipped {k}-XOR must be UNSAT"),
1505 }
1506 assert_eq!(cnf.into_solver().solve(), SolveResult::Unsat, "{k}-XOR CNF encoding is UNSAT");
1507 }
1508 }
1509
1510 #[test]
1511 fn pebbling_pyramid_is_unsat_with_the_expected_triangular_shape() {
1512 for h in 1..=5 {
1513 let (cnf, v) = pebbling_pyramid(h);
1514 assert_eq!(v, ExpectedVerdict::Unsat);
1515 let nodes = (h + 1) * (h + 2) / 2;
1516 assert_eq!(cnf.num_vars, nodes, "pyramid of height {h} has T(h+1) nodes");
1517 assert_eq!(cnf.clauses.len(), nodes + 1, "pebbling({h}) clause count = nodes + 1");
1519 assert!(cnf.clauses.iter().all(|c| !c.is_empty()), "no empty clause");
1520 decides(&cnf, ExpectedVerdict::Unsat);
1521 }
1522 }
1523}