use crate::cdcl::Lit;
use std::collections::BTreeMap;
const MAX_VARS: usize = 6;
const ROW_CAP: usize = 20_000;
const MAG_CAP: i128 = 1 << 50;
#[derive(Clone, Default)]
struct Poly {
c: i64,
lin: BTreeMap<usize, i64>,
quad: BTreeMap<(usize, usize), i64>,
}
impl Poly {
fn constant(c: i64) -> Self {
Poly { c, ..Default::default() }
}
fn x(i: usize) -> Self {
Poly { lin: BTreeMap::from([(i, 1)]), ..Default::default() }
}
fn add(&self, o: &Self) -> Self {
let mut r = self.clone();
r.c += o.c;
for (&k, &v) in &o.lin {
*r.lin.entry(k).or_insert(0) += v;
}
for (&k, &v) in &o.quad {
*r.quad.entry(k).or_insert(0) += v;
}
r.prune()
}
fn neg(&self) -> Self {
Poly {
c: -self.c,
lin: self.lin.iter().map(|(&k, &v)| (k, -v)).collect(),
quad: self.quad.iter().map(|(&k, &v)| (k, -v)).collect(),
}
}
fn sub(&self, o: &Self) -> Self {
self.add(&o.neg())
}
fn prune(mut self) -> Self {
self.lin.retain(|_, v| *v != 0);
self.quad.retain(|_, v| *v != 0);
self
}
fn mul_linear(a: &Self, b: &Self) -> Self {
debug_assert!(a.quad.is_empty() && b.quad.is_empty(), "mul_linear needs degree-1 inputs");
let mut r = Poly::constant(a.c * b.c);
for (&i, &ai) in &a.lin {
*r.lin.entry(i).or_insert(0) += ai * b.c;
}
for (&j, &bj) in &b.lin {
*r.lin.entry(j).or_insert(0) += a.c * bj;
}
for (&i, &ai) in &a.lin {
for (&j, &bj) in &b.lin {
if i == j {
*r.lin.entry(i).or_insert(0) += ai * bj; } else {
*r.quad.entry((i.min(j), i.max(j))).or_insert(0) += ai * bj;
}
}
}
r.prune()
}
}
fn lit_value(l: &Lit) -> Poly {
let x = Poly::x(l.var() as usize);
if l.is_positive() {
x
} else {
Poly::constant(1).sub(&x)
}
}
#[derive(Clone)]
struct Row {
coeffs: BTreeMap<usize, i128>,
constant: i128,
prov: BTreeMap<usize, i128>,
}
impl Row {
fn combine(&self, ka: i128, other: &Row, kb: i128) -> Option<Row> {
let mut coeffs = BTreeMap::new();
let mut acc = |dst: &mut BTreeMap<usize, i128>, src: &BTreeMap<usize, i128>, k: i128| -> Option<()> {
for (&v, &c) in src {
let e = dst.entry(v).or_insert(0);
*e = e.checked_add(c.checked_mul(k)?)?;
if e.abs() > MAG_CAP {
return None;
}
}
Some(())
};
acc(&mut coeffs, &self.coeffs, ka)?;
acc(&mut coeffs, &other.coeffs, kb)?;
coeffs.retain(|_, c| *c != 0);
let constant = self.constant.checked_mul(ka)?.checked_add(other.constant.checked_mul(kb)?)?;
if constant.abs() > MAG_CAP {
return None;
}
let mut prov = BTreeMap::new();
acc(&mut prov, &self.prov, ka)?;
acc(&mut prov, &other.prov, kb)?;
prov.retain(|_, c| *c != 0);
Some(Row { coeffs, constant, prov })
}
}
fn poly_to_row(p: &Poly, num_vars: usize, idx: usize) -> Row {
let mut coeffs = BTreeMap::new();
for (&i, &v) in &p.lin {
coeffs.insert(i, v as i128);
}
for (&(i, j), &v) in &p.quad {
coeffs.insert(num_vars + i * num_vars + j, v as i128); }
Row { coeffs, constant: p.c as i128, prov: BTreeMap::from([(idx, 1)]) }
}
fn ge0(out: &mut Vec<Poly>, p: Poly) {
out.push(p.neg());
}
fn lift_polys(num_vars: usize, clauses: &[Vec<Lit>]) -> Vec<Poly> {
let mut out: Vec<Poly> = Vec::new();
let one = Poly::constant(1);
for i in 0..num_vars {
ge0(&mut out, Poly::x(i));
ge0(&mut out, one.sub(&Poly::x(i)));
}
for i in 0..num_vars {
for j in (i + 1)..num_vars {
let z = Poly { quad: BTreeMap::from([((i, j), 1)]), ..Default::default() };
let (xi, xj) = (Poly::x(i), Poly::x(j));
ge0(&mut out, z.clone());
ge0(&mut out, xi.sub(&z));
ge0(&mut out, xj.sub(&z));
ge0(&mut out, z.sub(&xi).sub(&xj).add(&one));
ge0(&mut out, xi.add(&xj).sub(&z).sub(&z)); ge0(&mut out, one.sub(&xi).sub(&xj).add(&z).add(&z)); }
}
for c in clauses {
if c.is_empty() {
out.push(Poly::constant(1)); continue;
}
let mut cl = Poly::constant(-1);
for l in c {
cl = cl.add(&lit_value(l));
}
ge0(&mut out, cl.clone());
for j in 0..num_vars {
ge0(&mut out, Poly::mul_linear(&cl, &Poly::x(j)));
ge0(&mut out, Poly::mul_linear(&cl, &one.sub(&Poly::x(j))));
}
}
out
}
fn farkas_refute(rows: Vec<Row>, var_count: usize) -> Option<BTreeMap<usize, i128>> {
let contradiction = |rows: &[Row]| -> Option<BTreeMap<usize, i128>> {
rows.iter().find(|r| r.coeffs.is_empty() && r.constant > 0).map(|r| r.prov.clone())
};
let mut rows = rows;
if let Some(p) = contradiction(&rows) {
return Some(p);
}
for v in 0..var_count {
let (mut pos, mut neg, mut next): (Vec<&Row>, Vec<&Row>, Vec<Row>) =
(Vec::new(), Vec::new(), Vec::new());
for r in &rows {
match r.coeffs.get(&v).copied().unwrap_or(0) {
c if c > 0 => pos.push(r),
c if c < 0 => neg.push(r),
_ => next.push(r.clone()),
}
}
for p in &pos {
for n in &neg {
let pc = p.coeffs[&v];
let nc = -n.coeffs[&v];
let combined = p.combine(nc, n, pc)?; next.push(combined);
if next.len() > ROW_CAP {
return None; }
}
}
rows = next;
if let Some(p) = contradiction(&rows) {
return Some(p);
}
}
contradiction(&rows)
}
pub fn sos_refutes(num_vars: usize, clauses: &[Vec<Lit>]) -> bool {
sos_certificate(num_vars, clauses).is_some()
}
pub fn sos_certificate(num_vars: usize, clauses: &[Vec<Lit>]) -> Option<BTreeMap<usize, i128>> {
if num_vars > MAX_VARS {
return None;
}
let polys = lift_polys(num_vars, clauses);
let rows: Vec<Row> =
polys.iter().enumerate().map(|(i, p)| poly_to_row(p, num_vars, i)).collect();
let var_count = num_vars + num_vars * num_vars; farkas_refute(rows, var_count)
}
pub fn check_sos_certificate(num_vars: usize, clauses: &[Vec<Lit>], cert: &BTreeMap<usize, i128>) -> bool {
if cert.is_empty() {
return false;
}
let polys = lift_polys(num_vars, clauses);
let (mut lin, mut quad) = (BTreeMap::<usize, i128>::new(), BTreeMap::<(usize, usize), i128>::new());
let mut constant: i128 = 0;
for (&i, &mult) in cert {
if mult < 0 || i >= polys.len() {
return false; }
let p = &polys[i];
constant += mult * p.c as i128;
for (&v, &c) in &p.lin {
*lin.entry(v).or_insert(0) += mult * c as i128;
}
for (&k, &c) in &p.quad {
*quad.entry(k).or_insert(0) += mult * c as i128;
}
}
lin.values().all(|&c| c == 0) && quad.values().all(|&c| c == 0) && constant > 0
}
#[cfg(test)]
mod tests {
use super::*;
fn sat(num_vars: usize, clauses: &[Vec<Lit>]) -> bool {
(0u64..(1u64 << num_vars)).any(|x| {
clauses.iter().all(|c| c.iter().any(|l| ((x >> l.var()) & 1 != 0) == l.is_positive()))
})
}
fn splitmix(s: &mut u64) -> u64 {
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *s;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z ^ (z >> 31)
}
fn certificate_is_valid(num_vars: usize, clauses: &[Vec<Lit>], cert: &BTreeMap<usize, i128>) -> bool {
check_sos_certificate(num_vars, clauses, cert)
}
#[test]
fn sos_refutation_is_sound_against_brute_force() {
let mut state = 0x5050_0001u64;
for _ in 0..400 {
let nv = 2 + (splitmix(&mut state) % 3) as usize; let m = 1 + (splitmix(&mut state) % 8) as usize;
let mut cl: Vec<Vec<Lit>> = Vec::new();
for _ in 0..m {
let mut c = Vec::new();
for v in 0..nv {
if splitmix(&mut state) % 3 == 0 {
c.push(Lit::new(v as u32, splitmix(&mut state) % 2 == 0));
}
}
if !c.is_empty() {
cl.push(c);
}
}
if cl.is_empty() {
continue;
}
if let Some(cert) = sos_certificate(nv, &cl) {
assert!(!sat(nv, &cl), "SoS refuted a SATISFIABLE formula: {cl:?}");
assert!(certificate_is_valid(nv, &cl, &cert), "the Farkas certificate must re-check: {cl:?}");
}
}
}
#[test]
fn satisfiable_formulas_are_never_refuted() {
let cl = vec![
vec![Lit::new(0, true), Lit::new(1, true)],
vec![Lit::new(0, false), Lit::new(2, true)],
];
assert!(sat(3, &cl));
assert!(!sos_refutes(3, &cl), "a SAT formula must not be refuted");
}
#[test]
fn sos_closes_an_integrality_gap_that_is_linear_feasible() {
let cl = vec![
vec![Lit::new(0, true), Lit::new(1, false)],
vec![Lit::new(0, false), Lit::new(1, true)],
vec![Lit::new(0, true), Lit::new(1, true)],
vec![Lit::new(0, false), Lit::new(1, false)],
];
assert!(!sat(2, &cl), "x=y ∧ x≠y is UNSAT");
let cert = sos_certificate(2, &cl).expect("degree-2 SoS closes the x=y=½ integrality gap");
assert!(certificate_is_valid(2, &cl, &cert), "and its certificate re-checks");
}
#[test]
fn the_degree_2_lift_refutes_where_the_linear_relaxation_cannot() {
let cl = vec![
vec![Lit::new(0, true), Lit::new(1, false)],
vec![Lit::new(0, false), Lit::new(1, true)],
vec![Lit::new(0, true), Lit::new(1, true)],
vec![Lit::new(0, false), Lit::new(1, false)],
];
let one = Poly::constant(1);
let mut polys: Vec<Poly> = Vec::new();
for i in 0..2 {
polys.push(Poly::x(i).neg());
polys.push(one.sub(&Poly::x(i)).neg());
}
for c in &cl {
let mut p = Poly::constant(-1);
for l in c {
p = p.add(&lit_value(l));
}
polys.push(p.neg());
}
let rows: Vec<Row> = polys.iter().enumerate().map(|(i, p)| poly_to_row(p, 2, i)).collect();
assert!(farkas_refute(rows, 2 + 4).is_none(), "the degree-1 relaxation is feasible (x=y=½)");
assert!(sos_refutes(2, &cl), "but the degree-2 lift refutes it");
}
}