logicaffeine_proof/counting_principle.rs
1//! Fast refuter for the **modular counting principle** `Count_q(n)`: partition an `n`-element set into
2//! blocks of size `q`. Encoded as: one Boolean per `q`-subset (block); a coverage clause per element
3//! (the incident blocks, at least one chosen); and a disjointness clause for every pair of overlapping
4//! blocks. It is unsatisfiable exactly when `q ∤ n`.
5//!
6//! The refutation is a one-line counting argument: coverage + disjointness force each element into
7//! EXACTLY one chosen block, and every block covers exactly `q` elements, so summing over the elements
8//! gives `n = q · (#chosen blocks)` — impossible when `q ∤ n`. Recognizing the structure and checking
9//! `n mod q ≠ 0` is `O(clauses)`; the certificate is the triple `(n, q, n mod q)`.
10//!
11//! **Soundness:** the detector fires only when the clauses faithfully form `Count_q(n)` — every coverage
12//! clause's incident blocks are pairwise disjoint (a full at-most-one clique, so each element is covered
13//! at most once), every block covers exactly `q` elements, and `q ∤ n`. Then the counting argument makes
14//! the formula unsatisfiable. Any deviation returns `None`; never a false refutation.
15
16use crate::cdcl::Lit;
17use std::collections::{HashMap, HashSet};
18
19/// A modular-counting refutation: an `n`-element set cannot split into blocks of size `q` because
20/// `q ∤ n` (the remainder is non-zero). Re-checkable in O(1): `n % q == remainder != 0`.
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct CountingCert {
23 pub n: u64,
24 pub q: u64,
25 pub remainder: u64,
26}
27
28impl CountingCert {
29 /// Re-check from scratch: the certificate witnesses UNSAT iff `n mod q` is the stated non-zero
30 /// remainder.
31 pub fn check(&self) -> bool {
32 self.q >= 2 && self.remainder != 0 && self.n % self.q == self.remainder
33 }
34
35 pub fn byte_len(&self) -> usize {
36 24
37 }
38}
39
40/// Recover a `Count_q(n)` core from `clauses` and refute it by the `q ∤ n` counting argument, or `None`
41/// if there is no such core. Conservative / fail-closed — see the module docs.
42pub fn counting_certificate(num_vars: usize, clauses: &[Vec<Lit>]) -> Option<CountingCert> {
43 if num_vars < 2 {
44 return None;
45 }
46 let key2 = |x: u32, y: u32| if x < y { (x, y) } else { (y, x) };
47 // Coverage clauses = all-positive clauses (one per element, its incident blocks). Disjointness =
48 // binary all-negative clauses (overlapping blocks are mutually exclusive).
49 let mut coverage: Vec<Vec<u32>> = Vec::new();
50 let mut disjoint: HashSet<(u32, u32)> = HashSet::new();
51 for c in clauses {
52 if c.iter().all(|l| l.is_positive()) {
53 if c.is_empty() {
54 return None;
55 }
56 coverage.push(c.iter().map(|l| l.var()).collect());
57 } else if c.len() == 2 && !c[0].is_positive() && !c[1].is_positive() {
58 disjoint.insert(key2(c[0].var(), c[1].var()));
59 } else {
60 return None; // a clause that is neither coverage nor disjointness — not a clean Count_q
61 }
62 }
63 let n = coverage.len();
64 if n < 2 {
65 return None;
66 }
67 // Each element's incident blocks must be pairwise disjoint (a full at-most-one clique), so the
68 // element is covered at most once; with coverage that is exactly once.
69 let mut degree: HashMap<u32, usize> = HashMap::new();
70 for cov in &coverage {
71 let set: HashSet<u32> = cov.iter().copied().collect();
72 if set.len() != cov.len() {
73 return None; // a repeated block in one coverage clause
74 }
75 for i in 0..cov.len() {
76 *degree.entry(cov[i]).or_insert(0) += 1;
77 for j in (i + 1)..cov.len() {
78 if !disjoint.contains(&key2(cov[i], cov[j])) {
79 return None; // two blocks share this element but are not mutually exclusive
80 }
81 }
82 }
83 }
84 // Every block must cover the SAME number of elements q ≥ 2 (a uniform block size).
85 let q = *degree.values().next()?;
86 if q < 2 || degree.values().any(|&d| d != q) {
87 return None;
88 }
89 let (n, q) = (n as u64, q as u64);
90 let remainder = n % q;
91 if remainder == 0 {
92 return None; // q | n — a partition can exist, so this is not a refutation
93 }
94 Some(CountingCert { n, q, remainder })
95}
96
97/// Refute a `Count_q(n)` core (`q ∤ n`). `true` iff a certificate is recovered. Never a false refutation.
98pub fn refute_counting(num_vars: usize, clauses: &[Vec<Lit>]) -> bool {
99 counting_certificate(num_vars, clauses).is_some_and(|c| c.check())
100}