1use crate::cdcl::Lit;
27use std::collections::BTreeMap;
28
29const MAX_VARS: usize = 6;
33const ROW_CAP: usize = 20_000;
37const MAG_CAP: i128 = 1 << 50;
39
40#[derive(Clone, Default)]
42struct Poly {
43 c: i64,
44 lin: BTreeMap<usize, i64>,
45 quad: BTreeMap<(usize, usize), i64>,
46}
47
48impl Poly {
49 fn constant(c: i64) -> Self {
50 Poly { c, ..Default::default() }
51 }
52 fn x(i: usize) -> Self {
53 Poly { lin: BTreeMap::from([(i, 1)]), ..Default::default() }
54 }
55 fn add(&self, o: &Self) -> Self {
56 let mut r = self.clone();
57 r.c += o.c;
58 for (&k, &v) in &o.lin {
59 *r.lin.entry(k).or_insert(0) += v;
60 }
61 for (&k, &v) in &o.quad {
62 *r.quad.entry(k).or_insert(0) += v;
63 }
64 r.prune()
65 }
66 fn neg(&self) -> Self {
67 Poly {
68 c: -self.c,
69 lin: self.lin.iter().map(|(&k, &v)| (k, -v)).collect(),
70 quad: self.quad.iter().map(|(&k, &v)| (k, -v)).collect(),
71 }
72 }
73 fn sub(&self, o: &Self) -> Self {
74 self.add(&o.neg())
75 }
76 fn prune(mut self) -> Self {
77 self.lin.retain(|_, v| *v != 0);
78 self.quad.retain(|_, v| *v != 0);
79 self
80 }
81 fn mul_linear(a: &Self, b: &Self) -> Self {
83 debug_assert!(a.quad.is_empty() && b.quad.is_empty(), "mul_linear needs degree-1 inputs");
84 let mut r = Poly::constant(a.c * b.c);
85 for (&i, &ai) in &a.lin {
86 *r.lin.entry(i).or_insert(0) += ai * b.c;
87 }
88 for (&j, &bj) in &b.lin {
89 *r.lin.entry(j).or_insert(0) += a.c * bj;
90 }
91 for (&i, &ai) in &a.lin {
92 for (&j, &bj) in &b.lin {
93 if i == j {
94 *r.lin.entry(i).or_insert(0) += ai * bj; } else {
96 *r.quad.entry((i.min(j), i.max(j))).or_insert(0) += ai * bj;
97 }
98 }
99 }
100 r.prune()
101 }
102}
103
104fn lit_value(l: &Lit) -> Poly {
106 let x = Poly::x(l.var() as usize);
107 if l.is_positive() {
108 x
109 } else {
110 Poly::constant(1).sub(&x)
111 }
112}
113
114#[derive(Clone)]
117struct Row {
118 coeffs: BTreeMap<usize, i128>,
119 constant: i128,
120 prov: BTreeMap<usize, i128>,
121}
122
123impl Row {
124 fn combine(&self, ka: i128, other: &Row, kb: i128) -> Option<Row> {
126 let mut coeffs = BTreeMap::new();
127 let mut acc = |dst: &mut BTreeMap<usize, i128>, src: &BTreeMap<usize, i128>, k: i128| -> Option<()> {
128 for (&v, &c) in src {
129 let e = dst.entry(v).or_insert(0);
130 *e = e.checked_add(c.checked_mul(k)?)?;
131 if e.abs() > MAG_CAP {
132 return None;
133 }
134 }
135 Some(())
136 };
137 acc(&mut coeffs, &self.coeffs, ka)?;
138 acc(&mut coeffs, &other.coeffs, kb)?;
139 coeffs.retain(|_, c| *c != 0);
140 let constant = self.constant.checked_mul(ka)?.checked_add(other.constant.checked_mul(kb)?)?;
141 if constant.abs() > MAG_CAP {
142 return None;
143 }
144 let mut prov = BTreeMap::new();
145 acc(&mut prov, &self.prov, ka)?;
146 acc(&mut prov, &other.prov, kb)?;
147 prov.retain(|_, c| *c != 0);
148 Some(Row { coeffs, constant, prov })
149 }
150}
151
152fn poly_to_row(p: &Poly, num_vars: usize, idx: usize) -> Row {
155 let mut coeffs = BTreeMap::new();
156 for (&i, &v) in &p.lin {
157 coeffs.insert(i, v as i128);
158 }
159 for (&(i, j), &v) in &p.quad {
160 coeffs.insert(num_vars + i * num_vars + j, v as i128); }
162 Row { coeffs, constant: p.c as i128, prov: BTreeMap::from([(idx, 1)]) }
163}
164
165fn ge0(out: &mut Vec<Poly>, p: Poly) {
167 out.push(p.neg());
168}
169
170fn lift_polys(num_vars: usize, clauses: &[Vec<Lit>]) -> Vec<Poly> {
172 let mut out: Vec<Poly> = Vec::new();
173 let one = Poly::constant(1);
174 for i in 0..num_vars {
175 ge0(&mut out, Poly::x(i));
176 ge0(&mut out, one.sub(&Poly::x(i)));
177 }
178 for i in 0..num_vars {
179 for j in (i + 1)..num_vars {
180 let z = Poly { quad: BTreeMap::from([((i, j), 1)]), ..Default::default() };
181 let (xi, xj) = (Poly::x(i), Poly::x(j));
182 ge0(&mut out, z.clone());
183 ge0(&mut out, xi.sub(&z));
184 ge0(&mut out, xj.sub(&z));
185 ge0(&mut out, z.sub(&xi).sub(&xj).add(&one));
186 ge0(&mut out, xi.add(&xj).sub(&z).sub(&z)); ge0(&mut out, one.sub(&xi).sub(&xj).add(&z).add(&z)); }
189 }
190 for c in clauses {
191 if c.is_empty() {
192 out.push(Poly::constant(1)); continue;
194 }
195 let mut cl = Poly::constant(-1);
196 for l in c {
197 cl = cl.add(&lit_value(l));
198 }
199 ge0(&mut out, cl.clone());
200 for j in 0..num_vars {
201 ge0(&mut out, Poly::mul_linear(&cl, &Poly::x(j)));
202 ge0(&mut out, Poly::mul_linear(&cl, &one.sub(&Poly::x(j))));
203 }
204 }
205 out
206}
207
208fn farkas_refute(rows: Vec<Row>, var_count: usize) -> Option<BTreeMap<usize, i128>> {
213 let contradiction = |rows: &[Row]| -> Option<BTreeMap<usize, i128>> {
214 rows.iter().find(|r| r.coeffs.is_empty() && r.constant > 0).map(|r| r.prov.clone())
215 };
216 let mut rows = rows;
217 if let Some(p) = contradiction(&rows) {
218 return Some(p);
219 }
220 for v in 0..var_count {
221 let (mut pos, mut neg, mut next): (Vec<&Row>, Vec<&Row>, Vec<Row>) =
222 (Vec::new(), Vec::new(), Vec::new());
223 for r in &rows {
224 match r.coeffs.get(&v).copied().unwrap_or(0) {
225 c if c > 0 => pos.push(r),
226 c if c < 0 => neg.push(r),
227 _ => next.push(r.clone()),
228 }
229 }
230 for p in &pos {
231 for n in &neg {
232 let pc = p.coeffs[&v];
233 let nc = -n.coeffs[&v];
234 let combined = p.combine(nc, n, pc)?; next.push(combined);
236 if next.len() > ROW_CAP {
237 return None; }
239 }
240 }
241 rows = next;
242 if let Some(p) = contradiction(&rows) {
243 return Some(p);
244 }
245 }
246 contradiction(&rows)
247}
248
249pub fn sos_refutes(num_vars: usize, clauses: &[Vec<Lit>]) -> bool {
253 sos_certificate(num_vars, clauses).is_some()
254}
255
256pub fn sos_certificate(num_vars: usize, clauses: &[Vec<Lit>]) -> Option<BTreeMap<usize, i128>> {
260 if num_vars > MAX_VARS {
261 return None;
262 }
263 let polys = lift_polys(num_vars, clauses);
264 let rows: Vec<Row> =
265 polys.iter().enumerate().map(|(i, p)| poly_to_row(p, num_vars, i)).collect();
266 let var_count = num_vars + num_vars * num_vars; farkas_refute(rows, var_count)
268}
269
270pub fn check_sos_certificate(num_vars: usize, clauses: &[Vec<Lit>], cert: &BTreeMap<usize, i128>) -> bool {
278 if cert.is_empty() {
279 return false;
280 }
281 let polys = lift_polys(num_vars, clauses);
282 let (mut lin, mut quad) = (BTreeMap::<usize, i128>::new(), BTreeMap::<(usize, usize), i128>::new());
283 let mut constant: i128 = 0;
284 for (&i, &mult) in cert {
285 if mult < 0 || i >= polys.len() {
286 return false; }
288 let p = &polys[i];
289 constant += mult * p.c as i128;
290 for (&v, &c) in &p.lin {
291 *lin.entry(v).or_insert(0) += mult * c as i128;
292 }
293 for (&k, &c) in &p.quad {
294 *quad.entry(k).or_insert(0) += mult * c as i128;
295 }
296 }
297 lin.values().all(|&c| c == 0) && quad.values().all(|&c| c == 0) && constant > 0
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 fn sat(num_vars: usize, clauses: &[Vec<Lit>]) -> bool {
305 (0u64..(1u64 << num_vars)).any(|x| {
306 clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
307 })
308 }
309
310 fn splitmix(s: &mut u64) -> u64 {
311 *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
312 let mut z = *s;
313 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
314 z ^ (z >> 31)
315 }
316
317 fn certificate_is_valid(num_vars: usize, clauses: &[Vec<Lit>], cert: &BTreeMap<usize, i128>) -> bool {
321 check_sos_certificate(num_vars, clauses, cert)
322 }
323
324 #[test]
328 fn sos_refutation_is_sound_against_brute_force() {
329 let mut state = 0x5050_0001u64;
330 for _ in 0..400 {
331 let nv = 2 + (splitmix(&mut state) % 3) as usize; let m = 1 + (splitmix(&mut state) % 8) as usize;
333 let mut cl: Vec<Vec<Lit>> = Vec::new();
334 for _ in 0..m {
335 let mut c = Vec::new();
336 for v in 0..nv {
337 if splitmix(&mut state) % 3 == 0 {
338 c.push(Lit::new(v as u32, splitmix(&mut state) % 2 == 0));
339 }
340 }
341 if !c.is_empty() {
342 cl.push(c);
343 }
344 }
345 if cl.is_empty() {
346 continue;
347 }
348 if let Some(cert) = sos_certificate(nv, &cl) {
349 assert!(!sat(nv, &cl), "SoS refuted a SATISFIABLE formula: {cl:?}");
350 assert!(certificate_is_valid(nv, &cl, &cert), "the Farkas certificate must re-check: {cl:?}");
351 }
352 }
353 }
354
355 #[test]
357 fn satisfiable_formulas_are_never_refuted() {
358 let cl = vec![
359 vec![Lit::new(0, true), Lit::new(1, true)],
360 vec![Lit::new(0, false), Lit::new(2, true)],
361 ];
362 assert!(sat(3, &cl));
363 assert!(!sos_refutes(3, &cl), "a SAT formula must not be refuted");
364 }
365
366 #[test]
372 fn sos_closes_an_integrality_gap_that_is_linear_feasible() {
373 let cl = vec![
374 vec![Lit::new(0, true), Lit::new(1, false)],
375 vec![Lit::new(0, false), Lit::new(1, true)],
376 vec![Lit::new(0, true), Lit::new(1, true)],
377 vec![Lit::new(0, false), Lit::new(1, false)],
378 ];
379 assert!(!sat(2, &cl), "x=y ∧ x≠y is UNSAT");
380 let cert = sos_certificate(2, &cl).expect("degree-2 SoS closes the x=y=½ integrality gap");
381 assert!(certificate_is_valid(2, &cl, &cert), "and its certificate re-checks");
382 }
383
384 #[test]
388 fn the_degree_2_lift_refutes_where_the_linear_relaxation_cannot() {
389 let cl = vec![
390 vec![Lit::new(0, true), Lit::new(1, false)],
391 vec![Lit::new(0, false), Lit::new(1, true)],
392 vec![Lit::new(0, true), Lit::new(1, true)],
393 vec![Lit::new(0, false), Lit::new(1, false)],
394 ];
395 let one = Poly::constant(1);
397 let mut polys: Vec<Poly> = Vec::new();
398 for i in 0..2 {
399 polys.push(Poly::x(i).neg());
400 polys.push(one.sub(&Poly::x(i)).neg());
401 }
402 for c in &cl {
403 let mut p = Poly::constant(-1);
404 for l in c {
405 p = p.add(&lit_value(l));
406 }
407 polys.push(p.neg());
408 }
409 let rows: Vec<Row> = polys.iter().enumerate().map(|(i, p)| poly_to_row(p, 2, i)).collect();
410 assert!(farkas_refute(rows, 2 + 4).is_none(), "the degree-1 relaxation is feasible (x=y=½)");
411 assert!(sos_refutes(2, &cl), "but the degree-2 lift refutes it");
412 }
413}