Skip to main content

oxiz_math/
simplex.rs

1//! Simplex algorithm for linear arithmetic.
2//!
3//! This module implements the Simplex algorithm for solving linear programming
4//! problems and checking satisfiability of linear real arithmetic constraints.
5//!
6//! The implementation follows the two-phase simplex method:
7//! - Phase 1: Find a basic feasible solution (or prove infeasibility)
8//! - Phase 2: Optimize the objective function (or detect unboundedness)
9//!
10//! Reference: Z3's `math/simplex/` directory and standard LP textbooks.
11
12use crate::fast_rational::FastRational;
13#[allow(unused_imports)]
14use crate::prelude::*;
15use core::fmt;
16use num_bigint::BigInt;
17use num_traits::{One, Signed, Zero};
18
19/// Variable identifier for simplex.
20pub type VarId = u32;
21
22/// Constraint identifier.
23pub type ConstraintId = u32;
24
25/// Bound type for a variable.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum BoundType {
28    /// Lower bound: x >= value
29    Lower,
30    /// Upper bound: x <= value
31    Upper,
32    /// Equality: x = value
33    Equal,
34}
35
36/// A bound on a variable.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct Bound {
39    /// The variable this bound applies to.
40    pub var: VarId,
41    /// The type of bound (lower, upper, or equality).
42    pub bound_type: BoundType,
43    /// The bound value.
44    pub value: FastRational,
45}
46
47impl Bound {
48    /// Create a lower bound: var >= value.
49    pub fn lower(var: VarId, value: FastRational) -> Self {
50        Self {
51            var,
52            bound_type: BoundType::Lower,
53            value,
54        }
55    }
56
57    /// Create an upper bound: var <= value.
58    pub fn upper(var: VarId, value: FastRational) -> Self {
59        Self {
60            var,
61            bound_type: BoundType::Upper,
62            value,
63        }
64    }
65
66    /// Create an equality bound: var = value.
67    pub fn equal(var: VarId, value: FastRational) -> Self {
68        Self {
69            var,
70            bound_type: BoundType::Equal,
71            value,
72        }
73    }
74}
75
76/// Variable classification in the tableau.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum VarClass {
79    /// Basic variable (appears on LHS of a row).
80    Basic,
81    /// Non-basic variable (appears on RHS).
82    NonBasic,
83}
84
85/// A row in the simplex tableau represents: basic_var = constant + sum(coeff_i * nonbasic_var_i).
86#[derive(Debug, Clone)]
87pub struct Row {
88    /// The basic variable for this row.
89    pub basic_var: VarId,
90    /// The constant term.
91    pub constant: FastRational,
92    /// Coefficients for non-basic variables: var_id -> coefficient.
93    pub coeffs: FxHashMap<VarId, FastRational>,
94}
95
96impl Row {
97    /// Create a new row with a basic variable.
98    pub fn new(basic_var: VarId) -> Self {
99        Self {
100            basic_var,
101            constant: FastRational::zero(),
102            coeffs: FxHashMap::default(),
103        }
104    }
105
106    /// Create a row representing: basic_var = constant + sum(coeffs).
107    pub fn from_expr(
108        basic_var: VarId,
109        constant: FastRational,
110        coeffs: FxHashMap<VarId, FastRational>,
111    ) -> Self {
112        let mut row = Self {
113            basic_var,
114            constant,
115            coeffs: FxHashMap::default(),
116        };
117        for (var, coeff) in coeffs {
118            if !coeff.is_zero() {
119                row.coeffs.insert(var, coeff);
120            }
121        }
122        row
123    }
124
125    /// Get the value of the basic variable given values of non-basic variables.
126    pub fn eval(&self, non_basic_values: &FxHashMap<VarId, FastRational>) -> FastRational {
127        let mut value = self.constant.clone();
128        for (var, coeff) in &self.coeffs {
129            if let Some(val) = non_basic_values.get(var) {
130                value += coeff * val;
131            }
132        }
133        value
134    }
135
136    /// Add a multiple of another row to this row.
137    /// self += multiplier * other
138    pub fn add_row(&mut self, multiplier: &FastRational, other: &Row) {
139        if multiplier.is_zero() {
140            return;
141        }
142
143        self.constant += multiplier * &other.constant;
144
145        for (var, coeff) in &other.coeffs {
146            let new_coeff = self
147                .coeffs
148                .get(var)
149                .cloned()
150                .unwrap_or_else(FastRational::zero)
151                + multiplier * coeff;
152            if new_coeff.is_zero() {
153                self.coeffs.remove(var);
154            } else {
155                self.coeffs.insert(*var, new_coeff);
156            }
157        }
158    }
159
160    /// Multiply the row by a scalar.
161    pub fn scale(&mut self, scalar: &FastRational) {
162        if scalar.is_zero() {
163            self.constant = FastRational::zero();
164            self.coeffs.clear();
165            return;
166        }
167
168        self.constant *= scalar;
169        for coeff in self.coeffs.values_mut() {
170            *coeff *= scalar;
171        }
172    }
173
174    /// Substitute a non-basic variable using another row.
175    /// If var appears in this row, replace it using: var = row.constant + sum(row.coeffs).
176    pub fn substitute(&mut self, var: VarId, row: &Row) {
177        if let Some(coeff) = self.coeffs.remove(&var) {
178            // Add coeff * row to self
179            self.add_row(&coeff, row);
180        }
181    }
182
183    /// Check if the row is valid (no basic variable in RHS).
184    pub fn is_valid(&self, basic_vars: &FxHashSet<VarId>) -> bool {
185        for var in self.coeffs.keys() {
186            if basic_vars.contains(var) {
187                return false;
188            }
189        }
190        true
191    }
192
193    /// Normalize the row by dividing by the GCD of coefficients.
194    pub fn normalize(&mut self) {
195        if self.coeffs.is_empty() {
196            return;
197        }
198
199        // Compute GCD of all coefficients
200        let mut gcd: Option<BigInt> = None;
201        for coeff in self.coeffs.values() {
202            if !coeff.is_zero() {
203                let num = coeff.numer().clone();
204                gcd = Some(match gcd {
205                    None => num.abs(),
206                    Some(g) => gcd_bigint(g, num.abs()),
207                });
208            }
209        }
210
211        if let Some(g) = gcd
212            && !g.is_one()
213        {
214            let divisor = FastRational::from_integer(g);
215            self.scale(&(FastRational::one() / divisor));
216        }
217    }
218}
219
220impl fmt::Display for Row {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        write!(f, "x{} = {}", self.basic_var, self.constant)?;
223        for (var, coeff) in &self.coeffs {
224            if coeff.is_positive() {
225                write!(f, " + {}*x{}", coeff, var)?;
226            } else {
227                write!(f, " - {}*x{}", -coeff, var)?;
228            }
229        }
230        Ok(())
231    }
232}
233
234/// Compute GCD of two BigInts using Euclidean algorithm.
235fn gcd_bigint(mut a: BigInt, mut b: BigInt) -> BigInt {
236    while !b.is_zero() {
237        let t = &a % &b;
238        a = b;
239        b = t;
240    }
241    a.abs()
242}
243
244/// Result of a simplex operation.
245#[derive(Debug, Clone, PartialEq, Eq)]
246pub enum SimplexResult {
247    /// The system is satisfiable.
248    Sat,
249    /// The system is unsatisfiable.
250    Unsat,
251    /// The objective is unbounded.
252    Unbounded,
253    /// Unknown (shouldn't happen in simplex).
254    Unknown,
255}
256
257/// Explanation for why a constraint caused unsatisfiability.
258#[derive(Debug, Clone)]
259pub struct Conflict {
260    /// The constraints involved in the conflict.
261    pub constraints: Vec<ConstraintId>,
262}
263
264/// The Simplex tableau.
265#[derive(Debug, Clone)]
266pub struct SimplexTableau {
267    /// Rows of the tableau, indexed by basic variable.
268    rows: FxHashMap<VarId, Row>,
269    /// Set of basic variables.
270    basic_vars: FxHashSet<VarId>,
271    /// Set of non-basic variables.
272    non_basic_vars: FxHashSet<VarId>,
273    /// Current assignment to all variables.
274    assignment: FxHashMap<VarId, FastRational>,
275    /// Lower bounds for variables.
276    lower_bounds: FxHashMap<VarId, FastRational>,
277    /// Upper bounds for variables.
278    upper_bounds: FxHashMap<VarId, FastRational>,
279    /// Mapping from variables to the constraints that bound them.
280    var_to_constraints: FxHashMap<VarId, Vec<ConstraintId>>,
281    /// Next fresh variable ID.
282    next_var_id: VarId,
283    /// Use Bland's rule to prevent cycling.
284    use_blands_rule: bool,
285}
286
287impl SimplexTableau {
288    /// Create a new empty tableau.
289    pub fn new() -> Self {
290        Self {
291            rows: FxHashMap::default(),
292            basic_vars: FxHashSet::default(),
293            non_basic_vars: FxHashSet::default(),
294            assignment: FxHashMap::default(),
295            lower_bounds: FxHashMap::default(),
296            upper_bounds: FxHashMap::default(),
297            var_to_constraints: FxHashMap::default(),
298            next_var_id: 0,
299            use_blands_rule: true,
300        }
301    }
302
303    /// Create a fresh variable.
304    pub fn fresh_var(&mut self) -> VarId {
305        let id = self.next_var_id;
306        self.next_var_id += 1;
307        self.non_basic_vars.insert(id);
308        self.assignment.insert(id, FastRational::zero());
309        id
310    }
311
312    /// Add a variable with initial bounds.
313    pub fn add_var(&mut self, lower: Option<FastRational>, upper: Option<FastRational>) -> VarId {
314        let var = self.fresh_var();
315        if let Some(lb) = lower {
316            self.lower_bounds.insert(var, lb.clone());
317            self.assignment.insert(var, lb);
318        }
319        if let Some(ub) = upper {
320            self.upper_bounds.insert(var, ub);
321        }
322        var
323    }
324
325    /// Add a row to the tableau.
326    /// The row represents: basic_var = constant + sum(coeffs).
327    pub fn add_row(&mut self, row: Row) -> Result<(), String> {
328        let basic_var = row.basic_var;
329
330        // Ensure basic_var is not already basic
331        if self.basic_vars.contains(&basic_var) {
332            return Err(format!("Variable x{} is already basic", basic_var));
333        }
334
335        // Ensure all non-basic vars in coeffs are indeed non-basic
336        for var in row.coeffs.keys() {
337            if self.basic_vars.contains(var) {
338                return Err(format!("Variable x{} appears in RHS but is basic", var));
339            }
340            self.non_basic_vars.insert(*var);
341        }
342
343        // Move basic_var from non-basic to basic
344        self.non_basic_vars.remove(&basic_var);
345        self.basic_vars.insert(basic_var);
346
347        // Compute initial value for basic_var
348        let value = row.eval(&self.assignment);
349        self.assignment.insert(basic_var, value);
350
351        self.rows.insert(basic_var, row);
352        Ok(())
353    }
354
355    /// Add a bound constraint.
356    pub fn add_bound(
357        &mut self,
358        var: VarId,
359        bound_type: BoundType,
360        value: FastRational,
361        constraint_id: ConstraintId,
362    ) -> Result<(), Conflict> {
363        self.var_to_constraints
364            .entry(var)
365            .or_default()
366            .push(constraint_id);
367
368        match bound_type {
369            BoundType::Lower => {
370                let current_lb = self.lower_bounds.get(&var);
371                if let Some(lb) = current_lb {
372                    if &value > lb {
373                        self.lower_bounds.insert(var, value.clone());
374                    }
375                } else {
376                    self.lower_bounds.insert(var, value.clone());
377                }
378
379                // Check for immediate conflict
380                if let Some(ub) = self.upper_bounds.get(&var)
381                    && &value > ub
382                {
383                    return Err(Conflict {
384                        constraints: vec![constraint_id],
385                    });
386                }
387            }
388            BoundType::Upper => {
389                let current_ub = self.upper_bounds.get(&var);
390                if let Some(ub) = current_ub {
391                    if &value < ub {
392                        self.upper_bounds.insert(var, value.clone());
393                    }
394                } else {
395                    self.upper_bounds.insert(var, value.clone());
396                }
397
398                // Check for immediate conflict
399                if let Some(lb) = self.lower_bounds.get(&var)
400                    && &value < lb
401                {
402                    return Err(Conflict {
403                        constraints: vec![constraint_id],
404                    });
405                }
406            }
407            BoundType::Equal => {
408                self.lower_bounds.insert(var, value.clone());
409                self.upper_bounds.insert(var, value.clone());
410
411                // Check existing bounds
412                if let Some(lb) = self.lower_bounds.get(&var)
413                    && &value < lb
414                {
415                    return Err(Conflict {
416                        constraints: vec![constraint_id],
417                    });
418                }
419                if let Some(ub) = self.upper_bounds.get(&var)
420                    && &value > ub
421                {
422                    return Err(Conflict {
423                        constraints: vec![constraint_id],
424                    });
425                }
426            }
427        }
428
429        // A non-basic variable never appears in `basic_vars`, so
430        // `check()`'s pivoting loop (which only scans `find_violating_basic_var`)
431        // would never notice that this newly tightened bound now conflicts
432        // with the variable's *current* assignment -- letting `check()`
433        // report `Sat` for an infeasible system (e.g. row y = x with bounds
434        // x >= 5, y <= 3: x stays at its stale value 0 forever). Repair it
435        // here (Dutertre-de Moura "update" procedure): snap the non-basic
436        // variable to the violated bound and recompute every basic variable
437        // that depends on it so the tableau's `assignment` map stays
438        // consistent. Any bound violation this produces on a *basic*
439        // variable is then picked up by the normal `check()`/`check_dual()`
440        // pivoting loop.
441        self.repair_non_basic_bound(var);
442
443        Ok(())
444    }
445
446    /// Repair a non-basic variable's assignment so it satisfies its own
447    /// bounds, and propagate the change to all dependent basic variables.
448    ///
449    /// No-op for basic variables: their bound violations are handled by
450    /// `check()`/`check_dual()`'s pivoting loop instead.
451    fn repair_non_basic_bound(&mut self, var: VarId) {
452        if self.basic_vars.contains(&var) {
453            return;
454        }
455        let Some(current) = self.assignment.get(&var).cloned() else {
456            return;
457        };
458
459        let mut new_value = current.clone();
460        if let Some(lb) = self.lower_bounds.get(&var)
461            && &new_value < lb
462        {
463            new_value = lb.clone();
464        }
465        if let Some(ub) = self.upper_bounds.get(&var)
466            && &new_value > ub
467        {
468            new_value = ub.clone();
469        }
470
471        if new_value != current {
472            self.assignment.insert(var, new_value);
473            // Recompute every basic variable's value from the updated
474            // non-basic assignment.
475            self.update_assignment();
476        }
477    }
478
479    /// Get the current assignment to a variable.
480    pub fn get_value(&self, var: VarId) -> Option<&FastRational> {
481        self.assignment.get(&var)
482    }
483
484    /// Check if a basic variable violates its bounds.
485    fn violates_bounds(&self, var: VarId) -> bool {
486        if let Some(val) = self.assignment.get(&var) {
487            if let Some(lb) = self.lower_bounds.get(&var)
488                && val < lb
489            {
490                return true;
491            }
492            if let Some(ub) = self.upper_bounds.get(&var)
493                && val > ub
494            {
495                return true;
496            }
497        }
498        false
499    }
500
501    /// Find a basic variable that violates its bounds.
502    fn find_violating_basic_var(&self) -> Option<VarId> {
503        if self.use_blands_rule {
504            // Use Bland's rule: pick the smallest index
505            self.basic_vars
506                .iter()
507                .filter(|&&var| self.violates_bounds(var))
508                .min()
509                .copied()
510        } else {
511            self.basic_vars
512                .iter()
513                .find(|&&var| self.violates_bounds(var))
514                .copied()
515        }
516    }
517
518    /// Find a non-basic variable to pivot with.
519    /// Returns (non_basic_var, improving_direction).
520    fn find_pivot_non_basic(
521        &self,
522        basic_var: VarId,
523        target_increase: bool,
524    ) -> Option<(VarId, bool)> {
525        let row = self.rows.get(&basic_var)?;
526
527        let mut candidates = Vec::new();
528
529        for (nb_var, coeff) in &row.coeffs {
530            let current_val = self.assignment.get(nb_var)?;
531            let lb = self.lower_bounds.get(nb_var);
532            let ub = self.upper_bounds.get(nb_var);
533
534            // Determine if we can increase or decrease nb_var
535            let can_increase = ub.is_none_or(|bound| bound > current_val);
536            let can_decrease = lb.is_none_or(|bound| bound < current_val);
537
538            // If coeff > 0: increasing nb_var increases basic_var
539            // If coeff < 0: increasing nb_var decreases basic_var
540            let increases_basic = coeff.is_positive();
541
542            if target_increase {
543                // We want to increase basic_var
544                if increases_basic && can_increase {
545                    candidates.push((*nb_var, true));
546                } else if !increases_basic && can_decrease {
547                    candidates.push((*nb_var, false));
548                }
549            } else {
550                // We want to decrease basic_var
551                if increases_basic && can_decrease {
552                    candidates.push((*nb_var, false));
553                } else if !increases_basic && can_increase {
554                    candidates.push((*nb_var, true));
555                }
556            }
557        }
558
559        if candidates.is_empty() {
560            return None;
561        }
562
563        // Use Bland's rule: pick smallest index
564        if self.use_blands_rule {
565            candidates.sort_by_key(|(var, _)| *var);
566        }
567
568        Some(candidates[0])
569    }
570
571    /// Perform a pivot operation.
572    /// Swap basic_var (currently basic) with non_basic_var (currently non-basic).
573    pub fn pivot(&mut self, basic_var: VarId, non_basic_var: VarId) -> Result<(), String> {
574        // Get the row for basic_var
575        let row = self
576            .rows
577            .get(&basic_var)
578            .ok_or_else(|| format!("No row for basic variable x{}", basic_var))?;
579
580        // Get coefficient of non_basic_var in this row
581        let coeff = row
582            .coeffs
583            .get(&non_basic_var)
584            .ok_or_else(|| {
585                format!(
586                    "Non-basic variable x{} not in row for x{}",
587                    non_basic_var, basic_var
588                )
589            })?
590            .clone();
591
592        if coeff.is_zero() {
593            return Err(format!("Coefficient of x{} is zero", non_basic_var));
594        }
595
596        // Solve for non_basic_var in terms of basic_var
597        // Old: basic_var = constant + coeff * non_basic_var + sum(...)
598        // New: non_basic_var = (basic_var - constant - sum(...)) / coeff
599
600        let mut new_row = Row::new(non_basic_var);
601        new_row.constant = -&row.constant / &coeff;
602        new_row
603            .coeffs
604            .insert(basic_var, FastRational::one() / &coeff);
605
606        for (var, c) in &row.coeffs {
607            if var != &non_basic_var {
608                new_row.coeffs.insert(*var, -c / &coeff);
609            }
610        }
611
612        // Substitute non_basic_var in all other rows
613        let rows_to_update: Vec<VarId> = self
614            .rows
615            .keys()
616            .filter(|&&v| v != basic_var)
617            .copied()
618            .collect();
619
620        for row_var in rows_to_update {
621            if let Some(r) = self.rows.get_mut(&row_var) {
622                r.substitute(non_basic_var, &new_row);
623            }
624        }
625
626        // Remove old row and add new row
627        self.rows.remove(&basic_var);
628        self.rows.insert(non_basic_var, new_row);
629
630        // Update basic/non-basic sets
631        self.basic_vars.remove(&basic_var);
632        self.basic_vars.insert(non_basic_var);
633        self.non_basic_vars.remove(&non_basic_var);
634        self.non_basic_vars.insert(basic_var);
635
636        // Update assignments
637        self.update_assignment();
638
639        Ok(())
640    }
641
642    /// Update the assignment based on current tableau.
643    fn update_assignment(&mut self) {
644        // Evaluate all basic variables
645        for (basic_var, row) in &self.rows {
646            let value = row.eval(&self.assignment);
647            self.assignment.insert(*basic_var, value);
648        }
649    }
650
651    /// Check feasibility and fix violations using pivoting.
652    pub fn check(&mut self) -> Result<SimplexResult, Conflict> {
653        let max_iterations = 10000;
654        let mut iterations = 0;
655
656        while let Some(violating_var) = self.find_violating_basic_var() {
657            iterations += 1;
658            if iterations > max_iterations {
659                return Ok(SimplexResult::Unknown);
660            }
661
662            let current_val = self
663                .assignment
664                .get(&violating_var)
665                .cloned()
666                .ok_or_else(|| Conflict {
667                    constraints: vec![],
668                })?;
669
670            let lb = self.lower_bounds.get(&violating_var);
671            let ub = self.upper_bounds.get(&violating_var);
672
673            // Determine if we need to increase or decrease the variable
674            let need_increase = lb.is_some_and(|l| &current_val < l);
675            let need_decrease = ub.is_some_and(|u| &current_val > u);
676
677            if !need_increase && !need_decrease {
678                continue;
679            }
680
681            // Find a non-basic variable to pivot with
682            if let Some((nb_var, _direction)) =
683                self.find_pivot_non_basic(violating_var, need_increase)
684            {
685                // Compute the new value for nb_var
686                let target_value = if need_increase {
687                    lb.cloned().unwrap_or_else(FastRational::zero)
688                } else {
689                    ub.cloned().unwrap_or_else(FastRational::zero)
690                };
691
692                // Update nb_var to move basic_var to target
693                let row = self.rows.get(&violating_var).ok_or_else(|| Conflict {
694                    constraints: vec![],
695                })?;
696                let coeff = row.coeffs.get(&nb_var).cloned().ok_or_else(|| Conflict {
697                    constraints: vec![],
698                })?;
699
700                let delta = &target_value - &current_val;
701                let nb_delta = &delta / &coeff;
702                let current_nb = self
703                    .assignment
704                    .get(&nb_var)
705                    .cloned()
706                    .ok_or_else(|| Conflict {
707                        constraints: vec![],
708                    })?;
709                let new_nb = current_nb + nb_delta;
710
711                // Clamp to bounds
712                let new_nb = if let Some(lb) = self.lower_bounds.get(&nb_var) {
713                    new_nb.max(lb.clone())
714                } else {
715                    new_nb
716                };
717                let new_nb = if let Some(ub) = self.upper_bounds.get(&nb_var) {
718                    new_nb.min(ub.clone())
719                } else {
720                    new_nb
721                };
722
723                self.assignment.insert(nb_var, new_nb);
724                self.update_assignment();
725            } else {
726                // No pivot found - problem is infeasible or unbounded
727                let constraints = self
728                    .var_to_constraints
729                    .get(&violating_var)
730                    .cloned()
731                    .unwrap_or_default();
732                return Err(Conflict { constraints });
733            }
734        }
735
736        Ok(SimplexResult::Sat)
737    }
738
739    /// Dual simplex algorithm for Linear Programming.
740    ///
741    /// The dual simplex maintains dual feasibility (all reduced costs non-negative)
742    /// while working toward primal feasibility (all variables within bounds).
743    ///
744    /// This is particularly useful for:
745    /// - Reoptimization after adding constraints
746    /// - Branch-and-bound in integer programming
747    /// - Problems that naturally start dual-feasible
748    ///
749    /// Reference: Standard LP textbooks and Z3's dual simplex implementation.
750    pub fn check_dual(&mut self) -> Result<SimplexResult, Conflict> {
751        let max_iterations = 10000;
752        let mut iterations = 0;
753
754        // Dual simplex loop: restore primal feasibility
755        while let Some(leaving_var) = self.find_violating_basic_var() {
756            iterations += 1;
757            if iterations > max_iterations {
758                return Ok(SimplexResult::Unknown);
759            }
760
761            // Get the row for the leaving variable
762            let row = match self.rows.get(&leaving_var) {
763                Some(r) => r.clone(),
764                None => continue,
765            };
766
767            let current_val = self
768                .assignment
769                .get(&leaving_var)
770                .cloned()
771                .unwrap_or_else(FastRational::zero);
772
773            let lb = self.lower_bounds.get(&leaving_var);
774            let ub = self.upper_bounds.get(&leaving_var);
775
776            // Determine which bound is violated
777            let violated_lower = lb.is_some_and(|l| &current_val < l);
778            let violated_upper = ub.is_some_and(|u| &current_val > u);
779
780            if !violated_lower && !violated_upper {
781                continue;
782            }
783
784            // For dual simplex, find entering variable using dual pricing rule
785            let entering_var = self.find_entering_var_dual(&row, violated_lower)?;
786
787            // Pivot leaving_var out, entering_var in
788            self.pivot(leaving_var, entering_var)
789                .map_err(|_| Conflict {
790                    constraints: self
791                        .var_to_constraints
792                        .get(&leaving_var)
793                        .cloned()
794                        .unwrap_or_default(),
795                })?;
796        }
797
798        Ok(SimplexResult::Sat)
799    }
800
801    /// Find the entering variable for dual simplex using dual pricing rule.
802    ///
803    /// For dual simplex:
804    /// - If leaving var violates lower bound: choose entering var with negative coeff
805    /// - If leaving var violates upper bound: choose entering var with positive coeff
806    /// - Among candidates, choose one that maintains dual feasibility
807    fn find_entering_var_dual(&self, row: &Row, violated_lower: bool) -> Result<VarId, Conflict> {
808        let mut best_var = None;
809        let mut best_ratio: Option<FastRational> = None;
810
811        // Iterate through non-basic variables in the row
812        for (&nb_var, coeff) in &row.coeffs {
813            // For dual simplex pricing:
814            // - If violated_lower, we need coeff < 0 (to increase basic var)
815            // - If violated_upper, we need coeff > 0 (to decrease basic var)
816            let sign_ok = if violated_lower {
817                coeff.is_negative()
818            } else {
819                coeff.is_positive()
820            };
821
822            if !sign_ok || coeff.is_zero() {
823                continue;
824            }
825
826            // Compute the dual ratio for maintaining dual feasibility
827            // This is a simplified version; full implementation would compute reduced costs
828            let ratio = coeff.abs();
829
830            match &best_ratio {
831                None => {
832                    best_ratio = Some(ratio);
833                    best_var = Some(nb_var);
834                }
835                Some(current_best) => {
836                    // Choose the variable with smallest ratio (steepest descent in dual)
837                    // Use Bland's rule for tie-breaking if enabled
838                    if &ratio < current_best {
839                        best_ratio = Some(ratio);
840                        best_var = Some(nb_var);
841                    } else if self.use_blands_rule && &ratio == current_best {
842                        // Bland's rule: choose smaller index
843                        // best_var is guaranteed Some when best_ratio is Some
844                        if best_var.is_none_or(|bv| nb_var < bv) {
845                            best_var = Some(nb_var);
846                        }
847                    }
848                }
849            }
850        }
851
852        best_var.ok_or(Conflict {
853            constraints: vec![],
854        })
855    }
856
857    /// Get all variables.
858    pub fn vars(&self) -> Vec<VarId> {
859        let mut vars: Vec<VarId> = self
860            .basic_vars
861            .iter()
862            .chain(self.non_basic_vars.iter())
863            .copied()
864            .collect();
865        vars.sort_unstable();
866        vars
867    }
868
869    /// Get all basic variables.
870    pub fn basic_vars(&self) -> Vec<VarId> {
871        let mut vars: Vec<VarId> = self.basic_vars.iter().copied().collect();
872        vars.sort_unstable();
873        vars
874    }
875
876    /// Get all non-basic variables.
877    pub fn non_basic_vars(&self) -> Vec<VarId> {
878        let mut vars: Vec<VarId> = self.non_basic_vars.iter().copied().collect();
879        vars.sort_unstable();
880        vars
881    }
882
883    /// Get the number of rows in the tableau.
884    pub fn num_rows(&self) -> usize {
885        self.rows.len()
886    }
887
888    /// Get the number of variables.
889    pub fn num_vars(&self) -> usize {
890        self.basic_vars.len() + self.non_basic_vars.len()
891    }
892
893    /// Enable or disable Bland's anti-cycling rule.
894    pub fn set_blands_rule(&mut self, enable: bool) {
895        self.use_blands_rule = enable;
896    }
897
898    /// Get the current model (satisfying assignment).
899    /// Returns None if the system is not known to be satisfiable.
900    pub fn get_model(&self) -> Option<FxHashMap<VarId, FastRational>> {
901        // Check if all variables satisfy their bounds
902        for (var, val) in &self.assignment {
903            if let Some(lb) = self.lower_bounds.get(var)
904                && val < lb
905            {
906                return None;
907            }
908            if let Some(ub) = self.upper_bounds.get(var)
909                && val > ub
910            {
911                return None;
912            }
913        }
914        Some(self.assignment.clone())
915    }
916
917    /// Check if the current assignment satisfies all bounds.
918    pub fn is_feasible(&self) -> bool {
919        for (var, val) in &self.assignment {
920            if let Some(lb) = self.lower_bounds.get(var)
921                && val < lb
922            {
923                return false;
924            }
925            if let Some(ub) = self.upper_bounds.get(var)
926                && val > ub
927            {
928                return false;
929            }
930        }
931        true
932    }
933
934    /// Find a variable that violates its bounds, if any.
935    pub fn find_violated_bound(&self) -> Option<VarId> {
936        for (var, val) in &self.assignment {
937            if let Some(lb) = self.lower_bounds.get(var)
938                && val < lb
939            {
940                return Some(*var);
941            }
942            if let Some(ub) = self.upper_bounds.get(var)
943                && val > ub
944            {
945                return Some(*var);
946            }
947        }
948        None
949    }
950
951    /// Get all constraints associated with a variable.
952    pub fn get_constraints(&self, var: VarId) -> Vec<ConstraintId> {
953        self.var_to_constraints
954            .get(&var)
955            .cloned()
956            .unwrap_or_default()
957    }
958
959    /// Extract a minimal conflicting core from constraints.
960    /// This is a simple implementation that returns all constraints involved.
961    /// A more sophisticated version would compute a true minimal unsat core.
962    pub fn get_unsat_core(&self, conflict: &Conflict) -> Vec<ConstraintId> {
963        conflict.constraints.clone()
964    }
965
966    /// Theory propagation: deduce new bounds from existing constraints.
967    /// Returns a list of (var, bound_type, value) tuples representing deduced bounds.
968    pub fn propagate(&self) -> Vec<(VarId, BoundType, FastRational)> {
969        let mut propagated = Vec::new();
970
971        // For each row: basic_var = constant + sum(coeff * non_basic_var)
972        // We can deduce bounds on basic_var from bounds on non_basic vars
973        for row in self.rows.values() {
974            let basic_var = row.basic_var;
975
976            // Compute lower bound: min value of basic_var
977            // basic_var >= constant + sum(min(coeff * lb, coeff * ub))
978            let mut lower_bound = row.constant.clone();
979            let mut has_finite_lower = true;
980
981            for (var, coeff) in &row.coeffs {
982                if coeff.is_positive() {
983                    // Positive coeff: use lower bound of var
984                    if let Some(lb) = self.lower_bounds.get(var) {
985                        lower_bound += coeff * lb;
986                    } else {
987                        has_finite_lower = false;
988                        break;
989                    }
990                } else {
991                    // Negative coeff: use upper bound of var
992                    if let Some(ub) = self.upper_bounds.get(var) {
993                        lower_bound += coeff * ub;
994                    } else {
995                        has_finite_lower = false;
996                        break;
997                    }
998                }
999            }
1000
1001            if has_finite_lower {
1002                // Check if this is a tighter bound
1003                if let Some(current_lb) = self.lower_bounds.get(&basic_var) {
1004                    if &lower_bound > current_lb {
1005                        propagated.push((basic_var, BoundType::Lower, lower_bound.clone()));
1006                    }
1007                } else {
1008                    propagated.push((basic_var, BoundType::Lower, lower_bound.clone()));
1009                }
1010            }
1011
1012            // Compute upper bound: max value of basic_var
1013            let mut upper_bound = row.constant.clone();
1014            let mut has_finite_upper = true;
1015
1016            for (var, coeff) in &row.coeffs {
1017                if coeff.is_positive() {
1018                    // Positive coeff: use upper bound of var
1019                    if let Some(ub) = self.upper_bounds.get(var) {
1020                        upper_bound += coeff * ub;
1021                    } else {
1022                        has_finite_upper = false;
1023                        break;
1024                    }
1025                } else {
1026                    // Negative coeff: use lower bound of var
1027                    if let Some(lb) = self.lower_bounds.get(var) {
1028                        upper_bound += coeff * lb;
1029                    } else {
1030                        has_finite_upper = false;
1031                        break;
1032                    }
1033                }
1034            }
1035
1036            if has_finite_upper {
1037                // Check if this is a tighter bound
1038                if let Some(current_ub) = self.upper_bounds.get(&basic_var) {
1039                    if &upper_bound < current_ub {
1040                        propagated.push((basic_var, BoundType::Upper, upper_bound.clone()));
1041                    }
1042                } else {
1043                    propagated.push((basic_var, BoundType::Upper, upper_bound.clone()));
1044                }
1045            }
1046        }
1047
1048        propagated
1049    }
1050
1051    /// Assert a linear constraint: sum(coeffs) + constant {<=, >=, ==} 0.
1052    /// Returns a new slack variable if needed, and the constraint ID.
1053    pub fn assert_constraint(
1054        &mut self,
1055        coeffs: FxHashMap<VarId, FastRational>,
1056        constant: FastRational,
1057        bound_type: BoundType,
1058        constraint_id: ConstraintId,
1059    ) -> Result<VarId, Conflict> {
1060        // Create a fresh slack variable for the constraint
1061        let slack_var = self.fresh_var();
1062
1063        // slack_var = sum(coeffs) + constant
1064        let row = Row::from_expr(slack_var, constant, coeffs);
1065        self.add_row(row).map_err(|_| Conflict {
1066            constraints: vec![constraint_id],
1067        })?;
1068
1069        // Add appropriate bound on slack_var based on bound_type
1070        let zero = FastRational::zero();
1071        match bound_type {
1072            BoundType::Lower => {
1073                // sum(coeffs) + constant >= 0  =>  slack_var >= 0
1074                self.add_bound(slack_var, BoundType::Lower, zero, constraint_id)?;
1075            }
1076            BoundType::Upper => {
1077                // sum(coeffs) + constant <= 0  =>  slack_var <= 0
1078                self.add_bound(slack_var, BoundType::Upper, zero, constraint_id)?;
1079            }
1080            BoundType::Equal => {
1081                // sum(coeffs) + constant == 0  =>  slack_var == 0
1082                self.add_bound(slack_var, BoundType::Equal, zero.clone(), constraint_id)?;
1083                // Equal means both lower and upper bound
1084                self.add_bound(slack_var, BoundType::Lower, zero.clone(), constraint_id)?;
1085                self.add_bound(slack_var, BoundType::Upper, zero, constraint_id)?;
1086            }
1087        }
1088
1089        Ok(slack_var)
1090    }
1091}
1092
1093impl Default for SimplexTableau {
1094    fn default() -> Self {
1095        Self::new()
1096    }
1097}
1098
1099impl fmt::Display for SimplexTableau {
1100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1101        writeln!(f, "Simplex Tableau:")?;
1102        writeln!(f, "  Basic variables: {:?}", self.basic_vars())?;
1103        writeln!(f, "  Non-basic variables: {:?}", self.non_basic_vars())?;
1104        writeln!(f, "  Rows:")?;
1105        for row in self.rows.values() {
1106            writeln!(f, "    {}", row)?;
1107        }
1108        writeln!(f, "  Assignment:")?;
1109        for var in self.vars() {
1110            if let Some(val) = self.assignment.get(&var) {
1111                write!(f, "    x{} = {}", var, val)?;
1112                if let Some(lb) = self.lower_bounds.get(&var) {
1113                    write!(f, " (>= {})", lb)?;
1114                }
1115                if let Some(ub) = self.upper_bounds.get(&var) {
1116                    write!(f, " (<= {})", ub)?;
1117                }
1118                writeln!(f)?;
1119            }
1120        }
1121        Ok(())
1122    }
1123}
1124
1125#[cfg(test)]
1126mod tests {
1127    use super::*;
1128
1129    fn rat(n: i64) -> FastRational {
1130        FastRational::from_integer(BigInt::from(n))
1131    }
1132
1133    #[test]
1134    fn test_row_eval() {
1135        let mut row = Row::new(0);
1136        row.constant = rat(5);
1137        row.coeffs.insert(1, rat(2));
1138        row.coeffs.insert(2, rat(3));
1139
1140        let mut values = FxHashMap::default();
1141        values.insert(1, rat(1));
1142        values.insert(2, rat(2));
1143
1144        // 5 + 2*1 + 3*2 = 5 + 2 + 6 = 13
1145        assert_eq!(row.eval(&values), rat(13));
1146    }
1147
1148    #[test]
1149    fn test_row_add() {
1150        let mut row1 = Row::new(0);
1151        row1.constant = rat(5);
1152        row1.coeffs.insert(1, rat(2));
1153
1154        let mut row2 = Row::new(0);
1155        row2.constant = rat(3);
1156        row2.coeffs.insert(1, rat(1));
1157        row2.coeffs.insert(2, rat(4));
1158
1159        // row1 += 2 * row2
1160        // row1 = 5 + 2*x1 + 2*(3 + x1 + 4*x2)
1161        // row1 = 5 + 2*x1 + 6 + 2*x1 + 8*x2
1162        // row1 = 11 + 4*x1 + 8*x2
1163        row1.add_row(&rat(2), &row2);
1164        assert_eq!(row1.constant, rat(11));
1165        assert_eq!(row1.coeffs.get(&1), Some(&rat(4)));
1166        assert_eq!(row1.coeffs.get(&2), Some(&rat(8)));
1167    }
1168
1169    #[test]
1170    fn test_simplex_basic() {
1171        let mut tableau = SimplexTableau::new();
1172
1173        // Variables: x0, x1
1174        // Constraint: x0 + x1 <= 10
1175        // Introduce slack: x2 = 10 - x0 - x1
1176
1177        let x0 = tableau.fresh_var();
1178        let x1 = tableau.fresh_var();
1179        let x2 = tableau.fresh_var();
1180
1181        // x2 = 10 - x0 - x1
1182        let mut row = Row::new(x2);
1183        row.constant = rat(10);
1184        row.coeffs.insert(x0, rat(-1));
1185        row.coeffs.insert(x1, rat(-1));
1186
1187        tableau.add_row(row).expect("test operation should succeed");
1188
1189        // Bounds: x0 >= 0, x1 >= 0, x2 >= 0
1190        tableau
1191            .add_bound(x0, BoundType::Lower, rat(0), 0)
1192            .expect("test operation should succeed");
1193        tableau
1194            .add_bound(x1, BoundType::Lower, rat(0), 1)
1195            .expect("test operation should succeed");
1196        tableau
1197            .add_bound(x2, BoundType::Lower, rat(0), 2)
1198            .expect("test operation should succeed");
1199
1200        let result = tableau.check().expect("test operation should succeed");
1201        assert_eq!(result, SimplexResult::Sat);
1202    }
1203
1204    #[test]
1205    fn test_simplex_infeasible() {
1206        let mut tableau = SimplexTableau::new();
1207
1208        let x = tableau.fresh_var();
1209
1210        // x >= 5 and x <= 3 (conflicting bounds)
1211        tableau
1212            .add_bound(x, BoundType::Lower, rat(5), 0)
1213            .expect("test operation should succeed");
1214        let result = tableau.add_bound(x, BoundType::Upper, rat(3), 1);
1215
1216        assert!(result.is_err());
1217    }
1218
1219    #[test]
1220    fn test_simplex_pivot() {
1221        let mut tableau = SimplexTableau::new();
1222
1223        let x0 = tableau.fresh_var();
1224        let x1 = tableau.fresh_var();
1225        let x2 = tableau.fresh_var();
1226
1227        // x2 = 10 - 2*x0 - 3*x1
1228        let mut row = Row::new(x2);
1229        row.constant = rat(10);
1230        row.coeffs.insert(x0, rat(-2));
1231        row.coeffs.insert(x1, rat(-3));
1232
1233        tableau.add_row(row).expect("test operation should succeed");
1234
1235        // Pivot x2 and x0
1236        tableau
1237            .pivot(x2, x0)
1238            .expect("test operation should succeed");
1239
1240        // After pivot: x0 = (10 - x2 - 3*x1) / 2 = 5 - x2/2 - 3*x1/2
1241        assert!(tableau.basic_vars.contains(&x0));
1242        assert!(tableau.non_basic_vars.contains(&x2));
1243
1244        let new_row = tableau.rows.get(&x0).expect("key should exist in map");
1245        assert_eq!(new_row.constant, rat(5));
1246    }
1247
1248    #[test]
1249    fn test_simplex_dual() {
1250        let mut tableau = SimplexTableau::new();
1251
1252        // Test dual simplex with a simple LP
1253        // Variables: x0, x1
1254        // Constraint: x0 + x1 <= 10
1255        // Bounds: x0 >= 0, x1 >= 0
1256
1257        let x0 = tableau.fresh_var();
1258        let x1 = tableau.fresh_var();
1259        let x2 = tableau.fresh_var(); // slack variable
1260
1261        // x2 = 10 - x0 - x1
1262        let mut row = Row::new(x2);
1263        row.constant = rat(10);
1264        row.coeffs.insert(x0, rat(-1));
1265        row.coeffs.insert(x1, rat(-1));
1266
1267        tableau.add_row(row).expect("test operation should succeed");
1268
1269        // Bounds: x0 >= 0, x1 >= 0, x2 >= 0
1270        tableau
1271            .add_bound(x0, BoundType::Lower, rat(0), 0)
1272            .expect("test operation should succeed");
1273        tableau
1274            .add_bound(x1, BoundType::Lower, rat(0), 1)
1275            .expect("test operation should succeed");
1276        tableau
1277            .add_bound(x2, BoundType::Lower, rat(0), 2)
1278            .expect("test operation should succeed");
1279
1280        // Use dual simplex
1281        let result = tableau.check_dual().expect("test operation should succeed");
1282        assert_eq!(result, SimplexResult::Sat);
1283
1284        // Verify the solution is feasible
1285        assert!(tableau.is_feasible());
1286    }
1287
1288    #[test]
1289    fn test_simplex_dual_with_bounds() {
1290        let mut tableau = SimplexTableau::new();
1291
1292        // Test dual simplex with tighter bounds
1293        let x0 = tableau.fresh_var();
1294        let x1 = tableau.fresh_var();
1295        let x2 = tableau.fresh_var();
1296
1297        // x2 = 10 - x0 - x1
1298        let mut row = Row::new(x2);
1299        row.constant = rat(10);
1300        row.coeffs.insert(x0, rat(-1));
1301        row.coeffs.insert(x1, rat(-1));
1302
1303        tableau.add_row(row).expect("test operation should succeed");
1304
1305        // Bounds: 0 <= x0 <= 5, 0 <= x1 <= 5, x2 >= 0
1306        tableau
1307            .add_bound(x0, BoundType::Lower, rat(0), 0)
1308            .expect("test operation should succeed");
1309        tableau
1310            .add_bound(x0, BoundType::Upper, rat(5), 1)
1311            .expect("test operation should succeed");
1312        tableau
1313            .add_bound(x1, BoundType::Lower, rat(0), 2)
1314            .expect("test operation should succeed");
1315        tableau
1316            .add_bound(x1, BoundType::Upper, rat(5), 3)
1317            .expect("test operation should succeed");
1318        tableau
1319            .add_bound(x2, BoundType::Lower, rat(0), 4)
1320            .expect("test operation should succeed");
1321
1322        // Use dual simplex
1323        let result = tableau.check_dual().expect("test operation should succeed");
1324        assert_eq!(result, SimplexResult::Sat);
1325
1326        // Verify the solution is feasible
1327        assert!(tableau.is_feasible());
1328    }
1329
1330    /// Regression test for the audit finding: a non-basic variable whose
1331    /// bound is tightened past its current assignment was never repaired,
1332    /// so `check()` could report `Sat` for an infeasible system.
1333    ///
1334    /// Row: y = x (x non-basic, y basic).
1335    /// Bounds: x >= 5, then y <= 3.
1336    /// x >= 5 forces x (and hence y, via the row) up to 5, so y <= 3 is now
1337    /// unsatisfiable: `check()` must never report `Sat`.
1338    #[test]
1339    fn test_add_bound_repairs_non_basic_var_violating_own_bound() {
1340        let mut tableau = SimplexTableau::new();
1341
1342        let x = tableau.fresh_var();
1343        let y = tableau.fresh_var();
1344
1345        let mut row = Row::new(y);
1346        row.coeffs.insert(x, rat(1));
1347        tableau.add_row(row).expect("test operation should succeed");
1348
1349        tableau
1350            .add_bound(x, BoundType::Lower, rat(5), 0)
1351            .expect("x >= 5 alone is not a direct conflict");
1352
1353        // x's assignment must have been repaired to 5, and y (which depends
1354        // on x via the row) must have followed it to 5 -- this is the
1355        // Dutertre-de Moura "update" that was previously missing entirely.
1356        assert_eq!(tableau.get_value(x), Some(&rat(5)));
1357        assert_eq!(
1358            tableau.get_value(y),
1359            Some(&rat(5)),
1360            "basic variable y=x must be recomputed when non-basic x is repaired"
1361        );
1362
1363        // y <= 3 does not directly conflict with any bound *recorded* on y
1364        // (y has no prior bound), so add_bound may return Ok -- but y's
1365        // *current* value (5, basic) now violates it.
1366        let result = tableau.add_bound(y, BoundType::Upper, rat(3), 1);
1367
1368        match result {
1369            Err(_) => {
1370                // Immediate conflict detected at add_bound time is also
1371                // acceptable.
1372            }
1373            Ok(()) => {
1374                // Otherwise, check() must catch the bound-violating basic
1375                // variable and report the system as infeasible -- never Sat.
1376                let check_result = tableau.check();
1377                assert!(
1378                    check_result.is_err(),
1379                    "check() must not report the infeasible system \
1380                     (x>=5, y<=3, y=x) as satisfiable: {:?}",
1381                    check_result
1382                );
1383            }
1384        }
1385    }
1386
1387    /// A non-basic variable's bound tightening that does *not* conflict
1388    /// with the rest of the system must still leave `check()` reporting
1389    /// `Sat`, and the repaired value must be visible via `get_value`
1390    /// immediately (not only after calling `check()`).
1391    #[test]
1392    fn test_add_bound_repairs_non_basic_var_stays_sat_when_consistent() {
1393        let mut tableau = SimplexTableau::new();
1394
1395        let x = tableau.fresh_var();
1396        let y = tableau.fresh_var();
1397
1398        let mut row = Row::new(y);
1399        row.coeffs.insert(x, rat(1));
1400        tableau.add_row(row).expect("test operation should succeed");
1401
1402        // y <= 100 leaves plenty of room for x >= 5.
1403        tableau
1404            .add_bound(y, BoundType::Upper, rat(100), 0)
1405            .expect("test operation should succeed");
1406        tableau
1407            .add_bound(x, BoundType::Lower, rat(5), 1)
1408            .expect("test operation should succeed");
1409
1410        assert_eq!(tableau.get_value(x), Some(&rat(5)));
1411        assert_eq!(tableau.get_value(y), Some(&rat(5)));
1412
1413        let result = tableau.check().expect("test operation should succeed");
1414        assert_eq!(result, SimplexResult::Sat);
1415        assert!(tableau.is_feasible());
1416    }
1417}