Skip to main content

oxiz_math/lp/
dual_simplex.rs

1//! Dual Simplex Algorithm for Linear Programming.
2//!
3//! Implements the dual simplex method for solving LPs, particularly useful
4//! for handling infeasibility and for sensitivity analysis.
5
6#[allow(unused_imports)]
7use crate::prelude::*;
8use num_rational::BigRational;
9use num_traits::{Signed, Zero};
10
11/// Dual simplex solver for linear programming.
12#[derive(Clone)]
13pub struct DualSimplexSolver {
14    /// Current tableau
15    tableau: Vec<Vec<BigRational>>,
16    /// Basic variables
17    basis: Vec<VarId>,
18    /// Non-basic variables
19    non_basis: Vec<VarId>,
20    /// Objective row
21    objective: Vec<BigRational>,
22    /// Statistics
23    stats: DualSimplexStats,
24}
25
26/// Variable identifier
27pub type VarId = usize;
28
29/// Dual simplex statistics
30#[derive(Debug, Clone, Default)]
31pub struct DualSimplexStats {
32    /// Number of iterations
33    pub iterations: usize,
34    /// Number of pivot operations
35    pub pivots: usize,
36    /// Number of ratio tests
37    pub ratio_tests: usize,
38}
39
40/// Result of dual simplex solving
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum DualSimplexResult {
43    /// Optimal solution found
44    Optimal,
45    /// Problem is infeasible
46    Infeasible,
47    /// Problem is unbounded
48    Unbounded,
49    /// Unknown (iteration limit reached)
50    Unknown,
51}
52
53impl DualSimplexSolver {
54    /// Create a new dual simplex solver.
55    pub fn new(num_vars: usize, num_constraints: usize) -> Self {
56        Self {
57            tableau: vec![vec![BigRational::zero(); num_vars + 1]; num_constraints],
58            basis: (0..num_constraints).collect(),
59            non_basis: (0..num_vars).collect(),
60            objective: vec![BigRational::zero(); num_vars + 1],
61            stats: DualSimplexStats::default(),
62        }
63    }
64
65    /// Solve using dual simplex method.
66    pub fn solve(&mut self) -> DualSimplexResult {
67        const MAX_ITERATIONS: usize = 10000;
68
69        for _ in 0..MAX_ITERATIONS {
70            self.stats.iterations += 1;
71
72            // Check for optimality: all RHS values non-negative
73            if self.is_dual_feasible() {
74                return DualSimplexResult::Optimal;
75            }
76
77            // Select leaving variable (most negative RHS)
78            let leaving_idx = match self.select_leaving_variable() {
79                Some(idx) => idx,
80                None => return DualSimplexResult::Infeasible,
81            };
82
83            // Select entering variable (dual ratio test)
84            let entering_idx = match self.select_entering_variable(leaving_idx) {
85                Some(idx) => idx,
86                None => return DualSimplexResult::Unbounded,
87            };
88
89            // Perform pivot
90            self.pivot(leaving_idx, entering_idx);
91            self.stats.pivots += 1;
92        }
93
94        DualSimplexResult::Unknown
95    }
96
97    /// Check if current solution is dual feasible.
98    fn is_dual_feasible(&self) -> bool {
99        // All RHS values (last column) should be non-negative
100        for row in &self.tableau {
101            if let Some(rhs) = row.last()
102                && rhs < &BigRational::zero()
103            {
104                return false;
105            }
106        }
107        true
108    }
109
110    /// Select leaving variable (row with most negative RHS).
111    fn select_leaving_variable(&self) -> Option<usize> {
112        let mut min_rhs = BigRational::zero();
113        let mut leaving_idx = None;
114
115        for (i, row) in self.tableau.iter().enumerate() {
116            if let Some(rhs) = row.last()
117                && rhs < &min_rhs
118            {
119                min_rhs = rhs.clone();
120                leaving_idx = Some(i);
121            }
122        }
123
124        leaving_idx
125    }
126
127    /// Select entering variable using dual ratio test.
128    fn select_entering_variable(&mut self, leaving_row: usize) -> Option<usize> {
129        self.stats.ratio_tests += 1;
130
131        let mut min_ratio = None;
132        let mut entering_idx = None;
133
134        let leaving_row_vec = &self.tableau[leaving_row];
135
136        for (j, coeff) in leaving_row_vec
137            .iter()
138            .enumerate()
139            .take(leaving_row_vec.len() - 1)
140        {
141            // Only consider negative coefficients
142            if coeff >= &BigRational::zero() {
143                continue;
144            }
145
146            // Compute ratio: objective[j] / |coeff|
147            let obj_coeff = &self.objective[j];
148            let ratio = obj_coeff / coeff.abs();
149
150            match &min_ratio {
151                None => {
152                    min_ratio = Some(ratio.clone());
153                    entering_idx = Some(j);
154                }
155                Some(current_min) => {
156                    if ratio < *current_min {
157                        min_ratio = Some(ratio);
158                        entering_idx = Some(j);
159                    }
160                }
161            }
162        }
163
164        entering_idx
165    }
166
167    /// Perform pivot operation.
168    fn pivot(&mut self, leaving_row: usize, entering_col: usize) {
169        let pivot_element = self.tableau[leaving_row][entering_col].clone();
170
171        if pivot_element.is_zero() {
172            return; // Degenerate pivot
173        }
174
175        // Normalize pivot row
176        for elem in &mut self.tableau[leaving_row] {
177            *elem = &*elem / &pivot_element;
178        }
179
180        // Eliminate other rows
181        for i in 0..self.tableau.len() {
182            if i == leaving_row {
183                continue;
184            }
185
186            let multiplier = self.tableau[i][entering_col].clone();
187            for j in 0..self.tableau[i].len() {
188                let pivot_row_elem = &self.tableau[leaving_row][j];
189                self.tableau[i][j] = &self.tableau[i][j] - &multiplier * pivot_row_elem;
190            }
191        }
192
193        // Update objective row
194        let obj_multiplier = self.objective[entering_col].clone();
195        for j in 0..self.objective.len() {
196            let pivot_row_elem = &self.tableau[leaving_row][j];
197            self.objective[j] = &self.objective[j] - &obj_multiplier * pivot_row_elem;
198        }
199
200        // Update basis
201        self.basis[leaving_row] = self.non_basis[entering_col];
202        self.non_basis[entering_col] = leaving_row;
203    }
204
205    /// Get current solution.
206    pub fn get_solution(&self) -> FxHashMap<VarId, BigRational> {
207        let mut solution = FxHashMap::default();
208
209        for (i, &var_id) in self.basis.iter().enumerate() {
210            if let Some(rhs) = self.tableau[i].last() {
211                solution.insert(var_id, rhs.clone());
212            }
213        }
214
215        solution
216    }
217
218    /// Get objective value.
219    pub fn get_objective_value(&self) -> BigRational {
220        self.objective
221            .last()
222            .cloned()
223            .unwrap_or_else(BigRational::zero)
224    }
225
226    /// Get statistics.
227    pub fn stats(&self) -> &DualSimplexStats {
228        &self.stats
229    }
230
231    /// Return the number of decision variables (columns, excluding the RHS column).
232    pub fn num_vars(&self) -> usize {
233        self.non_basis.len()
234    }
235
236    /// Add constraint to tableau.
237    pub fn add_constraint(&mut self, coeffs: Vec<BigRational>, rhs: BigRational) {
238        let mut row = coeffs;
239        row.push(rhs);
240        self.tableau.push(row);
241    }
242
243    /// Set objective function.
244    pub fn set_objective(&mut self, coeffs: Vec<BigRational>) {
245        self.objective = coeffs;
246        self.objective.push(BigRational::zero());
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn test_dual_simplex_creation() {
256        let solver = DualSimplexSolver::new(3, 2);
257        assert_eq!(solver.stats.iterations, 0);
258    }
259
260    #[test]
261    fn test_is_dual_feasible() {
262        let solver = DualSimplexSolver::new(2, 1);
263        assert!(solver.is_dual_feasible());
264    }
265}