use std::collections::BTreeMap;
use crate::ProofTerm;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LinExpr {
pub coeffs: BTreeMap<String, i64>,
pub constant: i64,
}
impl LinExpr {
fn constant(c: i64) -> Self {
LinExpr { coeffs: BTreeMap::new(), constant: c }
}
fn var(name: &str) -> Self {
let mut coeffs = BTreeMap::new();
coeffs.insert(name.to_string(), 1);
LinExpr { coeffs, constant: 0 }
}
fn prune(mut self) -> Self {
self.coeffs.retain(|_, c| *c != 0);
self
}
fn add(&self, o: &Self) -> Self {
let mut coeffs = self.coeffs.clone();
for (k, v) in &o.coeffs {
*coeffs.entry(k.clone()).or_insert(0) += v;
}
LinExpr { coeffs, constant: self.constant + o.constant }.prune()
}
fn neg(&self) -> Self {
LinExpr {
coeffs: self.coeffs.iter().map(|(k, v)| (k.clone(), -v)).collect(),
constant: -self.constant,
}
}
pub fn sub(&self, o: &Self) -> Self {
self.add(&o.neg())
}
pub fn is_const(&self) -> bool {
self.coeffs.is_empty()
}
fn scale(&self, k: i64) -> Self {
LinExpr {
coeffs: self.coeffs.iter().map(|(x, v)| (x.clone(), v * k)).collect(),
constant: self.constant * k,
}
.prune()
}
fn is_constant(&self) -> bool {
self.coeffs.is_empty()
}
fn coeff(&self, v: &str) -> i64 {
self.coeffs.get(v).copied().unwrap_or(0)
}
}
pub fn parse_lin(t: &ProofTerm) -> Option<LinExpr> {
match t {
ProofTerm::Constant(s) => Some(match s.parse::<i64>() {
Ok(n) => LinExpr::constant(n),
Err(_) => LinExpr::var(s),
}),
ProofTerm::Variable(s) | ProofTerm::BoundVarRef(s) => Some(LinExpr::var(s)),
ProofTerm::Function(name, args) => match (name.as_str(), args.as_slice()) {
("add", [a, b]) => Some(parse_lin(a)?.add(&parse_lin(b)?)),
("sub", [a, b]) => Some(parse_lin(a)?.sub(&parse_lin(b)?)),
("mul", [a, b]) => {
let (la, lb) = (parse_lin(a)?, parse_lin(b)?);
if la.is_constant() {
Some(lb.scale(la.constant))
} else if lb.is_constant() {
Some(la.scale(lb.constant))
} else {
None
}
}
_ => None,
},
ProofTerm::Group(_) => None,
}
}
#[derive(Clone, Debug)]
struct Row {
e: LinExpr,
prov: BTreeMap<usize, i64>,
}
fn combine_prov(a: &BTreeMap<usize, i64>, ka: i64, b: &BTreeMap<usize, i64>, kb: i64) -> BTreeMap<usize, i64> {
let mut out = BTreeMap::new();
for (i, v) in a {
*out.entry(*i).or_insert(0) += v * ka;
}
for (i, v) in b {
*out.entry(*i).or_insert(0) += v * kb;
}
out.retain(|_, v| *v != 0);
out
}
pub fn find_farkas(constraints: &[LinExpr]) -> Option<BTreeMap<usize, i64>> {
let mut rows: Vec<Row> = constraints
.iter()
.enumerate()
.map(|(i, e)| Row { e: e.clone(), prov: BTreeMap::from([(i, 1)]) })
.collect();
let contradiction = |rows: &[Row]| -> Option<BTreeMap<usize, i64>> {
rows.iter()
.find(|r| r.e.is_constant() && r.e.constant > 0)
.map(|r| r.prov.clone())
};
if let Some(p) = contradiction(&rows) {
return Some(p);
}
let mut vars: Vec<String> = rows
.iter()
.flat_map(|r| r.e.coeffs.keys().cloned())
.collect();
vars.sort();
vars.dedup();
for v in vars {
let (mut pos, mut neg, mut zero) = (Vec::new(), Vec::new(), Vec::new());
for r in rows {
match r.e.coeff(&v) {
c if c > 0 => pos.push(r),
c if c < 0 => neg.push(r),
_ => zero.push(r),
}
}
let mut next = zero;
for p in &pos {
for n in &neg {
let (pc, nc) = (p.e.coeff(&v), -n.e.coeff(&v)); next.push(Row {
e: p.e.scale(nc).add(&n.e.scale(pc)),
prov: combine_prov(&p.prov, nc, &n.prov, pc),
});
}
}
rows = next;
if let Some(p) = contradiction(&rows) {
return Some(p);
}
}
contradiction(&rows)
}
pub fn combine(constraints: &[LinExpr], multipliers: &BTreeMap<usize, i64>) -> LinExpr {
let mut acc = LinExpr::constant(0);
for (&i, &m) in multipliers {
acc = acc.add(&constraints[i].scale(m));
}
acc
}
#[cfg(test)]
mod tests {
use super::*;
fn cst(c: i64) -> ProofTerm {
ProofTerm::Constant(c.to_string())
}
fn var(s: &str) -> ProofTerm {
ProofTerm::Constant(s.to_string())
}
fn f(n: &str, a: Vec<ProofTerm>) -> ProofTerm {
ProofTerm::Function(n.to_string(), a)
}
fn le_constraint(l: ProofTerm, r: ProofTerm) -> LinExpr {
parse_lin(&l).unwrap().sub(&parse_lin(&r).unwrap())
}
#[test]
fn parses_linear_forms() {
let e = le_constraint(f("add", vec![f("mul", vec![cst(2), var("x")]), cst(3)]), f("add", vec![var("x"), cst(1)]));
assert_eq!(e.coeff("x"), 1);
assert_eq!(e.constant, 2);
}
#[test]
fn rejects_nonlinear() {
assert!(parse_lin(&f("mul", vec![var("x"), var("y")])).is_none());
}
#[test]
fn chain_contradiction_5_le_x_le_3() {
let cs = vec![le_constraint(cst(5), var("x")), le_constraint(var("x"), cst(3))];
let cert = find_farkas(&cs).expect("5≤x≤3 is unsatisfiable");
assert_eq!(cert.get(&0), Some(&1));
assert_eq!(cert.get(&1), Some(&1));
}
#[test]
fn scaling_contradiction_needs_a_multiplier() {
let cs = vec![
le_constraint(cst(2), var("x")),
le_constraint(f("mul", vec![cst(2), var("x")]), cst(3)),
];
let cert = find_farkas(&cs).expect("2≤x and 2x≤3 is unsatisfiable");
assert_eq!(cert.get(&0), Some(&2), "needs to scale `2 ≤ x` by 2");
assert_eq!(cert.get(&1), Some(&1));
}
#[test]
fn two_variable_contradiction() {
let cs = vec![
le_constraint(var("x"), var("y")),
le_constraint(var("y"), var("z")),
le_constraint(var("z"), f("sub", vec![var("x"), cst(1)])),
];
let cert = find_farkas(&cs).expect("the cycle is unsatisfiable");
assert_eq!(cert.get(&0), Some(&1));
assert_eq!(cert.get(&1), Some(&1));
assert_eq!(cert.get(&2), Some(&1));
}
#[test]
fn satisfiable_system_has_no_certificate() {
let cs = vec![le_constraint(cst(1), var("x")), le_constraint(var("x"), cst(5))];
assert!(find_farkas(&cs).is_none(), "1≤x≤5 is satisfiable");
}
}