Skip to main content

logicaffeine_proof/
ordering.rs

1//! The **ordering-principle specialist** — a polynomial-time recognizer + refuter for GT(n), the
2//! linear-ordering contradiction. GT(n) asserts a strict total order (totality + antisymmetry +
3//! transitivity) in which every element has a strictly greater one ("no maximum"). That is impossible:
4//! a finite strict total order always has a maximum. So a complete GT(n) core is unsatisfiable, and any
5//! formula containing it (a superset of an UNSAT core) is UNSAT.
6//!
7//! The general cascade decides GT(n) only by super-polynomial search (measured: GT(20) ≈ 2.7s, ~68k
8//! conflicts, growing ~10x every 4 steps). This module recognizes the structure directly and certifies
9//! UNSAT from it — polynomial, and instant where search walls.
10//!
11//! **Soundness** is by faithful, conservative recognition, in the style of [`crate::pigeonhole`]: the
12//! recognizer verifies, for a single consistent element/edge identification, that ALL of totality,
13//! antisymmetry, transitivity (every ordered triple), and the no-maximum clause (every element) are
14//! present. Only then is a complete GT(n) core certified present — and it is genuinely UNSAT. Any
15//! missing, ambiguous, or extra-shaped piece makes the recognizer return `None` (never a false
16//! refutation); the caller then falls through to the general engine. Full transitivity is essential:
17//! without it a "no-maximum tournament" can be a satisfiable cycle (e.g. 0<1<2<0), so an incomplete
18//! structure must not be refuted.
19//!
20//! The refutation object is an [`OrderingCert`]: the element/edge identification, which a checker
21//! re-verifies against the raw clauses ([`check_ordering_cert`]) with zero trust in how it was produced.
22
23use crate::cdcl::Lit;
24use std::collections::{HashMap, HashSet};
25
26/// A re-checkable ordering-principle refutation: the recovered element/edge identification of a complete
27/// GT(n) core. `edge[i * n + j]` is the comparison variable `x_ij` ("i < j") for the ordered pair
28/// `(i, j)`; diagonal entries (`i == j`) are [`u32::MAX`] and unused. Given this map a checker
29/// re-verifies that every totality, antisymmetry, transitivity, and no-maximum clause is present — the
30/// whole certificate, no trust in the recognizer.
31#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct OrderingCert {
33    /// The number of ordered elements.
34    pub n: usize,
35    /// The comparison variable `x_ij` at flat index `i * n + j` (diagonal = `u32::MAX`).
36    pub edge: Vec<u32>,
37}
38
39impl OrderingCert {
40    fn get(&self, i: usize, j: usize) -> u32 {
41        self.edge[i * self.n + j]
42    }
43
44    /// The serialized size in bytes: the element count plus one 32-bit variable id per off-diagonal
45    /// ordered pair — the `O(n²)` identification a checker consumes.
46    pub fn byte_len(&self) -> usize {
47        8 + self.n * (self.n - 1) * 4
48    }
49}
50
51/// Recover a complete ordering-principle (GT(n)) core from `clauses`, or `None` if there is none. On
52/// `Some(cert)` the formula is unsatisfiable (a finite strict total order has a maximum, contradicting
53/// the no-maximum clauses), and `cert` re-checks via [`check_ordering_cert`]. Conservative / fail-closed
54/// — never a false certificate. See the module docs.
55pub fn ordering_certificate(num_vars: usize, clauses: &[Vec<Lit>]) -> Option<OrderingCert> {
56    if num_vars < 2 {
57        return None;
58    }
59    let key2 = |x: u32, y: u32| if x < y { (x, y) } else { (y, x) };
60
61    // ---- 1. Index clauses by shape. ----
62    // {a,b} both positive → totality candidate; {a,b} both negative → antisymmetry candidate;
63    // [¬a, ¬b, c] → transitivity; any all-positive clause → no-maximum candidate.
64    let mut pos2: HashSet<(u32, u32)> = HashSet::new();
65    let mut neg2: HashSet<(u32, u32)> = HashSet::new();
66    let mut trans: HashSet<(u32, u32, u32)> = HashSet::new(); // (a, b, c) for the clause ¬a ∨ ¬b ∨ c
67    let mut positive_clauses: Vec<Vec<u32>> = Vec::new();
68    for c in clauses {
69        if c.iter().all(|l| l.is_positive()) {
70            positive_clauses.push(c.iter().map(|l| l.var()).collect());
71        }
72        match c.len() {
73            2 => match (c[0].is_positive(), c[1].is_positive()) {
74                (true, true) => {
75                    pos2.insert(key2(c[0].var(), c[1].var()));
76                }
77                (false, false) => {
78                    neg2.insert(key2(c[0].var(), c[1].var()));
79                }
80                _ => {}
81            },
82            3 => {
83                let negs: Vec<u32> = c.iter().filter(|l| !l.is_positive()).map(|l| l.var()).collect();
84                let poss: Vec<u32> = c.iter().filter(|l| l.is_positive()).map(|l| l.var()).collect();
85                if negs.len() == 2 && poss.len() == 1 {
86                    trans.insert((negs[0], negs[1], poss[0]));
87                    trans.insert((negs[1], negs[0], poss[0]));
88                }
89            }
90            _ => {}
91        }
92    }
93
94    // ---- 2. Comparison pairs: {a,b} carrying BOTH totality and antisymmetry (the two directions of one
95    // element pair). Every comparison variable must belong to exactly one such pair. ----
96    let comparisons: Vec<(u32, u32)> = pos2.intersection(&neg2).copied().collect();
97    if comparisons.len() < 2 {
98        return None;
99    }
100    let mut partner: HashMap<u32, u32> = HashMap::new();
101    for &(a, b) in &comparisons {
102        if partner.insert(a, b).is_some() || partner.insert(b, a).is_some() {
103            return None; // a variable in two comparison pairs — not a clean ordering encoding
104        }
105    }
106    let comparison_vars: HashSet<u32> = partner.keys().copied().collect();
107
108    // ---- 3. No-maximum clauses partition the comparison variables into per-element OUTGOING groups. A
109    // no-maximum clause for element i is {x_ij : j≠i}: all positive comparison variables, pairwise
110    // NON-partners. A totality clause [x_ij, x_ji] is positive too, but its vars ARE partners. ----
111    let mut groups: Vec<Vec<u32>> = Vec::new();
112    let mut group_of: HashMap<u32, usize> = HashMap::new();
113    for pc in &positive_clauses {
114        if pc.is_empty() || !pc.iter().all(|v| comparison_vars.contains(v)) {
115            continue;
116        }
117        let set: HashSet<u32> = pc.iter().copied().collect();
118        if set.len() != pc.len() {
119            continue; // a repeated variable
120        }
121        if pc.iter().any(|v| set.contains(&partner[v])) {
122            continue; // contains a comparison pair → a totality clause, not a no-maximum clause
123        }
124        if pc.iter().any(|v| group_of.contains_key(v)) {
125            continue; // overlaps an already-claimed group — require a clean partition
126        }
127        let gid = groups.len();
128        for &v in pc {
129            group_of.insert(v, gid);
130        }
131        groups.push(pc.clone());
132    }
133    let n = groups.len();
134    if n < 2
135        || group_of.len() != comparison_vars.len()
136        || comparison_vars.len() != n * (n - 1)
137        || groups.iter().any(|g| g.len() != n - 1)
138    {
139        return None;
140    }
141
142    // ---- 4. Directed edge map: edge[(i,j)] = x_ij. For a comparison pair {a,b}, a lies in group i and
143    // b in group j, so a is the i→j edge and b the j→i edge. ----
144    let mut edge_map: HashMap<(usize, usize), u32> = HashMap::new();
145    for (&a, &b) in &partner {
146        let (i, j) = (group_of[&a], group_of[&b]);
147        if i == j || edge_map.insert((i, j), a).is_some() {
148            return None; // partner within one element, or two edges for the same ordered pair
149        }
150    }
151    if edge_map.len() != n * (n - 1) {
152        return None;
153    }
154    let mut edge = vec![u32::MAX; n * n];
155    for i in 0..n {
156        for j in 0..n {
157            if i == j {
158                continue;
159            }
160            match edge_map.get(&(i, j)) {
161                Some(&v) => edge[i * n + j] = v,
162                None => return None, // a missing directed comparison — not a complete tournament
163            }
164        }
165    }
166    // each group must be EXACTLY element i's outgoing set {edge[(i,j)] : j≠i}.
167    for (i, g) in groups.iter().enumerate() {
168        let expected: HashSet<u32> = (0..n).filter(|&j| j != i).map(|j| edge[i * n + j]).collect();
169        if expected != g.iter().copied().collect() {
170            return None;
171        }
172    }
173
174    // ---- 5. Verify COMPLETE transitivity: every ordered distinct triple (i,j,k) has ¬x_ij ∨ ¬x_jk ∨
175    // x_ik. Essential for soundness — a tournament missing transitivity can be a satisfiable cycle. ----
176    for i in 0..n {
177        for j in 0..n {
178            if j == i {
179                continue;
180            }
181            for k in 0..n {
182                if k == i || k == j {
183                    continue;
184                }
185                if !trans.contains(&(edge[i * n + j], edge[j * n + k], edge[i * n + k])) {
186                    return None;
187                }
188            }
189        }
190    }
191
192    Some(OrderingCert { n, edge })
193}
194
195/// Re-check an [`OrderingCert`] against the raw `clauses` from scratch, trusting nothing about how it was
196/// produced: verify that for its element/edge identification EVERY totality, antisymmetry, transitivity,
197/// and no-maximum clause is present. `true` iff the certificate genuinely witnesses a complete GT(n)
198/// core (which is unsatisfiable), so the formula is UNSAT.
199pub fn check_ordering_cert(cert: &OrderingCert, clauses: &[Vec<Lit>]) -> bool {
200    let n = cert.n;
201    if n < 2 || cert.edge.len() != n * n {
202        return false;
203    }
204    // every off-diagonal edge is a real variable, and the n·(n−1) of them are distinct.
205    let mut seen = HashSet::new();
206    for i in 0..n {
207        for j in 0..n {
208            if i == j {
209                continue;
210            }
211            let v = cert.get(i, j);
212            if v == u32::MAX || !seen.insert(v) {
213                return false;
214            }
215        }
216    }
217    // build clause lookups.
218    let key2 = |x: u32, y: u32| if x < y { (x, y) } else { (y, x) };
219    let mut pos2 = HashSet::new();
220    let mut neg2 = HashSet::new();
221    let mut trans = HashSet::new();
222    let mut positive: HashSet<Vec<u32>> = HashSet::new();
223    for c in clauses {
224        if c.iter().all(|l| l.is_positive()) {
225            let mut vs: Vec<u32> = c.iter().map(|l| l.var()).collect();
226            vs.sort_unstable();
227            positive.insert(vs);
228        }
229        match c.len() {
230            2 => match (c[0].is_positive(), c[1].is_positive()) {
231                (true, true) => {
232                    pos2.insert(key2(c[0].var(), c[1].var()));
233                }
234                (false, false) => {
235                    neg2.insert(key2(c[0].var(), c[1].var()));
236                }
237                _ => {}
238            },
239            3 => {
240                let negs: Vec<u32> = c.iter().filter(|l| !l.is_positive()).map(|l| l.var()).collect();
241                let poss: Vec<u32> = c.iter().filter(|l| l.is_positive()).map(|l| l.var()).collect();
242                if negs.len() == 2 && poss.len() == 1 {
243                    trans.insert((negs[0], negs[1], poss[0]));
244                }
245            }
246            _ => {}
247        }
248    }
249    // totality + antisymmetry for every unordered pair.
250    for i in 0..n {
251        for j in (i + 1)..n {
252            let (a, b) = (cert.get(i, j), cert.get(j, i));
253            if !pos2.contains(&key2(a, b)) || !neg2.contains(&key2(a, b)) {
254                return false;
255            }
256        }
257    }
258    // transitivity for every ordered distinct triple.
259    for i in 0..n {
260        for j in 0..n {
261            if j == i {
262                continue;
263            }
264            for k in 0..n {
265                if k == i || k == j {
266                    continue;
267                }
268                let want = (cert.get(i, j), cert.get(j, k), cert.get(i, k));
269                let alt = (cert.get(j, k), cert.get(i, j), cert.get(i, k));
270                if !trans.contains(&want) && !trans.contains(&alt) {
271                    return false;
272                }
273            }
274        }
275    }
276    // no-maximum clause for every element (its complete outgoing set, in sorted order).
277    for i in 0..n {
278        let mut out: Vec<u32> = (0..n).filter(|&j| j != i).map(|j| cert.get(i, j)).collect();
279        out.sort_unstable();
280        if !positive.contains(&out) {
281            return false;
282        }
283    }
284    true
285}
286
287/// Refute a formula that contains a complete ordering-principle core. `true` iff a certificate is
288/// recovered — see [`ordering_certificate`]. Never a false refutation.
289pub fn refute_ordering(num_vars: usize, clauses: &[Vec<Lit>]) -> bool {
290    ordering_certificate(num_vars, clauses).is_some()
291}
292
293/// The ordering cut over a `ProofExpr`: clausify and run the specialist. This is the entry the cascade
294/// ([`crate::sat::prove_unsat`]) calls.
295pub fn refutes_ordering_principle(e: &crate::ProofExpr) -> bool {
296    let mut cnf = crate::cnf::Cnf::new();
297    if cnf.assert(e).is_none() {
298        return false;
299    }
300    refute_ordering(cnf.num_vars(), cnf.clauses())
301}