1use std::collections::BTreeMap;
10
11use crate::ProofTerm;
12
13#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct LinExpr {
17 pub coeffs: BTreeMap<String, i64>,
18 pub constant: i64,
19}
20
21impl LinExpr {
22 fn constant(c: i64) -> Self {
23 LinExpr { coeffs: BTreeMap::new(), constant: c }
24 }
25 fn var(name: &str) -> Self {
26 let mut coeffs = BTreeMap::new();
27 coeffs.insert(name.to_string(), 1);
28 LinExpr { coeffs, constant: 0 }
29 }
30 fn prune(mut self) -> Self {
31 self.coeffs.retain(|_, c| *c != 0);
32 self
33 }
34 fn add(&self, o: &Self) -> Self {
35 let mut coeffs = self.coeffs.clone();
36 for (k, v) in &o.coeffs {
37 *coeffs.entry(k.clone()).or_insert(0) += v;
38 }
39 LinExpr { coeffs, constant: self.constant + o.constant }.prune()
40 }
41 fn neg(&self) -> Self {
42 LinExpr {
43 coeffs: self.coeffs.iter().map(|(k, v)| (k.clone(), -v)).collect(),
44 constant: -self.constant,
45 }
46 }
47 pub fn sub(&self, o: &Self) -> Self {
48 self.add(&o.neg())
49 }
50 pub fn is_const(&self) -> bool {
52 self.coeffs.is_empty()
53 }
54 fn scale(&self, k: i64) -> Self {
55 LinExpr {
56 coeffs: self.coeffs.iter().map(|(x, v)| (x.clone(), v * k)).collect(),
57 constant: self.constant * k,
58 }
59 .prune()
60 }
61 fn is_constant(&self) -> bool {
62 self.coeffs.is_empty()
63 }
64 fn coeff(&self, v: &str) -> i64 {
65 self.coeffs.get(v).copied().unwrap_or(0)
66 }
67}
68
69pub fn parse_lin(t: &ProofTerm) -> Option<LinExpr> {
73 match t {
74 ProofTerm::Constant(s) => Some(match s.parse::<i64>() {
75 Ok(n) => LinExpr::constant(n),
76 Err(_) => LinExpr::var(s),
77 }),
78 ProofTerm::Variable(s) | ProofTerm::BoundVarRef(s) => Some(LinExpr::var(s)),
79 ProofTerm::Function(name, args) => match (name.as_str(), args.as_slice()) {
80 ("add", [a, b]) => Some(parse_lin(a)?.add(&parse_lin(b)?)),
81 ("sub", [a, b]) => Some(parse_lin(a)?.sub(&parse_lin(b)?)),
82 ("mul", [a, b]) => {
83 let (la, lb) = (parse_lin(a)?, parse_lin(b)?);
84 if la.is_constant() {
85 Some(lb.scale(la.constant))
86 } else if lb.is_constant() {
87 Some(la.scale(lb.constant))
88 } else {
89 None
90 }
91 }
92 _ => None,
93 },
94 ProofTerm::Group(_) => None,
95 }
96}
97
98#[derive(Clone, Debug)]
101struct Row {
102 e: LinExpr,
103 prov: BTreeMap<usize, i64>,
104}
105
106fn combine_prov(a: &BTreeMap<usize, i64>, ka: i64, b: &BTreeMap<usize, i64>, kb: i64) -> BTreeMap<usize, i64> {
107 let mut out = BTreeMap::new();
108 for (i, v) in a {
109 *out.entry(*i).or_insert(0) += v * ka;
110 }
111 for (i, v) in b {
112 *out.entry(*i).or_insert(0) += v * kb;
113 }
114 out.retain(|_, v| *v != 0);
115 out
116}
117
118pub fn find_farkas(constraints: &[LinExpr]) -> Option<BTreeMap<usize, i64>> {
123 let mut rows: Vec<Row> = constraints
124 .iter()
125 .enumerate()
126 .map(|(i, e)| Row { e: e.clone(), prov: BTreeMap::from([(i, 1)]) })
127 .collect();
128
129 let contradiction = |rows: &[Row]| -> Option<BTreeMap<usize, i64>> {
130 rows.iter()
131 .find(|r| r.e.is_constant() && r.e.constant > 0)
132 .map(|r| r.prov.clone())
133 };
134 if let Some(p) = contradiction(&rows) {
135 return Some(p);
136 }
137
138 let mut vars: Vec<String> = rows
140 .iter()
141 .flat_map(|r| r.e.coeffs.keys().cloned())
142 .collect();
143 vars.sort();
144 vars.dedup();
145
146 for v in vars {
147 let (mut pos, mut neg, mut zero) = (Vec::new(), Vec::new(), Vec::new());
148 for r in rows {
149 match r.e.coeff(&v) {
150 c if c > 0 => pos.push(r),
151 c if c < 0 => neg.push(r),
152 _ => zero.push(r),
153 }
154 }
155 let mut next = zero;
156 for p in &pos {
157 for n in &neg {
158 let (pc, nc) = (p.e.coeff(&v), -n.e.coeff(&v)); next.push(Row {
161 e: p.e.scale(nc).add(&n.e.scale(pc)),
162 prov: combine_prov(&p.prov, nc, &n.prov, pc),
163 });
164 }
165 }
166 rows = next;
167 if let Some(p) = contradiction(&rows) {
168 return Some(p);
169 }
170 }
171 contradiction(&rows)
172}
173
174pub fn combine(constraints: &[LinExpr], multipliers: &BTreeMap<usize, i64>) -> LinExpr {
178 let mut acc = LinExpr::constant(0);
179 for (&i, &m) in multipliers {
180 acc = acc.add(&constraints[i].scale(m));
181 }
182 acc
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 fn cst(c: i64) -> ProofTerm {
190 ProofTerm::Constant(c.to_string())
191 }
192 fn var(s: &str) -> ProofTerm {
193 ProofTerm::Constant(s.to_string())
194 }
195 fn f(n: &str, a: Vec<ProofTerm>) -> ProofTerm {
196 ProofTerm::Function(n.to_string(), a)
197 }
198 fn le_constraint(l: ProofTerm, r: ProofTerm) -> LinExpr {
200 parse_lin(&l).unwrap().sub(&parse_lin(&r).unwrap())
201 }
202
203 #[test]
204 fn parses_linear_forms() {
205 let e = le_constraint(f("add", vec![f("mul", vec![cst(2), var("x")]), cst(3)]), f("add", vec![var("x"), cst(1)]));
207 assert_eq!(e.coeff("x"), 1);
208 assert_eq!(e.constant, 2);
209 }
210
211 #[test]
212 fn rejects_nonlinear() {
213 assert!(parse_lin(&f("mul", vec![var("x"), var("y")])).is_none());
214 }
215
216 #[test]
217 fn chain_contradiction_5_le_x_le_3() {
218 let cs = vec![le_constraint(cst(5), var("x")), le_constraint(var("x"), cst(3))];
220 let cert = find_farkas(&cs).expect("5≤x≤3 is unsatisfiable");
221 assert_eq!(cert.get(&0), Some(&1));
222 assert_eq!(cert.get(&1), Some(&1));
223 }
224
225 #[test]
226 fn scaling_contradiction_needs_a_multiplier() {
227 let cs = vec![
230 le_constraint(cst(2), var("x")),
231 le_constraint(f("mul", vec![cst(2), var("x")]), cst(3)),
232 ];
233 let cert = find_farkas(&cs).expect("2≤x and 2x≤3 is unsatisfiable");
234 assert_eq!(cert.get(&0), Some(&2), "needs to scale `2 ≤ x` by 2");
235 assert_eq!(cert.get(&1), Some(&1));
236 }
237
238 #[test]
239 fn two_variable_contradiction() {
240 let cs = vec![
242 le_constraint(var("x"), var("y")),
243 le_constraint(var("y"), var("z")),
244 le_constraint(var("z"), f("sub", vec![var("x"), cst(1)])),
245 ];
246 let cert = find_farkas(&cs).expect("the cycle is unsatisfiable");
247 assert_eq!(cert.get(&0), Some(&1));
248 assert_eq!(cert.get(&1), Some(&1));
249 assert_eq!(cert.get(&2), Some(&1));
250 }
251
252 #[test]
253 fn satisfiable_system_has_no_certificate() {
254 let cs = vec![le_constraint(cst(1), var("x")), le_constraint(var("x"), cst(5))];
256 assert!(find_farkas(&cs).is_none(), "1≤x≤5 is satisfiable");
257 }
258}