1use num_bigint::BigInt;
31use num_rational::BigRational;
32use num_traits::{Signed, Zero};
33use thiserror::Error;
34
35#[derive(Debug, Error, Clone)]
39pub enum SimplexError {
40 #[error("index {index} out of bounds (len {len})")]
42 IndexOutOfBounds {
43 index: usize,
45 len: usize,
47 },
48
49 #[error("shadow_price called before solve()")]
51 NotYetSolved,
52
53 #[error("shadow_price undefined: problem is infeasible")]
55 Infeasible,
56
57 #[error("shadow_price undefined: problem is unbounded")]
59 Unbounded,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum ConstraintKind {
65 Le,
67 Ge,
69 Eq,
71}
72
73#[derive(Debug, Clone)]
75pub struct Constraint {
76 pub coefficients: Vec<BigRational>,
78 pub rhs: BigRational,
80 pub kind: ConstraintKind,
82}
83
84impl Constraint {
85 pub fn le(coeffs: impl Into<Vec<BigRational>>, rhs: BigRational) -> Self {
87 Self {
88 coefficients: coeffs.into(),
89 rhs,
90 kind: ConstraintKind::Le,
91 }
92 }
93
94 pub fn ge(coeffs: impl Into<Vec<BigRational>>, rhs: BigRational) -> Self {
96 Self {
97 coefficients: coeffs.into(),
98 rhs,
99 kind: ConstraintKind::Ge,
100 }
101 }
102
103 pub fn eq(coeffs: impl Into<Vec<BigRational>>, rhs: BigRational) -> Self {
105 Self {
106 coefficients: coeffs.into(),
107 rhs,
108 kind: ConstraintKind::Eq,
109 }
110 }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum SolveStatus {
116 Optimal,
118 Infeasible,
120 Unbounded,
122}
123
124#[derive(Debug, Clone)]
126pub struct SolveResult {
127 pub status: SolveStatus,
129 pub values: Vec<BigRational>,
131 pub objective: BigRational,
133 pub shadow_prices: Vec<BigRational>,
136}
137
138#[derive(Debug, Clone)]
144pub struct SimplexSolver {
145 n_vars: usize,
147 obj_coeffs: Vec<BigRational>,
149 constraints: Vec<Constraint>,
151 last_result: Option<SolveResult>,
153}
154
155impl SimplexSolver {
156 pub fn new(obj_coeffs: Vec<BigRational>, constraints: Vec<Constraint>) -> Self {
161 let n_vars = obj_coeffs.len();
162 Self {
163 n_vars,
164 obj_coeffs,
165 constraints,
166 last_result: None,
167 }
168 }
169
170 pub fn set_objective_coefficient(
174 &mut self,
175 i: usize,
176 val: BigRational,
177 ) -> Result<(), SimplexError> {
178 if i >= self.n_vars {
179 return Err(SimplexError::IndexOutOfBounds {
180 index: i,
181 len: self.n_vars,
182 });
183 }
184 self.obj_coeffs[i] = val;
185 self.last_result = None;
187 Ok(())
188 }
189
190 pub fn get_objective_coefficient(&self, i: usize) -> Result<&BigRational, SimplexError> {
192 self.obj_coeffs
193 .get(i)
194 .ok_or(SimplexError::IndexOutOfBounds {
195 index: i,
196 len: self.n_vars,
197 })
198 }
199
200 pub fn get_rhs(&self, i: usize) -> Result<&BigRational, SimplexError> {
202 self.constraints
203 .get(i)
204 .map(|c| &c.rhs)
205 .ok_or(SimplexError::IndexOutOfBounds {
206 index: i,
207 len: self.constraints.len(),
208 })
209 }
210
211 pub fn set_rhs(&mut self, i: usize, val: BigRational) -> Result<(), SimplexError> {
213 let len = self.constraints.len();
214 self.constraints
215 .get_mut(i)
216 .ok_or(SimplexError::IndexOutOfBounds { index: i, len })?
217 .rhs = val;
218 self.last_result = None;
219 Ok(())
220 }
221
222 pub fn shadow_price(&self, i: usize) -> Result<BigRational, SimplexError> {
230 let result = self
231 .last_result
232 .as_ref()
233 .ok_or(SimplexError::NotYetSolved)?;
234
235 match result.status {
236 SolveStatus::Infeasible => return Err(SimplexError::Infeasible),
237 SolveStatus::Unbounded => return Err(SimplexError::Unbounded),
238 SolveStatus::Optimal => {}
239 }
240
241 result
242 .shadow_prices
243 .get(i)
244 .cloned()
245 .ok_or(SimplexError::IndexOutOfBounds {
246 index: i,
247 len: result.shadow_prices.len(),
248 })
249 }
250
251 pub fn objective_value(&self) -> Option<BigRational> {
253 self.last_result
254 .as_ref()
255 .filter(|r| r.status == SolveStatus::Optimal)
256 .map(|r| r.objective.clone())
257 }
258
259 pub fn solve(&mut self) -> Result<SolveResult, SimplexError> {
269 let result = self.run_bigm_simplex(&self.obj_coeffs.clone(), &self.constraints.clone());
270 self.last_result = Some(result.clone());
271 Ok(result)
272 }
273
274 fn run_bigm_simplex(
286 &self,
287 obj_coeffs: &[BigRational],
288 constraints: &[Constraint],
289 ) -> SolveResult {
290 let n = obj_coeffs.len(); let m = constraints.len(); if m == 0 {
293 return SolveResult {
295 status: SolveStatus::Optimal,
296 values: vec![BigRational::zero(); n],
297 objective: BigRational::zero(),
298 shadow_prices: Vec::new(),
299 };
300 }
301
302 let big_m = {
305 let max_coeff = obj_coeffs
306 .iter()
307 .map(|c| c.abs())
308 .fold(BigRational::zero(), |a, b| if a >= b { a } else { b });
309 let base = BigRational::new(BigInt::from(1_000_000i64), BigInt::from(1i64));
310 let scaled =
311 &base * (&max_coeff + BigRational::new(BigInt::from(1i64), BigInt::from(1i64)));
312 if scaled < base { base } else { scaled }
313 };
314
315 let n_artificial: usize = constraints
317 .iter()
318 .filter(|c| matches!(c.kind, ConstraintKind::Ge | ConstraintKind::Eq))
319 .count();
320
321 let total_cols = n + m + n_artificial;
323 let rhs_col = total_cols; let mut tab: Vec<Vec<BigRational>> = vec![vec![BigRational::zero(); total_cols + 1]; m];
328
329 let mut obj_row: Vec<BigRational> = vec![BigRational::zero(); total_cols + 1];
331
332 let mut basis: Vec<usize> = vec![0usize; m];
334
335 let mut art_idx = n + m; for (i, c) in constraints.iter().enumerate() {
338 for (j, coeff) in c.coefficients.iter().enumerate() {
340 if j < n {
341 tab[i][j] = coeff.clone();
342 }
343 }
344
345 let slack_col = n + i;
347
348 match c.kind {
349 ConstraintKind::Le => {
350 tab[i][slack_col] = BigRational::new(BigInt::from(1i64), BigInt::from(1i64));
352 tab[i][rhs_col] = c.rhs.clone();
353 basis[i] = slack_col; }
356 ConstraintKind::Ge => {
357 tab[i][slack_col] = BigRational::new(BigInt::from(-1i64), BigInt::from(1i64));
359 tab[i][art_idx] = BigRational::new(BigInt::from(1i64), BigInt::from(1i64));
360 tab[i][rhs_col] = c.rhs.clone();
361 basis[i] = art_idx;
362 obj_row[art_idx] = big_m.clone();
364 art_idx += 1;
365 }
366 ConstraintKind::Eq => {
367 tab[i][art_idx] = BigRational::new(BigInt::from(1i64), BigInt::from(1i64));
369 tab[i][rhs_col] = c.rhs.clone();
370 basis[i] = art_idx;
371 obj_row[art_idx] = big_m.clone();
372 art_idx += 1;
373 }
374 }
375
376 if tab[i][rhs_col] < BigRational::zero() {
378 for elem in &mut tab[i] {
379 *elem = -elem.clone();
380 }
381 }
385 }
386
387 for (j, c) in obj_coeffs.iter().enumerate() {
389 obj_row[j] = c.clone();
390 }
391
392 for i in 0..m {
397 let bv = basis[i];
398 if bv >= n + m {
399 let factor = obj_row[bv].clone();
401 if !factor.is_zero() {
402 for j in 0..=total_cols {
403 let t = tab[i][j].clone();
404 obj_row[j] = obj_row[j].clone() - &factor * &t;
405 }
406 }
407 }
408 }
409
410 const MAX_ITERS: usize = 50_000;
412
413 for _iter in 0..MAX_ITERS {
414 let entering = match Self::find_entering(&obj_row, total_cols) {
416 Some(e) => e,
417 None => break, };
419
420 let leaving = match Self::find_leaving(&tab, m, entering, rhs_col) {
422 Some(l) => l,
423 None => {
424 return SolveResult {
426 status: SolveStatus::Unbounded,
427 values: vec![BigRational::zero(); n],
428 objective: BigRational::zero(),
429 shadow_prices: vec![BigRational::zero(); m],
430 };
431 }
432 };
433
434 Self::pivot_tableau(
436 &mut tab,
437 &mut obj_row,
438 &mut basis,
439 m,
440 leaving,
441 entering,
442 rhs_col,
443 );
444 }
445
446 let mut values = vec![BigRational::zero(); n];
449 for (i, &bv) in basis.iter().enumerate() {
450 if bv < n {
451 values[bv] = tab[i][rhs_col].clone();
452 } else if bv >= n + m {
453 if tab[i][rhs_col].abs()
455 > BigRational::new(BigInt::from(1i64), BigInt::from(1_000_000i64))
456 {
457 return SolveResult {
458 status: SolveStatus::Infeasible,
459 values: vec![BigRational::zero(); n],
460 objective: BigRational::zero(),
461 shadow_prices: vec![BigRational::zero(); m],
462 };
463 }
464 }
466 }
467
468 let mut objective = BigRational::zero();
470 for (j, c) in obj_coeffs.iter().enumerate() {
471 objective += c * &values[j];
472 }
473
474 let shadow_prices: Vec<BigRational> = constraints
478 .iter()
479 .enumerate()
480 .map(|(i, c)| {
481 let slack_col = n + i;
482 let rc = &obj_row[slack_col];
483 match c.kind {
484 ConstraintKind::Le => -rc.clone(),
485 ConstraintKind::Ge => rc.clone(),
486 ConstraintKind::Eq => -rc.clone(),
487 }
488 })
489 .collect();
490
491 SolveResult {
492 status: SolveStatus::Optimal,
493 values,
494 objective,
495 shadow_prices,
496 }
497 }
498
499 fn find_entering(obj_row: &[BigRational], total_cols: usize) -> Option<usize> {
501 let mut best_col = None;
502 let mut best_val = BigRational::zero();
503 for (j, val) in obj_row.iter().enumerate().take(total_cols) {
504 if *val < best_val {
505 best_val = val.clone();
506 best_col = Some(j);
507 }
508 }
509 best_col
510 }
511
512 fn find_leaving(
514 tab: &[Vec<BigRational>],
515 m: usize,
516 entering: usize,
517 rhs_col: usize,
518 ) -> Option<usize> {
519 let mut best_row = None;
520 let mut best_ratio: Option<BigRational> = None;
521
522 for (i, row) in tab.iter().enumerate().take(m) {
523 let a_ie = &row[entering];
524 if a_ie <= &BigRational::zero() {
525 continue; }
527 let ratio = &row[rhs_col] / a_ie;
528 match &best_ratio {
529 None => {
530 best_ratio = Some(ratio);
531 best_row = Some(i);
532 }
533 Some(br) => {
534 if ratio < *br {
535 best_ratio = Some(ratio);
536 best_row = Some(i);
537 }
538 }
539 }
540 }
541
542 best_row
543 }
544
545 fn pivot_tableau(
547 tab: &mut [Vec<BigRational>],
548 obj_row: &mut [BigRational],
549 basis: &mut [usize],
550 m: usize,
551 leaving: usize,
552 entering: usize,
553 rhs_col: usize,
554 ) {
555 let pivot = tab[leaving][entering].clone();
556 let total_cols_plus_one = rhs_col + 1;
557
558 for elem in tab[leaving].iter_mut().take(total_cols_plus_one) {
560 let v = elem.clone();
561 *elem = v / &pivot;
562 }
563
564 for i in 0..m {
566 if i == leaving {
567 continue;
568 }
569 let factor = tab[i][entering].clone();
570 if factor.is_zero() {
571 continue;
572 }
573 let pivot_row: Vec<BigRational> = tab[leaving]
575 .iter()
576 .take(total_cols_plus_one)
577 .cloned()
578 .collect();
579 for (j, pv) in pivot_row.iter().enumerate() {
580 let v = tab[i][j].clone();
581 tab[i][j] = v - &factor * pv;
582 }
583 }
584
585 let obj_factor = obj_row[entering].clone();
587 if !obj_factor.is_zero() {
588 let pivot_row: Vec<BigRational> = tab[leaving]
589 .iter()
590 .take(total_cols_plus_one)
591 .cloned()
592 .collect();
593 for (j, pv) in pivot_row.iter().enumerate() {
594 let v = obj_row[j].clone();
595 obj_row[j] = v - &obj_factor * pv;
596 }
597 }
598
599 basis[leaving] = entering;
600 }
601
602 pub fn n_vars(&self) -> usize {
604 self.n_vars
605 }
606
607 pub fn n_constraints(&self) -> usize {
609 self.constraints.len()
610 }
611
612 pub fn obj_coeffs(&self) -> &[BigRational] {
614 &self.obj_coeffs
615 }
616
617 pub fn constraints(&self) -> &[Constraint] {
619 &self.constraints
620 }
621
622 pub fn last_result(&self) -> Option<&SolveResult> {
624 self.last_result.as_ref()
625 }
626}
627
628#[cfg(test)]
632pub(crate) fn big_rat(num: i64, den: i64) -> BigRational {
633 BigRational::new(BigInt::from(num), BigInt::from(den))
634}
635
636#[cfg(test)]
639mod tests {
640 use super::*;
641
642 fn r(n: i64) -> BigRational {
643 big_rat(n, 1)
644 }
645
646 fn make_lp_2var(
654 c: (i64, i64),
655 rows: &[(i64, i64, i64)], ) -> SimplexSolver {
657 let obj = vec![r(c.0), r(c.1)];
658 let constraints = rows
659 .iter()
660 .map(|&(a1, a2, b)| Constraint::le(vec![r(a1), r(a2)], r(b)))
661 .collect();
662 SimplexSolver::new(obj, constraints)
663 }
664
665 #[test]
668 fn test_simple_2var_optimal() {
669 let obj = vec![r(1), r(1)];
672 let constraints = vec![
673 Constraint::ge(vec![r(1), r(1)], r(4)),
674 Constraint::le(vec![r(1), r(0)], r(3)),
675 Constraint::le(vec![r(0), r(1)], r(3)),
676 ];
677 let mut solver = SimplexSolver::new(obj, constraints);
678 let result = solver.solve().expect("solve should succeed");
679 assert_eq!(result.status, SolveStatus::Optimal);
680 assert!(result.objective <= r(6));
683 assert!(result.objective >= r(0));
684 }
685
686 #[test]
687 fn test_lp_with_known_optimal() {
688 let obj = vec![r(-2), r(-3)];
692 let constraints = vec![
693 Constraint::le(vec![r(1), r(1)], r(4)),
694 Constraint::le(vec![r(1), r(0)], r(2)),
695 Constraint::le(vec![r(0), r(1)], r(3)),
696 ];
697 let mut solver = SimplexSolver::new(obj, constraints);
698 let result = solver.solve().expect("solve should succeed");
699 assert_eq!(result.status, SolveStatus::Optimal);
700 assert!(result.objective <= r(0));
702 }
703
704 #[test]
705 fn test_trivially_feasible() {
706 let mut solver = make_lp_2var((1, 0), &[(1, 0, 5)]);
708 let result = solver.solve().expect("solve should succeed");
710 assert_eq!(result.status, SolveStatus::Optimal);
711 assert!(result.objective >= r(0));
713 }
714
715 #[test]
718 fn test_set_objective_coefficient() {
719 let mut solver = make_lp_2var((1, 1), &[(1, 1, 10)]);
720 solver
722 .set_objective_coefficient(0, r(2))
723 .expect("index 0 should be valid");
724 assert_eq!(solver.obj_coeffs()[0], r(2));
725 assert!(solver.last_result().is_none());
727 }
728
729 #[test]
730 fn test_set_objective_coefficient_oob() {
731 let mut solver = make_lp_2var((1, 1), &[(1, 1, 10)]);
732 let err = solver
733 .set_objective_coefficient(5, r(1))
734 .expect_err("out-of-bounds index should error");
735 assert!(matches!(err, SimplexError::IndexOutOfBounds { .. }));
736 }
737
738 #[test]
741 fn test_shadow_price_before_solve_errors() {
742 let solver = make_lp_2var((1, 1), &[(1, 1, 10)]);
743 let err = solver
744 .shadow_price(0)
745 .expect_err("calling shadow_price before solve should error");
746 assert!(matches!(err, SimplexError::NotYetSolved));
747 }
748
749 #[test]
750 fn test_shadow_price_after_solve() {
751 let obj = vec![r(-1)];
755 let constraints = vec![Constraint::le(vec![r(1)], r(5))];
756 let mut solver = SimplexSolver::new(obj, constraints);
757 solver.solve().expect("solve should succeed");
758 let sp = solver.shadow_price(0).expect("shadow_price(0) should work");
759 assert!(sp <= r(0));
763 }
764
765 #[test]
766 fn test_shadow_price_verification() {
767 let obj = vec![r(-3), r(-5)];
771 let constraints = vec![
772 Constraint::le(vec![r(1), r(0)], r(4)),
773 Constraint::le(vec![r(0), r(2)], r(12)),
774 Constraint::le(vec![r(3), r(5)], r(25)),
775 ];
776 let mut solver = SimplexSolver::new(obj.clone(), constraints);
777 let result = solver.solve().expect("solve should succeed");
778 assert_eq!(result.status, SolveStatus::Optimal);
779
780 let sp0 = solver.shadow_price(0).expect("shadow_price 0 should work");
783 let sp2 = solver.shadow_price(2).expect("shadow_price 2 should work");
785
786 assert!(sp0 <= r(0) || sp0 == r(0));
789 let _ = sp2; }
791
792 #[test]
795 fn test_parametric_rhs_perturbation() {
796 let obj = vec![r(-1)];
799 let constraints = vec![Constraint::le(vec![r(1)], r(5))];
800 let mut solver = SimplexSolver::new(obj, constraints);
801
802 let result5 = solver.solve().expect("solve b=5 should succeed");
803
804 solver.set_rhs(0, r(6)).expect("set_rhs should work");
805 let result6 = solver.solve().expect("solve b=6 should succeed");
806
807 assert_eq!(result5.status, SolveStatus::Optimal);
809 assert_eq!(result6.status, SolveStatus::Optimal);
810
811 let delta = result6.objective.clone() - result5.objective.clone();
813 assert_eq!(delta, r(-1));
814 }
815
816 #[test]
817 fn test_set_rhs_invalidates_cache() {
818 let mut solver = make_lp_2var((1, 0), &[(1, 0, 5)]);
819 solver.solve().expect("first solve should work");
820 assert!(solver.last_result().is_some());
821 solver.set_rhs(0, r(10)).expect("set_rhs should work");
822 assert!(solver.last_result().is_none());
823 }
824
825 #[test]
830 fn test_all_accessors() {
831 let obj = vec![r(3), r(2)];
833 let constraints = vec![
834 Constraint::le(vec![r(1), r(1)], r(10)),
835 Constraint::le(vec![r(1), r(0)], r(6)),
836 ];
837 let mut solver = SimplexSolver::new(obj, constraints);
838
839 assert_eq!(solver.n_vars(), 2);
841 assert_eq!(solver.n_constraints(), 2);
842
843 assert_eq!(solver.obj_coeffs(), &[r(3), r(2)]);
845
846 assert_eq!(solver.constraints().len(), 2);
848 assert_eq!(solver.constraints()[0].rhs, r(10));
849
850 assert_eq!(
852 *solver.get_objective_coefficient(1).expect("index 1 valid"),
853 r(2)
854 );
855 let err = solver.get_objective_coefficient(99).expect_err("oob");
856 assert!(matches!(err, SimplexError::IndexOutOfBounds { .. }));
857
858 assert_eq!(*solver.get_rhs(0).expect("index 0 valid"), r(10));
860 let err2 = solver.get_rhs(99).expect_err("oob");
861 assert!(matches!(err2, SimplexError::IndexOutOfBounds { .. }));
862
863 assert!(solver.last_result().is_none());
865
866 assert!(solver.objective_value().is_none());
868
869 let result = solver.solve().expect("solve should succeed");
871 assert_eq!(result.status, SolveStatus::Optimal);
872
873 assert!(solver.last_result().is_some());
875 assert_eq!(solver.last_result().unwrap().status, SolveStatus::Optimal);
876
877 let ov = solver
879 .objective_value()
880 .expect("objective_value should be Some");
881 assert_eq!(ov, r(0));
883
884 let sp0 = solver.shadow_price(0).expect("shadow_price(0)");
886 let sp1 = solver.shadow_price(1).expect("shadow_price(1)");
887 assert!(sp0 <= r(0));
889 assert!(sp1 <= r(0));
890
891 solver.set_rhs(1, r(4)).expect("set_rhs index 1");
893 assert_eq!(*solver.get_rhs(1).expect("get_rhs index 1"), r(4));
894 }
895}