Skip to main content

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}