oxiz_math/lp/
dual_simplex.rs1#[allow(unused_imports)]
7use crate::prelude::*;
8use num_rational::BigRational;
9use num_traits::{Signed, Zero};
10
11#[derive(Clone)]
13pub struct DualSimplexSolver {
14 tableau: Vec<Vec<BigRational>>,
16 basis: Vec<VarId>,
18 non_basis: Vec<VarId>,
20 objective: Vec<BigRational>,
22 stats: DualSimplexStats,
24}
25
26pub type VarId = usize;
28
29#[derive(Debug, Clone, Default)]
31pub struct DualSimplexStats {
32 pub iterations: usize,
34 pub pivots: usize,
36 pub ratio_tests: usize,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum DualSimplexResult {
43 Optimal,
45 Infeasible,
47 Unbounded,
49 Unknown,
51}
52
53impl DualSimplexSolver {
54 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 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 if self.is_dual_feasible() {
74 return DualSimplexResult::Optimal;
75 }
76
77 let leaving_idx = match self.select_leaving_variable() {
79 Some(idx) => idx,
80 None => return DualSimplexResult::Infeasible,
81 };
82
83 let entering_idx = match self.select_entering_variable(leaving_idx) {
85 Some(idx) => idx,
86 None => return DualSimplexResult::Unbounded,
87 };
88
89 self.pivot(leaving_idx, entering_idx);
91 self.stats.pivots += 1;
92 }
93
94 DualSimplexResult::Unknown
95 }
96
97 fn is_dual_feasible(&self) -> bool {
99 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 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 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 if coeff >= &BigRational::zero() {
143 continue;
144 }
145
146 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 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; }
174
175 for elem in &mut self.tableau[leaving_row] {
177 *elem = &*elem / &pivot_element;
178 }
179
180 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 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 self.basis[leaving_row] = self.non_basis[entering_col];
202 self.non_basis[entering_col] = leaving_row;
203 }
204
205 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 pub fn get_objective_value(&self) -> BigRational {
220 self.objective
221 .last()
222 .cloned()
223 .unwrap_or_else(BigRational::zero)
224 }
225
226 pub fn stats(&self) -> &DualSimplexStats {
228 &self.stats
229 }
230
231 pub fn num_vars(&self) -> usize {
233 self.non_basis.len()
234 }
235
236 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 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}