1#[allow(unused_imports)]
31use crate::prelude::*;
32use num_bigint::BigInt;
33use num_rational::BigRational;
34use num_traits::{Signed, Zero};
35use thiserror::Error;
36
37#[derive(Debug, Error, Clone)]
41pub enum SimplexError {
42 #[error("index {index} out of bounds (len {len})")]
44 IndexOutOfBounds {
45 index: usize,
47 len: usize,
49 },
50
51 #[error("shadow_price called before solve()")]
53 NotYetSolved,
54
55 #[error("shadow_price undefined: problem is infeasible")]
57 Infeasible,
58
59 #[error("shadow_price undefined: problem is unbounded")]
61 Unbounded,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum ConstraintKind {
67 Le,
69 Ge,
71 Eq,
73}
74
75#[derive(Debug, Clone)]
77pub struct Constraint {
78 pub coefficients: Vec<BigRational>,
80 pub rhs: BigRational,
82 pub kind: ConstraintKind,
84}
85
86impl Constraint {
87 pub fn le(coeffs: impl Into<Vec<BigRational>>, rhs: BigRational) -> Self {
89 Self {
90 coefficients: coeffs.into(),
91 rhs,
92 kind: ConstraintKind::Le,
93 }
94 }
95
96 pub fn ge(coeffs: impl Into<Vec<BigRational>>, rhs: BigRational) -> Self {
98 Self {
99 coefficients: coeffs.into(),
100 rhs,
101 kind: ConstraintKind::Ge,
102 }
103 }
104
105 pub fn eq(coeffs: impl Into<Vec<BigRational>>, rhs: BigRational) -> Self {
107 Self {
108 coefficients: coeffs.into(),
109 rhs,
110 kind: ConstraintKind::Eq,
111 }
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum SolveStatus {
118 Optimal,
120 Infeasible,
122 Unbounded,
124}
125
126#[derive(Debug, Clone)]
128pub struct SolveResult {
129 pub status: SolveStatus,
131 pub values: Vec<BigRational>,
133 pub objective: BigRational,
135 pub shadow_prices: Vec<BigRational>,
138}
139
140#[derive(Debug, Clone)]
146pub struct SimplexSolver {
147 n_vars: usize,
149 obj_coeffs: Vec<BigRational>,
151 constraints: Vec<Constraint>,
153 last_result: Option<SolveResult>,
155}
156
157impl SimplexSolver {
158 pub fn new(obj_coeffs: Vec<BigRational>, constraints: Vec<Constraint>) -> Self {
163 let n_vars = obj_coeffs.len();
164 Self {
165 n_vars,
166 obj_coeffs,
167 constraints,
168 last_result: None,
169 }
170 }
171
172 pub fn set_objective_coefficient(
176 &mut self,
177 i: usize,
178 val: BigRational,
179 ) -> Result<(), SimplexError> {
180 if i >= self.n_vars {
181 return Err(SimplexError::IndexOutOfBounds {
182 index: i,
183 len: self.n_vars,
184 });
185 }
186 self.obj_coeffs[i] = val;
187 self.last_result = None;
189 Ok(())
190 }
191
192 pub fn get_objective_coefficient(&self, i: usize) -> Result<&BigRational, SimplexError> {
194 self.obj_coeffs
195 .get(i)
196 .ok_or(SimplexError::IndexOutOfBounds {
197 index: i,
198 len: self.n_vars,
199 })
200 }
201
202 pub fn get_rhs(&self, i: usize) -> Result<&BigRational, SimplexError> {
204 self.constraints
205 .get(i)
206 .map(|c| &c.rhs)
207 .ok_or(SimplexError::IndexOutOfBounds {
208 index: i,
209 len: self.constraints.len(),
210 })
211 }
212
213 pub fn set_rhs(&mut self, i: usize, val: BigRational) -> Result<(), SimplexError> {
215 let len = self.constraints.len();
216 self.constraints
217 .get_mut(i)
218 .ok_or(SimplexError::IndexOutOfBounds { index: i, len })?
219 .rhs = val;
220 self.last_result = None;
221 Ok(())
222 }
223
224 pub fn shadow_price(&self, i: usize) -> Result<BigRational, SimplexError> {
232 let result = self
233 .last_result
234 .as_ref()
235 .ok_or(SimplexError::NotYetSolved)?;
236
237 match result.status {
238 SolveStatus::Infeasible => return Err(SimplexError::Infeasible),
239 SolveStatus::Unbounded => return Err(SimplexError::Unbounded),
240 SolveStatus::Optimal => {}
241 }
242
243 result
244 .shadow_prices
245 .get(i)
246 .cloned()
247 .ok_or(SimplexError::IndexOutOfBounds {
248 index: i,
249 len: result.shadow_prices.len(),
250 })
251 }
252
253 pub fn objective_value(&self) -> Option<BigRational> {
255 self.last_result
256 .as_ref()
257 .filter(|r| r.status == SolveStatus::Optimal)
258 .map(|r| r.objective.clone())
259 }
260
261 pub fn solve(&mut self) -> Result<SolveResult, SimplexError> {
271 let result = self.run_bigm_simplex(&self.obj_coeffs.clone(), &self.constraints.clone());
272 self.last_result = Some(result.clone());
273 Ok(result)
274 }
275
276 fn run_bigm_simplex(
288 &self,
289 obj_coeffs: &[BigRational],
290 constraints: &[Constraint],
291 ) -> SolveResult {
292 let n = obj_coeffs.len(); let m = constraints.len(); if m == 0 {
295 return SolveResult {
297 status: SolveStatus::Optimal,
298 values: vec![BigRational::zero(); n],
299 objective: BigRational::zero(),
300 shadow_prices: Vec::new(),
301 };
302 }
303
304 let big_m = {
307 let max_coeff = obj_coeffs
308 .iter()
309 .map(|c| c.abs())
310 .fold(BigRational::zero(), |a, b| if a >= b { a } else { b });
311 let base = BigRational::new(BigInt::from(1_000_000i64), BigInt::from(1i64));
312 let scaled =
313 &base * (&max_coeff + BigRational::new(BigInt::from(1i64), BigInt::from(1i64)));
314 if scaled < base { base } else { scaled }
315 };
316
317 let n_artificial: usize = constraints
319 .iter()
320 .filter(|c| matches!(c.kind, ConstraintKind::Ge | ConstraintKind::Eq))
321 .count();
322
323 let total_cols = n + m + n_artificial;
325 let rhs_col = total_cols; let mut tab: Vec<Vec<BigRational>> = vec![vec![BigRational::zero(); total_cols + 1]; m];
330
331 let mut obj_row: Vec<BigRational> = vec![BigRational::zero(); total_cols + 1];
333
334 let mut basis: Vec<usize> = vec![0usize; m];
336
337 let mut art_idx = n + m; for (i, c) in constraints.iter().enumerate() {
340 for (j, coeff) in c.coefficients.iter().enumerate() {
342 if j < n {
343 tab[i][j] = coeff.clone();
344 }
345 }
346
347 let slack_col = n + i;
349
350 match c.kind {
351 ConstraintKind::Le => {
352 tab[i][slack_col] = BigRational::new(BigInt::from(1i64), BigInt::from(1i64));
354 tab[i][rhs_col] = c.rhs.clone();
355 basis[i] = slack_col; }
358 ConstraintKind::Ge => {
359 tab[i][slack_col] = BigRational::new(BigInt::from(-1i64), BigInt::from(1i64));
361 tab[i][art_idx] = BigRational::new(BigInt::from(1i64), BigInt::from(1i64));
362 tab[i][rhs_col] = c.rhs.clone();
363 basis[i] = art_idx;
364 obj_row[art_idx] = big_m.clone();
366 art_idx += 1;
367 }
368 ConstraintKind::Eq => {
369 tab[i][art_idx] = BigRational::new(BigInt::from(1i64), BigInt::from(1i64));
371 tab[i][rhs_col] = c.rhs.clone();
372 basis[i] = art_idx;
373 obj_row[art_idx] = big_m.clone();
374 art_idx += 1;
375 }
376 }
377
378 if tab[i][rhs_col] < BigRational::zero() {
380 for elem in &mut tab[i] {
381 *elem = -elem.clone();
382 }
383 }
387 }
388
389 for (j, c) in obj_coeffs.iter().enumerate() {
391 obj_row[j] = c.clone();
392 }
393
394 for i in 0..m {
399 let bv = basis[i];
400 if bv >= n + m {
401 let factor = obj_row[bv].clone();
403 if !factor.is_zero() {
404 for j in 0..=total_cols {
405 let t = tab[i][j].clone();
406 obj_row[j] = obj_row[j].clone() - &factor * &t;
407 }
408 }
409 }
410 }
411
412 const MAX_ITERS: usize = 50_000;
414
415 for _iter in 0..MAX_ITERS {
416 let entering = match Self::find_entering(&obj_row, total_cols) {
418 Some(e) => e,
419 None => break, };
421
422 let leaving = match Self::find_leaving(&tab, m, entering, rhs_col) {
424 Some(l) => l,
425 None => {
426 return SolveResult {
428 status: SolveStatus::Unbounded,
429 values: vec![BigRational::zero(); n],
430 objective: BigRational::zero(),
431 shadow_prices: vec![BigRational::zero(); m],
432 };
433 }
434 };
435
436 Self::pivot_tableau(
438 &mut tab,
439 &mut obj_row,
440 &mut basis,
441 m,
442 leaving,
443 entering,
444 rhs_col,
445 );
446 }
447
448 let mut values = vec![BigRational::zero(); n];
451 for (i, &bv) in basis.iter().enumerate() {
452 if bv < n {
453 values[bv] = tab[i][rhs_col].clone();
454 } else if bv >= n + m {
455 if tab[i][rhs_col].abs()
457 > BigRational::new(BigInt::from(1i64), BigInt::from(1_000_000i64))
458 {
459 return SolveResult {
460 status: SolveStatus::Infeasible,
461 values: vec![BigRational::zero(); n],
462 objective: BigRational::zero(),
463 shadow_prices: vec![BigRational::zero(); m],
464 };
465 }
466 }
468 }
469
470 let mut objective = BigRational::zero();
472 for (j, c) in obj_coeffs.iter().enumerate() {
473 objective += c * &values[j];
474 }
475
476 let shadow_prices: Vec<BigRational> = constraints
480 .iter()
481 .enumerate()
482 .map(|(i, c)| {
483 let slack_col = n + i;
484 let rc = &obj_row[slack_col];
485 match c.kind {
486 ConstraintKind::Le => -rc.clone(),
487 ConstraintKind::Ge => rc.clone(),
488 ConstraintKind::Eq => -rc.clone(),
489 }
490 })
491 .collect();
492
493 SolveResult {
494 status: SolveStatus::Optimal,
495 values,
496 objective,
497 shadow_prices,
498 }
499 }
500
501 fn find_entering(obj_row: &[BigRational], total_cols: usize) -> Option<usize> {
503 let mut best_col = None;
504 let mut best_val = BigRational::zero();
505 for (j, val) in obj_row.iter().enumerate().take(total_cols) {
506 if *val < best_val {
507 best_val = val.clone();
508 best_col = Some(j);
509 }
510 }
511 best_col
512 }
513
514 fn find_leaving(
516 tab: &[Vec<BigRational>],
517 m: usize,
518 entering: usize,
519 rhs_col: usize,
520 ) -> Option<usize> {
521 let mut best_row = None;
522 let mut best_ratio: Option<BigRational> = None;
523
524 for (i, row) in tab.iter().enumerate().take(m) {
525 let a_ie = &row[entering];
526 if a_ie <= &BigRational::zero() {
527 continue; }
529 let ratio = &row[rhs_col] / a_ie;
530 match &best_ratio {
531 None => {
532 best_ratio = Some(ratio);
533 best_row = Some(i);
534 }
535 Some(br) => {
536 if ratio < *br {
537 best_ratio = Some(ratio);
538 best_row = Some(i);
539 }
540 }
541 }
542 }
543
544 best_row
545 }
546
547 fn pivot_tableau(
549 tab: &mut [Vec<BigRational>],
550 obj_row: &mut [BigRational],
551 basis: &mut [usize],
552 m: usize,
553 leaving: usize,
554 entering: usize,
555 rhs_col: usize,
556 ) {
557 let pivot = tab[leaving][entering].clone();
558 let total_cols_plus_one = rhs_col + 1;
559
560 for elem in tab[leaving].iter_mut().take(total_cols_plus_one) {
562 let v = elem.clone();
563 *elem = v / &pivot;
564 }
565
566 for i in 0..m {
568 if i == leaving {
569 continue;
570 }
571 let factor = tab[i][entering].clone();
572 if factor.is_zero() {
573 continue;
574 }
575 let pivot_row: Vec<BigRational> = tab[leaving]
577 .iter()
578 .take(total_cols_plus_one)
579 .cloned()
580 .collect();
581 for (j, pv) in pivot_row.iter().enumerate() {
582 let v = tab[i][j].clone();
583 tab[i][j] = v - &factor * pv;
584 }
585 }
586
587 let obj_factor = obj_row[entering].clone();
589 if !obj_factor.is_zero() {
590 let pivot_row: Vec<BigRational> = tab[leaving]
591 .iter()
592 .take(total_cols_plus_one)
593 .cloned()
594 .collect();
595 for (j, pv) in pivot_row.iter().enumerate() {
596 let v = obj_row[j].clone();
597 obj_row[j] = v - &obj_factor * pv;
598 }
599 }
600
601 basis[leaving] = entering;
602 }
603
604 pub fn n_vars(&self) -> usize {
606 self.n_vars
607 }
608
609 pub fn n_constraints(&self) -> usize {
611 self.constraints.len()
612 }
613
614 pub fn obj_coeffs(&self) -> &[BigRational] {
616 &self.obj_coeffs
617 }
618
619 pub fn constraints(&self) -> &[Constraint] {
621 &self.constraints
622 }
623
624 pub fn last_result(&self) -> Option<&SolveResult> {
626 self.last_result.as_ref()
627 }
628}
629
630#[cfg(test)]
634pub(crate) fn big_rat(num: i64, den: i64) -> BigRational {
635 BigRational::new(BigInt::from(num), BigInt::from(den))
636}
637
638#[cfg(test)]
641mod tests {
642 use super::*;
643
644 fn r(n: i64) -> BigRational {
645 big_rat(n, 1)
646 }
647
648 fn make_lp_2var(
656 c: (i64, i64),
657 rows: &[(i64, i64, i64)], ) -> SimplexSolver {
659 let obj = vec![r(c.0), r(c.1)];
660 let constraints = rows
661 .iter()
662 .map(|&(a1, a2, b)| Constraint::le(vec![r(a1), r(a2)], r(b)))
663 .collect();
664 SimplexSolver::new(obj, constraints)
665 }
666
667 #[test]
670 fn test_simple_2var_optimal() {
671 let obj = vec![r(1), r(1)];
674 let constraints = vec![
675 Constraint::ge(vec![r(1), r(1)], r(4)),
676 Constraint::le(vec![r(1), r(0)], r(3)),
677 Constraint::le(vec![r(0), r(1)], r(3)),
678 ];
679 let mut solver = SimplexSolver::new(obj, constraints);
680 let result = solver.solve().expect("solve should succeed");
681 assert_eq!(result.status, SolveStatus::Optimal);
682 assert!(result.objective <= r(6));
685 assert!(result.objective >= r(0));
686 }
687
688 #[test]
689 fn test_lp_with_known_optimal() {
690 let obj = vec![r(-2), r(-3)];
694 let constraints = vec![
695 Constraint::le(vec![r(1), r(1)], r(4)),
696 Constraint::le(vec![r(1), r(0)], r(2)),
697 Constraint::le(vec![r(0), r(1)], r(3)),
698 ];
699 let mut solver = SimplexSolver::new(obj, constraints);
700 let result = solver.solve().expect("solve should succeed");
701 assert_eq!(result.status, SolveStatus::Optimal);
702 assert!(result.objective <= r(0));
704 }
705
706 #[test]
707 fn test_trivially_feasible() {
708 let mut solver = make_lp_2var((1, 0), &[(1, 0, 5)]);
710 let result = solver.solve().expect("solve should succeed");
712 assert_eq!(result.status, SolveStatus::Optimal);
713 assert!(result.objective >= r(0));
715 }
716
717 #[test]
720 fn test_set_objective_coefficient() {
721 let mut solver = make_lp_2var((1, 1), &[(1, 1, 10)]);
722 solver
724 .set_objective_coefficient(0, r(2))
725 .expect("index 0 should be valid");
726 assert_eq!(solver.obj_coeffs()[0], r(2));
727 assert!(solver.last_result().is_none());
729 }
730
731 #[test]
732 fn test_set_objective_coefficient_oob() {
733 let mut solver = make_lp_2var((1, 1), &[(1, 1, 10)]);
734 let err = solver
735 .set_objective_coefficient(5, r(1))
736 .expect_err("out-of-bounds index should error");
737 assert!(matches!(err, SimplexError::IndexOutOfBounds { .. }));
738 }
739
740 #[test]
743 fn test_shadow_price_before_solve_errors() {
744 let solver = make_lp_2var((1, 1), &[(1, 1, 10)]);
745 let err = solver
746 .shadow_price(0)
747 .expect_err("calling shadow_price before solve should error");
748 assert!(matches!(err, SimplexError::NotYetSolved));
749 }
750
751 #[test]
752 fn test_shadow_price_after_solve() {
753 let obj = vec![r(-1)];
757 let constraints = vec![Constraint::le(vec![r(1)], r(5))];
758 let mut solver = SimplexSolver::new(obj, constraints);
759 solver.solve().expect("solve should succeed");
760 let sp = solver.shadow_price(0).expect("shadow_price(0) should work");
761 assert!(sp <= r(0));
765 }
766
767 #[test]
768 fn test_shadow_price_verification() {
769 let obj = vec![r(-3), r(-5)];
773 let constraints = vec![
774 Constraint::le(vec![r(1), r(0)], r(4)),
775 Constraint::le(vec![r(0), r(2)], r(12)),
776 Constraint::le(vec![r(3), r(5)], r(25)),
777 ];
778 let mut solver = SimplexSolver::new(obj.clone(), constraints);
779 let result = solver.solve().expect("solve should succeed");
780 assert_eq!(result.status, SolveStatus::Optimal);
781
782 let sp0 = solver.shadow_price(0).expect("shadow_price 0 should work");
785 let sp2 = solver.shadow_price(2).expect("shadow_price 2 should work");
787
788 assert!(sp0 <= r(0) || sp0 == r(0));
791 let _ = sp2; }
793
794 #[test]
797 fn test_parametric_rhs_perturbation() {
798 let obj = vec![r(-1)];
801 let constraints = vec![Constraint::le(vec![r(1)], r(5))];
802 let mut solver = SimplexSolver::new(obj, constraints);
803
804 let result5 = solver.solve().expect("solve b=5 should succeed");
805
806 solver.set_rhs(0, r(6)).expect("set_rhs should work");
807 let result6 = solver.solve().expect("solve b=6 should succeed");
808
809 assert_eq!(result5.status, SolveStatus::Optimal);
811 assert_eq!(result6.status, SolveStatus::Optimal);
812
813 let delta = result6.objective.clone() - result5.objective.clone();
815 assert_eq!(delta, r(-1));
816 }
817
818 #[test]
819 fn test_set_rhs_invalidates_cache() {
820 let mut solver = make_lp_2var((1, 0), &[(1, 0, 5)]);
821 solver.solve().expect("first solve should work");
822 assert!(solver.last_result().is_some());
823 solver.set_rhs(0, r(10)).expect("set_rhs should work");
824 assert!(solver.last_result().is_none());
825 }
826
827 #[test]
832 fn test_all_accessors() {
833 let obj = vec![r(3), r(2)];
835 let constraints = vec![
836 Constraint::le(vec![r(1), r(1)], r(10)),
837 Constraint::le(vec![r(1), r(0)], r(6)),
838 ];
839 let mut solver = SimplexSolver::new(obj, constraints);
840
841 assert_eq!(solver.n_vars(), 2);
843 assert_eq!(solver.n_constraints(), 2);
844
845 assert_eq!(solver.obj_coeffs(), &[r(3), r(2)]);
847
848 assert_eq!(solver.constraints().len(), 2);
850 assert_eq!(solver.constraints()[0].rhs, r(10));
851
852 assert_eq!(
854 *solver.get_objective_coefficient(1).expect("index 1 valid"),
855 r(2)
856 );
857 let err = solver.get_objective_coefficient(99).expect_err("oob");
858 assert!(matches!(err, SimplexError::IndexOutOfBounds { .. }));
859
860 assert_eq!(*solver.get_rhs(0).expect("index 0 valid"), r(10));
862 let err2 = solver.get_rhs(99).expect_err("oob");
863 assert!(matches!(err2, SimplexError::IndexOutOfBounds { .. }));
864
865 assert!(solver.last_result().is_none());
867
868 assert!(solver.objective_value().is_none());
870
871 let result = solver.solve().expect("solve should succeed");
873 assert_eq!(result.status, SolveStatus::Optimal);
874
875 assert!(solver.last_result().is_some());
877 assert_eq!(solver.last_result().unwrap().status, SolveStatus::Optimal);
878
879 let ov = solver
881 .objective_value()
882 .expect("objective_value should be Some");
883 assert_eq!(ov, r(0));
885
886 let sp0 = solver.shadow_price(0).expect("shadow_price(0)");
888 let sp1 = solver.shadow_price(1).expect("shadow_price(1)");
889 assert!(sp0 <= r(0));
891 assert!(sp1 <= r(0));
892
893 solver.set_rhs(1, r(4)).expect("set_rhs index 1");
895 assert_eq!(*solver.get_rhs(1).expect("get_rhs index 1"), r(4));
896 }
897}