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