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}