logicaffeine_kernel/lia.rs
1//! LIA Tactic: Linear Integer Arithmetic by Fourier-Motzkin Elimination
2//!
3//! This module implements a decision procedure for linear arithmetic over
4//! rational numbers (with integer semantics handled by [`crate::omega`]).
5//!
6//! # Algorithm
7//!
8//! The lia tactic works in four steps:
9//! 1. **Reification**: Convert Syntax terms to [`LinearExpr`] representation
10//! 2. **Negation**: Convert the goal to constraints (validity = negation is unsatisfiable)
11//! 3. **Elimination**: Use Fourier-Motzkin to eliminate variables one by one
12//! 4. **Check**: Verify the resulting constant constraints are contradictory
13//!
14//! # Fourier-Motzkin Elimination
15//!
16//! This classical algorithm eliminates variables from a system of linear inequalities.
17//! For each variable x:
18//! - Partition constraints into lower bounds (x >= L), upper bounds (x <= U), and independent
19//! - Combine each lower with each upper: L <= U
20//! - The resulting system has one fewer variable
21//!
22//! # Rational Arithmetic
23//!
24//! The module uses exact rational arithmetic during elimination to avoid
25//! precision issues. Rationals are automatically normalized to lowest terms.
26//!
27//! # Supported Relations
28//!
29//! - `Lt` (less than)
30//! - `Le` (less than or equal)
31//! - `Gt` (greater than)
32//! - `Ge` (greater than or equal)
33
34use std::collections::{BTreeMap, HashSet};
35
36use crate::term::{Literal, Term};
37
38/// Exact rational number for arithmetic during Fourier-Motzkin elimination.
39///
40/// Rationals are automatically normalized to lowest terms with positive denominator.
41/// This ensures consistent comparison and canonical representation.
42///
43/// # Invariants
44///
45/// - `denominator > 0` (sign carried by numerator)
46/// - `gcd(|numerator|, denominator) = 1` (lowest terms)
47#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
48pub struct Rational {
49 /// The numerator (may be negative).
50 pub numerator: i64,
51 /// The denominator (always positive, never zero).
52 pub denominator: i64,
53}
54
55impl Rational {
56 /// Create a new rational, automatically normalizing to lowest terms
57 pub fn new(n: i64, d: i64) -> Self {
58 if d == 0 {
59 panic!("Rational denominator cannot be zero");
60 }
61 let g = gcd(n.abs(), d.abs()).max(1);
62 let sign = if d < 0 { -1 } else { 1 };
63 Rational {
64 numerator: sign * n / g,
65 denominator: (d.abs()) / g,
66 }
67 }
68
69 /// The zero rational
70 pub fn zero() -> Self {
71 Rational {
72 numerator: 0,
73 denominator: 1,
74 }
75 }
76
77 /// Create a rational from an integer
78 pub fn from_int(n: i64) -> Self {
79 Rational {
80 numerator: n,
81 denominator: 1,
82 }
83 }
84
85 /// Add two rationals
86 pub fn add(&self, other: &Rational) -> Rational {
87 Rational::new(
88 self.numerator * other.denominator + other.numerator * self.denominator,
89 self.denominator * other.denominator,
90 )
91 }
92
93 /// Negate a rational
94 pub fn neg(&self) -> Rational {
95 Rational {
96 numerator: -self.numerator,
97 denominator: self.denominator,
98 }
99 }
100
101 /// Subtract two rationals
102 pub fn sub(&self, other: &Rational) -> Rational {
103 self.add(&other.neg())
104 }
105
106 /// Multiply two rationals
107 pub fn mul(&self, other: &Rational) -> Rational {
108 Rational::new(
109 self.numerator * other.numerator,
110 self.denominator * other.denominator,
111 )
112 }
113
114 /// Divide two rationals (returns None if dividing by zero)
115 pub fn div(&self, other: &Rational) -> Option<Rational> {
116 if other.numerator == 0 {
117 return None;
118 }
119 Some(Rational::new(
120 self.numerator * other.denominator,
121 self.denominator * other.numerator,
122 ))
123 }
124
125 /// Check if negative
126 pub fn is_negative(&self) -> bool {
127 self.numerator < 0
128 }
129
130 /// Check if positive
131 pub fn is_positive(&self) -> bool {
132 self.numerator > 0
133 }
134
135 /// Check if zero
136 pub fn is_zero(&self) -> bool {
137 self.numerator == 0
138 }
139}
140
141/// Greatest common divisor using Euclidean algorithm
142fn gcd(a: i64, b: i64) -> i64 {
143 if b == 0 {
144 a
145 } else {
146 gcd(b, a % b)
147 }
148}
149
150/// A linear expression of the form c₀ + c₁x₁ + c₂x₂ + ... + cₙxₙ.
151///
152/// Stored as a constant term plus a sparse map of variable coefficients.
153/// Variables with coefficient 0 are automatically removed.
154///
155/// # Representation
156///
157/// The expression `3 + 2x - y` is stored as:
158/// - `constant = 3`
159/// - `coefficients = {0: 2, 1: -1}` (assuming x is var 0, y is var 1)
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct LinearExpr {
162 /// The constant term c₀.
163 pub constant: Rational,
164 /// Maps variable index to its coefficient (sparse representation).
165 pub coefficients: BTreeMap<i64, Rational>,
166}
167
168impl LinearExpr {
169 /// Create a constant expression
170 pub fn constant(c: Rational) -> Self {
171 LinearExpr {
172 constant: c,
173 coefficients: BTreeMap::new(),
174 }
175 }
176
177 /// Create a single variable expression: 1*x_idx + 0
178 pub fn var(idx: i64) -> Self {
179 let mut coeffs = BTreeMap::new();
180 coeffs.insert(idx, Rational::from_int(1));
181 LinearExpr {
182 constant: Rational::zero(),
183 coefficients: coeffs,
184 }
185 }
186
187 /// Add two linear expressions
188 pub fn add(&self, other: &LinearExpr) -> LinearExpr {
189 let mut result = self.clone();
190 result.constant = result.constant.add(&other.constant);
191 for (var, coeff) in &other.coefficients {
192 let entry = result
193 .coefficients
194 .entry(*var)
195 .or_insert(Rational::zero());
196 *entry = entry.add(coeff);
197 if entry.is_zero() {
198 result.coefficients.remove(var);
199 }
200 }
201 result
202 }
203
204 /// Negate a linear expression
205 pub fn neg(&self) -> LinearExpr {
206 LinearExpr {
207 constant: self.constant.neg(),
208 coefficients: self
209 .coefficients
210 .iter()
211 .map(|(v, c)| (*v, c.neg()))
212 .collect(),
213 }
214 }
215
216 /// Subtract two linear expressions
217 pub fn sub(&self, other: &LinearExpr) -> LinearExpr {
218 self.add(&other.neg())
219 }
220
221 /// Scale a linear expression by a rational constant
222 pub fn scale(&self, c: &Rational) -> LinearExpr {
223 if c.is_zero() {
224 return LinearExpr::constant(Rational::zero());
225 }
226 LinearExpr {
227 constant: self.constant.mul(c),
228 coefficients: self
229 .coefficients
230 .iter()
231 .map(|(v, coeff)| (*v, coeff.mul(c)))
232 .filter(|(_, c)| !c.is_zero())
233 .collect(),
234 }
235 }
236
237 /// Check if this is a constant expression (no variables)
238 pub fn is_constant(&self) -> bool {
239 self.coefficients.is_empty()
240 }
241
242 /// Get coefficient of a variable (0 if not present)
243 pub fn get_coeff(&self, var: i64) -> Rational {
244 self.coefficients
245 .get(&var)
246 .cloned()
247 .unwrap_or(Rational::zero())
248 }
249}
250
251/// A linear constraint representing either `expr <= 0` or `expr < 0`.
252///
253/// All inequalities are normalized to this form during processing.
254/// For example, `x >= 5` becomes `-x + 5 <= 0`, i.e., `5 - x <= 0`.
255#[derive(Debug, Clone)]
256pub struct Constraint {
257 /// The linear expression (constraint is expr OP 0).
258 pub expr: LinearExpr,
259 /// If true, this is a strict inequality (`< 0`).
260 /// If false, this is a non-strict inequality (`<= 0`).
261 pub strict: bool,
262}
263
264impl Constraint {
265 /// Check if a constant constraint is satisfied
266 /// For non-constant constraints, returns true (we can't tell yet)
267 pub fn is_satisfied_constant(&self) -> bool {
268 if !self.expr.is_constant() {
269 return true; // Can't determine yet
270 }
271 let c = &self.expr.constant;
272 if self.strict {
273 c.is_negative() // c < 0
274 } else {
275 !c.is_positive() // c ≤ 0
276 }
277 }
278}
279
280/// Error during reification to linear expression
281#[derive(Debug)]
282pub enum LiaError {
283 /// Expression is not linear (e.g., x*y)
284 NonLinear(String),
285 /// Malformed term structure
286 MalformedTerm,
287 /// Goal is not an inequality
288 NotInequality,
289}
290
291/// Reify a Syntax term to a linear expression.
292///
293/// Converts the deep embedding of terms (Syntax) into a linear expression
294/// suitable for Fourier-Motzkin elimination.
295///
296/// # Supported Forms
297///
298/// - `SLit n` - Integer literal becomes a constant
299/// - `SVar i` - De Bruijn variable becomes a linear variable
300/// - `SName "x"` - Named global becomes a linear variable (hashed)
301/// - `SApp (SApp (SName "add") a) b` - Linear addition
302/// - `SApp (SApp (SName "sub") a) b` - Linear subtraction
303/// - `SApp (SApp (SName "mul") c) x` - Scaling (only if one operand is constant)
304///
305/// # Errors
306///
307/// Returns [`LiaError::NonLinear`] if the term contains non-linear operations
308/// (e.g., multiplication of two variables).
309pub fn reify_linear(term: &Term) -> Result<LinearExpr, LiaError> {
310 // SLit n -> constant
311 if let Some(n) = extract_slit(term) {
312 return Ok(LinearExpr::constant(Rational::from_int(n)));
313 }
314
315 // SVar i -> variable
316 if let Some(i) = extract_svar(term) {
317 return Ok(LinearExpr::var(i));
318 }
319
320 // SName "x" -> named variable (global constant treated as free variable)
321 if let Some(name) = extract_sname(term) {
322 let hash = name_to_var_index(&name);
323 return Ok(LinearExpr::var(hash));
324 }
325
326 // Binary operations
327 if let Some((op, a, b)) = extract_binary_app(term) {
328 match op.as_str() {
329 "add" => {
330 let la = reify_linear(&a)?;
331 let lb = reify_linear(&b)?;
332 return Ok(la.add(&lb));
333 }
334 "sub" => {
335 let la = reify_linear(&a)?;
336 let lb = reify_linear(&b)?;
337 return Ok(la.sub(&lb));
338 }
339 "mul" => {
340 let la = reify_linear(&a)?;
341 let lb = reify_linear(&b)?;
342 // Only linear if one side is constant
343 if la.is_constant() {
344 return Ok(lb.scale(&la.constant));
345 }
346 if lb.is_constant() {
347 return Ok(la.scale(&lb.constant));
348 }
349 return Err(LiaError::NonLinear(
350 "multiplication of two variables is not linear".to_string(),
351 ));
352 }
353 "div" | "mod" => {
354 return Err(LiaError::NonLinear(format!(
355 "operation '{}' is not supported in lia",
356 op
357 )));
358 }
359 _ => {
360 return Err(LiaError::NonLinear(format!("unknown operation '{}'", op)));
361 }
362 }
363 }
364
365 Err(LiaError::MalformedTerm)
366}
367
368/// Extract comparison from goal: (SApp (SApp (SName "Lt"|"Le"|"Gt"|"Ge") lhs) rhs)
369pub fn extract_comparison(term: &Term) -> Option<(String, Term, Term)> {
370 if let Some((rel, lhs, rhs)) = extract_binary_app(term) {
371 match rel.as_str() {
372 "Lt" | "Le" | "Gt" | "Ge" | "lt" | "le" | "gt" | "ge" => {
373 return Some((rel, lhs, rhs));
374 }
375 _ => {}
376 }
377 }
378 None
379}
380
381/// Convert a goal to constraints for validity checking.
382///
383/// To prove a goal is valid, we check if its negation is unsatisfiable.
384/// - Lt(a, b) is valid iff a - b < 0 always, i.e., negation a - b >= 0 is unsat
385/// - Le(a, b) is valid iff a - b <= 0 always, i.e., negation a - b > 0 is unsat
386pub fn goal_to_negated_constraint(
387 rel: &str,
388 lhs: &LinearExpr,
389 rhs: &LinearExpr,
390) -> Option<Constraint> {
391 // diff = lhs - rhs
392 let diff = lhs.sub(rhs);
393
394 match rel {
395 // Lt: a < b valid iff NOT(a >= b), i.e., a - b >= 0 is unsat
396 // So negation constraint is: a - b >= 0, i.e., -(a - b) <= 0, i.e., (b - a) <= 0
397 // Actually: a >= b means a - b >= 0, which means -(a - b) <= 0
398 // But we want to find if a - b >= 0 can ever be true
399 // If we want to prove a < b (always), we check if a >= b (ever) is unsat
400 // Constraint form: expr <= 0 or expr < 0
401 // a >= b means a - b >= 0, means -(a - b) <= 0, means (b - a) <= 0
402 "Lt" | "lt" => {
403 // Want to prove: a < b always
404 // Negation: a >= b (can be true)
405 // a >= b means a - b >= 0
406 // In our constraint form (expr <= 0): -(a - b) <= 0, i.e., (rhs - lhs) <= 0
407 Some(Constraint {
408 expr: rhs.sub(lhs),
409 strict: false, // <= 0
410 })
411 }
412 // Le: a <= b valid iff NOT(a > b), i.e., a - b > 0 is unsat
413 "Le" | "le" => {
414 // Want to prove: a <= b always
415 // Negation: a > b
416 // a > b means a - b > 0
417 // In constraint form: -(a - b) < 0, i.e., (rhs - lhs) < 0
418 Some(Constraint {
419 expr: rhs.sub(lhs),
420 strict: true, // < 0
421 })
422 }
423 // Gt: a > b valid iff NOT(a <= b), i.e., a - b <= 0 is unsat
424 "Gt" | "gt" => {
425 // Want to prove: a > b always
426 // Negation: a <= b
427 // a <= b means a - b <= 0
428 // In constraint form: (a - b) <= 0, i.e., (lhs - rhs) <= 0
429 Some(Constraint {
430 expr: diff, // (lhs - rhs) <= 0
431 strict: false,
432 })
433 }
434 // Ge: a >= b valid iff NOT(a < b), i.e., a - b < 0 is unsat
435 "Ge" | "ge" => {
436 // Want to prove: a >= b always
437 // Negation: a < b
438 // a < b means a - b < 0
439 // In constraint form: (a - b) < 0, i.e., (lhs - rhs) < 0
440 Some(Constraint {
441 expr: diff, // (lhs - rhs) < 0
442 strict: true,
443 })
444 }
445 _ => None,
446 }
447}
448
449/// Check if a constraint set is unsatisfiable using Fourier-Motzkin elimination.
450///
451/// This is the core decision procedure. It eliminates variables one by one
452/// until only constant constraints remain, then checks for contradictions.
453///
454/// # Algorithm
455///
456/// For each variable x in the system:
457/// 1. Partition constraints into lower bounds on x, upper bounds on x, and independent
458/// 2. For each pair (lower, upper), derive a new constraint without x
459/// 3. Check for immediate contradictions (e.g., `5 <= 0`)
460///
461/// # Returns
462///
463/// - `true` if the constraints are unsatisfiable (contradiction found)
464/// - `false` if the constraints may be satisfiable
465///
466/// # Usage for Validity
467///
468/// To prove a goal G is valid, we check if NOT(G) is unsatisfiable.
469/// If `fourier_motzkin_unsat(negation_constraints)` returns true, the goal is valid.
470pub fn fourier_motzkin_unsat(constraints: &[Constraint]) -> bool {
471 if constraints.is_empty() {
472 return false; // Empty set is satisfiable
473 }
474
475 // Collect all variables
476 let vars: Vec<i64> = constraints
477 .iter()
478 .flat_map(|c| c.expr.coefficients.keys().copied())
479 .collect::<HashSet<_>>()
480 .into_iter()
481 .collect();
482
483 let mut current = constraints.to_vec();
484
485 // Eliminate each variable
486 for var in vars {
487 current = eliminate_variable(¤t, var);
488
489 // Early termination: check for constant contradictions
490 for c in ¤t {
491 if c.expr.is_constant() && !c.is_satisfied_constant() {
492 return true; // Contradiction found!
493 }
494 }
495 }
496
497 // Check all remaining constant constraints
498 current.iter().any(|c| !c.is_satisfied_constant())
499}
500
501/// Eliminate a variable from a set of constraints using Fourier-Motzkin.
502///
503/// Partitions constraints into:
504/// - Lower bounds: x >= expr (coeff < 0 means -|c|*x + rest <= 0 => x >= rest/|c|)
505/// - Upper bounds: x <= expr (coeff > 0 means c*x + rest <= 0 => x <= -rest/c)
506/// - Independent: doesn't contain variable
507///
508/// Combines each lower with each upper to get new constraints without the variable.
509fn eliminate_variable(constraints: &[Constraint], var: i64) -> Vec<Constraint> {
510 let mut lower: Vec<(LinearExpr, bool)> = vec![]; // lower bound on var
511 let mut upper: Vec<(LinearExpr, bool)> = vec![]; // upper bound on var
512 let mut independent: Vec<Constraint> = vec![];
513
514 for c in constraints {
515 let coeff = c.expr.get_coeff(var);
516 if coeff.is_zero() {
517 independent.push(c.clone());
518 } else {
519 // c.expr = coeff*var + rest <= 0 (or < 0)
520 // Extract: rest = c.expr - coeff*var
521 let mut rest = c.expr.clone();
522 rest.coefficients.remove(&var);
523
524 if coeff.is_positive() {
525 // coeff*var + rest <= 0
526 // var <= -rest/coeff
527 // Upper bound: -rest/coeff
528 let bound = rest.neg().scale(&coeff.div(&Rational::from_int(1)).unwrap());
529 let bound = bound.scale(
530 &Rational::from_int(1)
531 .div(&coeff)
532 .unwrap_or(Rational::from_int(1)),
533 );
534 upper.push((rest.neg().scale(&coeff.div(&coeff).unwrap()), c.strict));
535 } else {
536 // coeff*var + rest <= 0, coeff < 0
537 // |coeff|*(-var) + rest <= 0
538 // -var <= -rest/|coeff|
539 // var >= rest/|coeff|
540 // Lower bound: rest/|coeff|
541 let abs_coeff = coeff.neg();
542 lower.push((rest.scale(&abs_coeff.div(&abs_coeff).unwrap()), c.strict));
543 }
544 }
545 }
546
547 // Combine lower and upper bounds
548 // If lo <= var <= hi, then lo <= hi must hold
549 for (lo_expr, lo_strict) in &lower {
550 for (hi_expr, hi_strict) in &upper {
551 // We need: lo <= hi
552 // In constraint form: lo - hi <= 0
553 let diff = lo_expr.sub(hi_expr);
554 independent.push(Constraint {
555 expr: diff,
556 strict: *lo_strict || *hi_strict,
557 });
558 }
559 }
560
561 independent
562}
563
564// =============================================================================
565// Helper functions for extracting Syntax patterns (same as ring.rs)
566// =============================================================================
567
568/// Extract integer from SLit n
569fn extract_slit(term: &Term) -> Option<i64> {
570 if let Term::App(ctor, arg) = term {
571 if let Term::Global(name) = ctor.as_ref() {
572 if name == "SLit" {
573 if let Term::Lit(Literal::Int(n)) = arg.as_ref() {
574 return Some(*n);
575 }
576 }
577 }
578 }
579 None
580}
581
582/// Extract variable index from SVar i
583fn extract_svar(term: &Term) -> Option<i64> {
584 if let Term::App(ctor, arg) = term {
585 if let Term::Global(name) = ctor.as_ref() {
586 if name == "SVar" {
587 if let Term::Lit(Literal::Int(i)) = arg.as_ref() {
588 return Some(*i);
589 }
590 }
591 }
592 }
593 None
594}
595
596/// Extract name from SName "x"
597fn extract_sname(term: &Term) -> Option<String> {
598 if let Term::App(ctor, arg) = term {
599 if let Term::Global(name) = ctor.as_ref() {
600 if name == "SName" {
601 if let Term::Lit(Literal::Text(s)) = arg.as_ref() {
602 return Some(s.clone());
603 }
604 }
605 }
606 }
607 None
608}
609
610/// Extract binary application: SApp (SApp (SName "op") a) b
611fn extract_binary_app(term: &Term) -> Option<(String, Term, Term)> {
612 if let Term::App(outer, b) = term {
613 if let Term::App(sapp_outer, inner) = outer.as_ref() {
614 if let Term::Global(ctor) = sapp_outer.as_ref() {
615 if ctor == "SApp" {
616 if let Term::App(partial, a) = inner.as_ref() {
617 if let Term::App(sapp_inner, op_term) = partial.as_ref() {
618 if let Term::Global(ctor2) = sapp_inner.as_ref() {
619 if ctor2 == "SApp" {
620 if let Some(op) = extract_sname(op_term) {
621 return Some((
622 op,
623 a.as_ref().clone(),
624 b.as_ref().clone(),
625 ));
626 }
627 }
628 }
629 }
630 }
631 }
632 }
633 }
634 }
635 None
636}
637
638/// Convert a name to a unique negative variable index
639fn name_to_var_index(name: &str) -> i64 {
640 let hash: i64 = name
641 .bytes()
642 .fold(0i64, |acc, b| acc.wrapping_mul(31).wrapping_add(b as i64));
643 -(hash.abs() + 1_000_000)
644}
645
646#[cfg(test)]
647mod tests {
648 use super::*;
649
650 #[test]
651 fn test_rational_arithmetic() {
652 let half = Rational::new(1, 2);
653 let third = Rational::new(1, 3);
654 let sum = half.add(&third);
655 assert_eq!(sum, Rational::new(5, 6));
656 }
657
658 #[test]
659 fn test_linear_expr_add() {
660 let x = LinearExpr::var(0);
661 let y = LinearExpr::var(1);
662 let sum = x.add(&y);
663 assert!(!sum.is_constant());
664 assert_eq!(sum.get_coeff(0), Rational::from_int(1));
665 assert_eq!(sum.get_coeff(1), Rational::from_int(1));
666 }
667
668 #[test]
669 fn test_linear_expr_cancel() {
670 let x = LinearExpr::var(0);
671 let neg_x = x.neg();
672 let zero = x.add(&neg_x);
673 assert!(zero.is_constant());
674 assert!(zero.constant.is_zero());
675 }
676
677 #[test]
678 fn test_constraint_satisfied() {
679 // -1 <= 0 is satisfied
680 let c1 = Constraint {
681 expr: LinearExpr::constant(Rational::from_int(-1)),
682 strict: false,
683 };
684 assert!(c1.is_satisfied_constant());
685
686 // 1 <= 0 is NOT satisfied
687 let c2 = Constraint {
688 expr: LinearExpr::constant(Rational::from_int(1)),
689 strict: false,
690 };
691 assert!(!c2.is_satisfied_constant());
692
693 // 0 <= 0 is satisfied
694 let c3 = Constraint {
695 expr: LinearExpr::constant(Rational::zero()),
696 strict: false,
697 };
698 assert!(c3.is_satisfied_constant());
699
700 // 0 < 0 is NOT satisfied (strict)
701 let c4 = Constraint {
702 expr: LinearExpr::constant(Rational::zero()),
703 strict: true,
704 };
705 assert!(!c4.is_satisfied_constant());
706 }
707
708 #[test]
709 fn test_fourier_motzkin_constant() {
710 // Single constraint: 1 <= 0 (false)
711 let constraints = vec![Constraint {
712 expr: LinearExpr::constant(Rational::from_int(1)),
713 strict: false,
714 }];
715 assert!(fourier_motzkin_unsat(&constraints));
716
717 // Single constraint: -1 <= 0 (true)
718 let constraints2 = vec![Constraint {
719 expr: LinearExpr::constant(Rational::from_int(-1)),
720 strict: false,
721 }];
722 assert!(!fourier_motzkin_unsat(&constraints2));
723 }
724
725 #[test]
726 fn test_x_lt_x_plus_1() {
727 // x < x + 1 is always true
728 // Negation: x >= x + 1, i.e., x - x - 1 >= 0, i.e., -1 >= 0
729 // Constraint: -(-1) <= 0 => 1 <= 0 which is unsat => goal is valid
730 let x = LinearExpr::var(0);
731 let one = LinearExpr::constant(Rational::from_int(1));
732 let xp1 = x.add(&one);
733
734 // Goal: Lt x (x+1)
735 // Negation constraint: (x+1) - x <= 0 (non-strict for Lt's negation Ge)
736 // Wait, let me reconsider...
737 // Lt(a, b) valid means a < b always
738 // Negation: a >= b can be true
739 // For FM: we want to show a >= b is unsat
740 // a >= b means a - b >= 0
741 // In our form (expr <= 0): -(a - b) <= 0, i.e., (b - a) <= 0
742
743 // So for Lt(x, x+1): negation constraint is (x+1 - x) <= 0 = 1 <= 0
744 let constraint = Constraint {
745 expr: LinearExpr::constant(Rational::from_int(1)),
746 strict: false,
747 };
748 // 1 <= 0 is unsat, so goal is valid
749 assert!(fourier_motzkin_unsat(&[constraint]));
750 }
751}