Skip to main content

oxiz_math/simd/
simplex_simd.rs

1//! SIMD-Optimized Simplex Algorithm.
2#![allow(clippy::needless_range_loop)] // Simplex algorithm uses explicit indexing
3//!
4//! Provides cache-friendly simplex operations for linear programming.
5
6#[allow(unused_imports)]
7use crate::prelude::*;
8use core::ops::{Add, Div, Mul, Sub};
9use num_traits::Float;
10
11/// SIMD-friendly simplex tableau.
12#[derive(Debug, Clone)]
13pub struct SimplexTableau<T> {
14    /// Tableau matrix (includes slack variables and RHS)
15    pub tableau: Vec<Vec<T>>,
16    /// Basic variable indices
17    pub basis: Vec<usize>,
18    /// Number of original variables
19    pub num_vars: usize,
20    /// Number of constraints
21    pub num_constraints: usize,
22}
23
24impl<T> SimplexTableau<T>
25where
26    T: Clone + Add<Output = T> + Sub<Output = T> + Mul<Output = T> + Div<Output = T> + Float,
27{
28    /// Create a new simplex tableau.
29    pub fn new(constraints: Vec<Vec<T>>, objective: Vec<T>, rhs: Vec<T>) -> Self {
30        let num_constraints = constraints.len();
31        let num_vars = objective.len();
32
33        // Build initial tableau with slack variables
34        let total_vars = num_vars + num_constraints;
35        let mut tableau = vec![vec![T::zero(); total_vars + 1]; num_constraints + 1];
36
37        // Add constraints
38        for (i, constraint) in constraints.iter().enumerate() {
39            for (j, &val) in constraint.iter().enumerate() {
40                tableau[i][j] = val;
41            }
42            // Add slack variable
43            tableau[i][num_vars + i] = T::one();
44            // Add RHS
45            tableau[i][total_vars] = rhs[i];
46        }
47
48        // Add objective function (negated for maximization)
49        for (j, &val) in objective.iter().enumerate() {
50            tableau[num_constraints][j] = T::zero() - val;
51        }
52
53        // Initial basis is slack variables
54        let basis = (num_vars..(num_vars + num_constraints)).collect();
55
56        Self {
57            tableau,
58            basis,
59            num_vars,
60            num_constraints,
61        }
62    }
63
64    /// Perform one iteration of the simplex algorithm.
65    pub fn pivot(&mut self) -> Result<bool, SimplexError> {
66        // Find entering variable (most negative coefficient in objective row)
67        let entering_col = self.find_entering_variable()?;
68
69        if entering_col.is_none() {
70            return Ok(true); // Optimal solution found
71        }
72
73        let entering = entering_col.expect("entering_col should be valid");
74
75        // Find leaving variable using minimum ratio test
76        let leaving_row = self.find_leaving_variable(entering)?;
77
78        if leaving_row.is_none() {
79            return Err(SimplexError::Unbounded);
80        }
81
82        let leaving = leaving_row.expect("leaving_row should be valid");
83
84        // Perform pivot operation
85        self.perform_pivot(entering, leaving);
86
87        // Update basis
88        self.basis[leaving] = entering;
89
90        Ok(false) // Not optimal yet
91    }
92
93    /// Find entering variable (most negative coefficient).
94    fn find_entering_variable(&self) -> Result<Option<usize>, SimplexError> {
95        let obj_row = &self.tableau[self.num_constraints];
96        let total_vars = self.num_vars + self.num_constraints;
97
98        let mut min_val = T::zero();
99        let mut min_idx = None;
100
101        for j in 0..total_vars {
102            if obj_row[j] < min_val {
103                min_val = obj_row[j];
104                min_idx = Some(j);
105            }
106        }
107
108        Ok(min_idx)
109    }
110
111    /// Find leaving variable using minimum ratio test.
112    fn find_leaving_variable(&self, entering: usize) -> Result<Option<usize>, SimplexError> {
113        let total_vars = self.num_vars + self.num_constraints;
114        let rhs_col = total_vars;
115
116        let mut min_ratio = T::infinity();
117        let mut min_idx = None;
118
119        for i in 0..self.num_constraints {
120            let coeff = self.tableau[i][entering];
121
122            if coeff > T::epsilon() {
123                let rhs = self.tableau[i][rhs_col];
124                let ratio = rhs / coeff;
125
126                if ratio < min_ratio {
127                    min_ratio = ratio;
128                    min_idx = Some(i);
129                }
130            }
131        }
132
133        Ok(min_idx)
134    }
135
136    /// Perform pivot operation with SIMD-friendly access pattern.
137    fn perform_pivot(&mut self, entering: usize, leaving: usize) {
138        let total_vars = self.num_vars + self.num_constraints;
139        let num_cols = total_vars + 1;
140
141        // Get pivot element
142        let pivot = self.tableau[leaving][entering];
143
144        if pivot.abs() < T::epsilon() {
145            return; // Avoid division by zero
146        }
147
148        // Normalize pivot row
149        for j in 0..num_cols {
150            self.tableau[leaving][j] = self.tableau[leaving][j] / pivot;
151        }
152
153        // Eliminate column in other rows
154        // Process in chunks for cache locality
155        const CHUNK_SIZE: usize = 8;
156
157        for i in 0..=self.num_constraints {
158            if i == leaving {
159                continue;
160            }
161
162            let factor = self.tableau[i][entering];
163
164            if factor.abs() < T::epsilon() {
165                continue;
166            }
167
168            // Process columns in chunks
169            for chunk_start in (0..num_cols).step_by(CHUNK_SIZE) {
170                let chunk_end = (chunk_start + CHUNK_SIZE).min(num_cols);
171
172                for j in chunk_start..chunk_end {
173                    let update = self.tableau[leaving][j] * factor;
174                    self.tableau[i][j] = self.tableau[i][j] - update;
175                }
176            }
177        }
178    }
179
180    /// Get current solution.
181    pub fn get_solution(&self) -> Vec<T> {
182        let mut solution = vec![T::zero(); self.num_vars];
183        let total_vars = self.num_vars + self.num_constraints;
184        let rhs_col = total_vars;
185
186        for (row, &var_idx) in self.basis.iter().enumerate() {
187            if var_idx < self.num_vars {
188                solution[var_idx] = self.tableau[row][rhs_col];
189            }
190        }
191
192        solution
193    }
194
195    /// Get objective value.
196    pub fn get_objective_value(&self) -> T {
197        let total_vars = self.num_vars + self.num_constraints;
198        let rhs_col = total_vars;
199        self.tableau[self.num_constraints][rhs_col]
200    }
201
202    /// Check if current solution is feasible.
203    pub fn is_feasible(&self) -> bool {
204        let total_vars = self.num_vars + self.num_constraints;
205        let rhs_col = total_vars;
206
207        for i in 0..self.num_constraints {
208            if self.tableau[i][rhs_col] < T::zero() - T::epsilon() {
209                return false;
210            }
211        }
212
213        true
214    }
215}
216
217/// Solve linear program using simplex algorithm.
218pub fn simd_simplex_solve<T>(
219    constraints: Vec<Vec<T>>,
220    objective: Vec<T>,
221    rhs: Vec<T>,
222    max_iterations: usize,
223) -> Result<SimplexSolution<T>, SimplexError>
224where
225    T: Clone + Add<Output = T> + Sub<Output = T> + Mul<Output = T> + Div<Output = T> + Float,
226{
227    let mut tableau = SimplexTableau::new(constraints, objective, rhs);
228
229    if !tableau.is_feasible() {
230        return Err(SimplexError::Infeasible);
231    }
232
233    let mut iterations = 0;
234
235    loop {
236        if iterations >= max_iterations {
237            return Err(SimplexError::MaxIterationsReached);
238        }
239
240        let optimal = tableau.pivot()?;
241
242        if optimal {
243            break;
244        }
245
246        iterations += 1;
247    }
248
249    Ok(SimplexSolution {
250        solution: tableau.get_solution(),
251        objective_value: tableau.get_objective_value(),
252        iterations,
253    })
254}
255
256/// Result of simplex optimization.
257#[derive(Debug, Clone)]
258pub struct SimplexSolution<T> {
259    /// Optimal solution
260    pub solution: Vec<T>,
261    /// Optimal objective value
262    pub objective_value: T,
263    /// Number of iterations
264    pub iterations: usize,
265}
266
267/// Errors in simplex algorithm.
268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269pub enum SimplexError {
270    /// Problem is infeasible
271    Infeasible,
272    /// Problem is unbounded
273    Unbounded,
274    /// Maximum iterations reached
275    MaxIterationsReached,
276}
277
278impl core::fmt::Display for SimplexError {
279    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
280        match self {
281            Self::Infeasible => write!(f, "linear program is infeasible"),
282            Self::Unbounded => write!(f, "linear program is unbounded"),
283            Self::MaxIterationsReached => write!(f, "maximum iterations reached"),
284        }
285    }
286}
287
288impl core::error::Error for SimplexError {}
289
290/// Dual simplex algorithm for handling infeasibility.
291pub fn simd_dual_simplex<T>(
292    constraints: Vec<Vec<T>>,
293    objective: Vec<T>,
294    rhs: Vec<T>,
295    max_iterations: usize,
296) -> Result<SimplexSolution<T>, SimplexError>
297where
298    T: Clone + Add<Output = T> + Sub<Output = T> + Mul<Output = T> + Div<Output = T> + Float,
299{
300    let mut tableau = SimplexTableau::new(constraints, objective, rhs);
301
302    let mut iterations = 0;
303
304    loop {
305        if iterations >= max_iterations {
306            return Err(SimplexError::MaxIterationsReached);
307        }
308
309        // Check if optimal
310        if tableau.is_feasible() && is_dual_feasible(&tableau) {
311            break;
312        }
313
314        // Find leaving variable (most negative RHS)
315        let leaving_row = find_dual_leaving_variable(&tableau)?;
316
317        if leaving_row.is_none() {
318            return Err(SimplexError::Infeasible);
319        }
320
321        let leaving = leaving_row.expect("leaving_row should be valid");
322
323        // Find entering variable
324        let entering_col = find_dual_entering_variable(&tableau, leaving)?;
325
326        if entering_col.is_none() {
327            return Err(SimplexError::Infeasible);
328        }
329
330        let entering = entering_col.expect("entering_col should be valid");
331
332        // Perform pivot
333        tableau.perform_pivot(entering, leaving);
334        tableau.basis[leaving] = entering;
335
336        iterations += 1;
337    }
338
339    Ok(SimplexSolution {
340        solution: tableau.get_solution(),
341        objective_value: tableau.get_objective_value(),
342        iterations,
343    })
344}
345
346fn is_dual_feasible<T>(tableau: &SimplexTableau<T>) -> bool
347where
348    T: Clone + Float,
349{
350    let total_vars = tableau.num_vars + tableau.num_constraints;
351    let obj_row = &tableau.tableau[tableau.num_constraints];
352
353    for j in 0..total_vars {
354        if obj_row[j] < T::zero() - T::epsilon() {
355            return false;
356        }
357    }
358
359    true
360}
361
362fn find_dual_leaving_variable<T>(tableau: &SimplexTableau<T>) -> Result<Option<usize>, SimplexError>
363where
364    T: Clone + Float,
365{
366    let total_vars = tableau.num_vars + tableau.num_constraints;
367    let rhs_col = total_vars;
368
369    let mut min_val = T::zero();
370    let mut min_idx = None;
371
372    for i in 0..tableau.num_constraints {
373        let rhs = tableau.tableau[i][rhs_col];
374        if rhs < min_val {
375            min_val = rhs;
376            min_idx = Some(i);
377        }
378    }
379
380    Ok(min_idx)
381}
382
383fn find_dual_entering_variable<T>(
384    tableau: &SimplexTableau<T>,
385    leaving: usize,
386) -> Result<Option<usize>, SimplexError>
387where
388    T: Clone + Float + Div<Output = T>,
389{
390    let total_vars = tableau.num_vars + tableau.num_constraints;
391    let obj_row = &tableau.tableau[tableau.num_constraints];
392    let leaving_row = &tableau.tableau[leaving];
393
394    let mut min_ratio = T::infinity();
395    let mut min_idx = None;
396
397    for j in 0..total_vars {
398        let coeff = leaving_row[j];
399
400        if coeff < T::zero() - T::epsilon() {
401            let obj_coeff = obj_row[j];
402            let ratio = obj_coeff / (T::zero() - coeff);
403
404            if ratio < min_ratio {
405                min_ratio = ratio;
406                min_idx = Some(j);
407            }
408        }
409    }
410
411    Ok(min_idx)
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417
418    #[test]
419    fn test_simplex_basic() {
420        // Maximize: 3x + 2y
421        // Subject to: x + y <= 4, 2x + y <= 5, x, y >= 0
422        let constraints = vec![vec![1.0, 1.0], vec![2.0, 1.0]];
423        let objective = vec![3.0, 2.0];
424        let rhs = vec![4.0, 5.0];
425
426        let result = simd_simplex_solve(constraints, objective, rhs, 100).expect("simplex failed");
427
428        assert!(result.iterations > 0);
429        assert!(result.objective_value > 0.0);
430    }
431
432    #[test]
433    fn test_simplex_unbounded() {
434        // Maximize: x + y
435        // Subject to: -x + y <= 1
436        let constraints = vec![vec![-1.0, 1.0]];
437        let objective = vec![1.0, 1.0];
438        let rhs = vec![1.0];
439
440        let result = simd_simplex_solve(constraints, objective, rhs, 100);
441
442        assert!(result.is_err());
443    }
444
445    #[test]
446    fn test_tableau_creation() {
447        let constraints = vec![vec![1.0, 1.0]];
448        let objective = vec![1.0, 1.0];
449        let rhs = vec![5.0];
450
451        let tableau = SimplexTableau::new(constraints, objective, rhs);
452
453        assert_eq!(tableau.num_vars, 2);
454        assert_eq!(tableau.num_constraints, 1);
455        assert_eq!(tableau.basis.len(), 1);
456    }
457}