use std::fmt;
use num_bigint::BigInt;
use num_integer::Integer;
use num_rational::Ratio;
use num_traits::{One, Signed, Zero};
use crate::api::context::Context;
use crate::api::expr::Ex;
use crate::base::errors::SymplexError;
use crate::domains::matrix::Matrix;
pub type Q = Ratio<BigInt>;
pub fn q(n: i64, d: i64) -> Q {
Ratio::new(BigInt::from(n), BigInt::from(d))
}
pub fn qi(n: i64) -> Q {
Ratio::from_integer(BigInt::from(n))
}
fn invalid(operation: &'static str, reason: impl Into<String>) -> SymplexError {
SymplexError::InvalidArgument {
operation,
reason: reason.into(),
}
}
fn failed(operation: &'static str, reason: impl Into<String>) -> SymplexError {
SymplexError::ComputationFailed {
operation,
reason: reason.into(),
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Objective {
Minimize,
Maximize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Relation {
Eq,
Le,
Ge,
}
#[derive(Clone, Debug)]
struct Constraint {
row: Vec<Q>,
rhs: Q,
relation: Relation,
}
#[derive(Clone, Debug)]
pub struct LpProblem {
objective: Objective,
c: Vec<Q>,
constraints: Vec<Constraint>,
bounds: Vec<(Option<Q>, Option<Q>)>,
bad_var: Option<usize>,
}
impl LpProblem {
fn new(objective: Objective, c: Vec<Q>) -> Self {
let n = c.len();
LpProblem {
objective,
c,
constraints: Vec::new(),
bounds: vec![(Some(Q::zero()), None); n],
bad_var: None,
}
}
pub fn minimize(c: Vec<Q>) -> Self {
Self::new(Objective::Minimize, c)
}
pub fn maximize(c: Vec<Q>) -> Self {
Self::new(Objective::Maximize, c)
}
fn add(&mut self, row: Vec<Q>, rhs: Q, relation: Relation) {
self.constraints.push(Constraint { row, rhs, relation });
}
pub fn eq(mut self, row: Vec<Q>, rhs: Q) -> Self {
self.add(row, rhs, Relation::Eq);
self
}
pub fn le(mut self, row: Vec<Q>, rhs: Q) -> Self {
self.add(row, rhs, Relation::Le);
self
}
pub fn ge(mut self, row: Vec<Q>, rhs: Q) -> Self {
self.add(row, rhs, Relation::Ge);
self
}
pub fn bounds(mut self, var: usize, lo: Option<Q>, hi: Option<Q>) -> Self {
match self.bounds.get_mut(var) {
Some(slot) => *slot = (lo, hi),
None => self.bad_var = self.bad_var.or(Some(var)),
}
self
}
pub fn free(self, var: usize) -> Self {
self.bounds(var, None, None)
}
pub fn num_vars(&self) -> usize {
self.c.len()
}
pub fn num_constraints(&self) -> usize {
self.constraints.len()
}
pub fn solve(&self) -> Result<LpSolution, SymplexError> {
self.validate()?;
solve_lp(self)
}
fn validate(&self) -> Result<(), SymplexError> {
let n = self.c.len();
if n == 0 {
return Err(invalid(
"linprog",
"the objective must have at least one variable",
));
}
if let Some(bad) = self.bad_var {
return Err(invalid(
"linprog",
format!("bounds were set for variable {bad} but there are only {n} variables"),
));
}
for (i, con) in self.constraints.iter().enumerate() {
if con.row.len() != n {
return Err(invalid(
"linprog",
format!(
"constraint {i} has {} coefficients but there are {n} variables",
con.row.len()
),
));
}
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LpStatus {
Optimal,
Infeasible,
Unbounded,
}
#[derive(Clone, Debug)]
pub struct LpSolution {
pub status: LpStatus,
pub x: Vec<Q>,
pub objective: Option<Q>,
pub duals: Vec<Q>,
pub farkas: Option<Vec<Q>>,
}
impl LpSolution {
pub fn is_optimal(&self) -> bool {
self.status == LpStatus::Optimal
}
pub fn x_ex(&self, ctx: &Context) -> Vec<Ex> {
self.x.iter().map(|v| ctx.from_ratio(v.clone())).collect()
}
pub fn duals_ex(&self, ctx: &Context) -> Vec<Ex> {
self.duals
.iter()
.map(|v| ctx.from_ratio(v.clone()))
.collect()
}
fn infeasible(farkas: Option<Vec<Q>>) -> Self {
LpSolution {
status: LpStatus::Infeasible,
x: Vec::new(),
objective: None,
duals: Vec::new(),
farkas,
}
}
fn unbounded() -> Self {
LpSolution {
status: LpStatus::Unbounded,
x: Vec::new(),
objective: None,
duals: Vec::new(),
farkas: None,
}
}
}
#[derive(Clone, Debug)]
enum VarMap {
Shifted { col: usize, lo: Q },
Mirrored { col: usize, hi: Q },
Split { pos: usize, neg: usize },
}
struct Standard {
a: Vec<Vec<Q>>,
b: Vec<Q>,
c: Vec<Q>,
constant: Q,
var_map: Vec<VarMap>,
row_sign: Vec<Q>,
m_orig: usize,
}
enum Standardized {
Ready(Standard),
BoundsInfeasible,
}
fn standardize(p: &LpProblem) -> Standardized {
let n = p.c.len();
let m = p.constraints.len();
let zero = Q::zero();
let one = Q::one();
let mut var_map = Vec::with_capacity(n);
let mut ncols = 0usize;
let mut bound_rows: Vec<(usize, Q)> = Vec::new();
for (lo, hi) in &p.bounds {
match (lo, hi) {
(Some(lo), Some(hi)) => {
if hi < lo {
return Standardized::BoundsInfeasible;
}
var_map.push(VarMap::Shifted {
col: ncols,
lo: lo.clone(),
});
bound_rows.push((ncols, hi - lo));
ncols += 1;
}
(Some(lo), None) => {
var_map.push(VarMap::Shifted {
col: ncols,
lo: lo.clone(),
});
ncols += 1;
}
(None, Some(hi)) => {
var_map.push(VarMap::Mirrored {
col: ncols,
hi: hi.clone(),
});
ncols += 1;
}
(None, None) => {
var_map.push(VarMap::Split {
pos: ncols,
neg: ncols + 1,
});
ncols += 2;
}
}
}
let n_slack: usize = p
.constraints
.iter()
.filter(|c| c.relation != Relation::Eq)
.count();
let total_cols = ncols + n_slack + bound_rows.len();
let total_rows = m + bound_rows.len();
let sign = match p.objective {
Objective::Minimize => one.clone(),
Objective::Maximize => -one.clone(),
};
let mut c = vec![zero.clone(); total_cols];
let mut constant = zero.clone();
for (j, vm) in var_map.iter().enumerate() {
let cj = &sign * &p.c[j];
match vm {
VarMap::Shifted { col, lo } => {
c[*col] = cj.clone();
constant += &cj * lo;
}
VarMap::Mirrored { col, hi } => {
c[*col] = -cj.clone();
constant += &cj * hi;
}
VarMap::Split { pos, neg } => {
c[*pos] = cj.clone();
c[*neg] = -cj;
}
}
}
let mut a = vec![vec![zero.clone(); total_cols]; total_rows];
let mut b = vec![zero.clone(); total_rows];
let mut row_sign = vec![one.clone(); m];
let mut next_slack = ncols;
for (i, con) in p.constraints.iter().enumerate() {
let mut rhs = con.rhs.clone();
for (j, vm) in var_map.iter().enumerate() {
let aij = &con.row[j];
if aij.is_zero() {
continue;
}
match vm {
VarMap::Shifted { col, lo } => {
a[i][*col] = aij.clone();
rhs -= aij * lo;
}
VarMap::Mirrored { col, hi } => {
a[i][*col] = -aij.clone();
rhs -= aij * hi;
}
VarMap::Split { pos, neg } => {
a[i][*pos] = aij.clone();
a[i][*neg] = -aij.clone();
}
}
}
match con.relation {
Relation::Le => {
a[i][next_slack] = one.clone();
next_slack += 1;
}
Relation::Ge => {
a[i][next_slack] = -one.clone();
next_slack += 1;
}
Relation::Eq => {}
}
if rhs.is_negative() {
row_sign[i] = -one.clone();
for v in a[i].iter_mut() {
if !v.is_zero() {
*v = -std::mem::take(v);
}
}
rhs = -rhs;
}
b[i] = rhs;
}
for (k, (col, width)) in bound_rows.into_iter().enumerate() {
let r = m + k;
a[r][col] = one.clone();
a[r][next_slack] = one.clone();
next_slack += 1;
b[r] = width;
}
debug_assert_eq!(next_slack, total_cols);
Standardized::Ready(Standard {
a,
b,
c,
constant,
var_map,
row_sign,
m_orig: m,
})
}
struct Tableau<T: Cell> {
rows: Vec<T>,
width: usize,
obj: Vec<T>,
d: T,
obj_scale: T,
row_scale: Vec<T>,
basis: Vec<usize>,
m: usize,
n: usize,
pivots_done: usize,
max_pivots: usize,
stall: usize,
}
const STALL_LIMIT: usize = 12;
trait Cell: Clone + PartialEq + Eq + Ord + fmt::Debug {
fn cell_zero() -> Self;
fn cell_one() -> Self;
fn from_big(v: &BigInt) -> Option<Self>;
fn to_big(&self) -> BigInt;
fn is_zero(&self) -> bool;
fn is_negative(&self) -> bool;
fn signum(&self) -> std::cmp::Ordering;
fn neg(&self) -> Option<Self>;
fn mul(&self, o: &Self) -> Option<Self>;
fn pivot_update(v: &Self, p: &Self, f: &Self, pr: &Self, d: &Self) -> Option<Self>;
fn rescale(v: &Self, p: &Self, d: &Self) -> Option<Self>;
fn sub_mul(&self, f: &Self, r: &Self) -> Option<Self>;
fn cmp_products(a: &Self, b: &Self, c: &Self, d: &Self) -> std::cmp::Ordering;
}
impl Cell for BigInt {
fn cell_zero() -> Self {
<BigInt as Zero>::zero()
}
fn cell_one() -> Self {
<BigInt as One>::one()
}
fn from_big(v: &BigInt) -> Option<Self> {
Some(v.clone())
}
fn to_big(&self) -> BigInt {
self.clone()
}
fn is_zero(&self) -> bool {
Zero::is_zero(self)
}
fn is_negative(&self) -> bool {
Signed::is_negative(self)
}
fn signum(&self) -> std::cmp::Ordering {
match self.sign() {
num_bigint::Sign::Minus => std::cmp::Ordering::Less,
num_bigint::Sign::NoSign => std::cmp::Ordering::Equal,
num_bigint::Sign::Plus => std::cmp::Ordering::Greater,
}
}
fn neg(&self) -> Option<Self> {
Some(-self)
}
fn mul(&self, o: &Self) -> Option<Self> {
Some(self * o)
}
fn pivot_update(v: &Self, p: &Self, f: &Self, pr: &Self, d: &Self) -> Option<Self> {
let t = if Zero::is_zero(pr) {
v * p
} else if Zero::is_zero(v) {
-(f * pr)
} else {
v * p - f * pr
};
debug_assert!(
Zero::is_zero(&(&t % d)),
"integer pivoting: inexact division"
);
Some(t / d)
}
fn rescale(v: &Self, p: &Self, d: &Self) -> Option<Self> {
let t = v * p;
debug_assert!(
Zero::is_zero(&(&t % d)),
"integer pivoting: inexact division"
);
Some(t / d)
}
fn sub_mul(&self, f: &Self, r: &Self) -> Option<Self> {
Some(self - f * r)
}
fn cmp_products(a: &Self, b: &Self, c: &Self, d: &Self) -> std::cmp::Ordering {
(a * b).cmp(&(c * d))
}
}
impl Cell for i64 {
fn cell_zero() -> Self {
0
}
fn cell_one() -> Self {
1
}
fn from_big(v: &BigInt) -> Option<Self> {
i64::try_from(v).ok()
}
fn to_big(&self) -> BigInt {
BigInt::from(*self)
}
fn is_zero(&self) -> bool {
*self == 0
}
fn is_negative(&self) -> bool {
*self < 0
}
fn signum(&self) -> std::cmp::Ordering {
self.cmp(&0)
}
fn neg(&self) -> Option<Self> {
self.checked_neg()
}
fn mul(&self, o: &Self) -> Option<Self> {
self.checked_mul(*o)
}
fn pivot_update(v: &Self, p: &Self, f: &Self, pr: &Self, d: &Self) -> Option<Self> {
let t = i128::from(*v) * i128::from(*p) - i128::from(*f) * i128::from(*pr);
debug_assert!(
t % i128::from(*d) == 0,
"integer pivoting: inexact division"
);
i64::try_from(t / i128::from(*d)).ok()
}
fn rescale(v: &Self, p: &Self, d: &Self) -> Option<Self> {
let t = i128::from(*v) * i128::from(*p);
debug_assert!(
t % i128::from(*d) == 0,
"integer pivoting: inexact division"
);
i64::try_from(t / i128::from(*d)).ok()
}
fn sub_mul(&self, f: &Self, r: &Self) -> Option<Self> {
i64::try_from(i128::from(*self) - i128::from(*f) * i128::from(*r)).ok()
}
fn cmp_products(a: &Self, b: &Self, c: &Self, d: &Self) -> std::cmp::Ordering {
(i128::from(*a) * i128::from(*b)).cmp(&(i128::from(*c) * i128::from(*d)))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct I256 {
hi: i128,
lo: u128,
}
impl I256 {
fn from_i128(v: i128) -> Self {
I256 {
hi: if v < 0 { -1 } else { 0 },
lo: v as u128,
}
}
fn mul(a: i128, b: i128) -> Self {
let neg = (a < 0) != (b < 0);
let (ua, ub) = (a.unsigned_abs(), b.unsigned_abs());
let (a0, a1) = (ua as u64 as u128, ua >> 64);
let (b0, b1) = (ub as u64 as u128, ub >> 64);
let p00 = a0 * b0;
let p01 = a0 * b1;
let p10 = a1 * b0;
let p11 = a1 * b1;
let mid = (p00 >> 64) + (p01 as u64 as u128) + (p10 as u64 as u128);
let lo = (p00 as u64 as u128) | (mid << 64);
let hi = p11 + (p01 >> 64) + (p10 >> 64) + (mid >> 64);
let mag = I256 { hi: hi as i128, lo };
if neg { mag.neg() } else { mag }
}
fn neg(self) -> Self {
let lo = (!self.lo).wrapping_add(1);
let hi = (!self.hi).wrapping_add(u128::from(lo == 0) as i128);
I256 { hi, lo }
}
fn sub(self, o: Self) -> Self {
let (lo, borrow) = self.lo.overflowing_sub(o.lo);
let hi = self.hi.wrapping_sub(o.hi).wrapping_sub(i128::from(borrow));
I256 { hi, lo }
}
fn is_negative(self) -> bool {
self.hi < 0
}
fn to_i128(self) -> Option<i128> {
let lo = self.lo as i128;
let fits = (self.hi == 0 && lo >= 0) || (self.hi == -1 && lo < 0);
fits.then_some(lo)
}
fn div_exact_unsigned(self, d: u128) -> Option<u128> {
debug_assert!(!self.is_negative());
let hi = self.hi as u128;
if hi == 0 {
return Some(self.lo / d);
}
let k = d.trailing_zeros();
let d_odd = d >> k;
let lo = if k == 0 {
self.lo
} else {
(self.lo >> k) | (hi << (128 - k))
};
let hi = hi >> k;
if hi >= d_odd {
return None;
}
Some(lo.wrapping_mul(inverse_mod_2_128(d_odd)))
}
fn div_exact(self, d: i128) -> Option<i128> {
let neg = self.is_negative() != (d < 0);
let mag = if self.is_negative() { self.neg() } else { self };
let q = mag.div_exact_unsigned(d.unsigned_abs())?;
if neg {
if q > (1u128 << 127) {
return None;
}
Some((q as i128).wrapping_neg())
} else {
i128::try_from(q).ok()
}
}
}
fn inverse_mod_2_128(d: u128) -> u128 {
debug_assert!(d & 1 == 1);
let mut x = d;
for _ in 0..6 {
x = x.wrapping_mul(2u128.wrapping_sub(d.wrapping_mul(x)));
}
debug_assert_eq!(d.wrapping_mul(x), 1);
x
}
impl PartialOrd for I256 {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for I256 {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.hi.cmp(&other.hi).then(self.lo.cmp(&other.lo))
}
}
impl Cell for i128 {
fn cell_zero() -> Self {
0
}
fn cell_one() -> Self {
1
}
fn from_big(v: &BigInt) -> Option<Self> {
i128::try_from(v).ok()
}
fn to_big(&self) -> BigInt {
BigInt::from(*self)
}
fn is_zero(&self) -> bool {
*self == 0
}
fn is_negative(&self) -> bool {
*self < 0
}
fn signum(&self) -> std::cmp::Ordering {
self.cmp(&0)
}
fn neg(&self) -> Option<Self> {
self.checked_neg()
}
fn mul(&self, o: &Self) -> Option<Self> {
self.checked_mul(*o)
}
fn pivot_update(v: &Self, p: &Self, f: &Self, pr: &Self, d: &Self) -> Option<Self> {
let t = I256::mul(*v, *p).sub(I256::mul(*f, *pr));
t.div_exact(*d)
}
fn rescale(v: &Self, p: &Self, d: &Self) -> Option<Self> {
I256::mul(*v, *p).div_exact(*d)
}
fn sub_mul(&self, f: &Self, r: &Self) -> Option<Self> {
I256::from_i128(*self).sub(I256::mul(*f, *r)).to_i128()
}
fn cmp_products(a: &Self, b: &Self, c: &Self, d: &Self) -> std::cmp::Ordering {
I256::mul(*a, *b).cmp(&I256::mul(*c, *d))
}
}
enum Step {
Optimal,
Unbounded,
}
enum Halt {
Overflow,
Error(SymplexError),
}
impl From<SymplexError> for Halt {
fn from(e: SymplexError) -> Self {
Halt::Error(e)
}
}
fn denominator_lcm<'a>(values: impl Iterator<Item = &'a Q>) -> BigInt {
values.fold(<BigInt as One>::one(), |l, q| l.lcm(q.denom()))
}
impl<T: Cell> Tableau<T> {
fn new(std: &Standard) -> Option<Self> {
let m = std.a.len();
let n = std.c.len();
let width = n + m + 1;
let mut rows = Vec::with_capacity(m * width);
let mut row_scale = Vec::with_capacity(m);
for i in 0..m {
let s = denominator_lcm(std.a[i].iter().chain(std::iter::once(&std.b[i])));
let scaled = |q: &Q| T::from_big(&(q.numer() * (&s / q.denom())));
for q in &std.a[i] {
rows.push(scaled(q)?);
}
for k in 0..m {
rows.push(if k == i {
T::cell_one()
} else {
T::cell_zero()
});
}
rows.push(scaled(&std.b[i])?);
row_scale.push(T::from_big(&s)?);
}
let basis: Vec<usize> = (0..m).map(|i| n + i).collect();
Some(Tableau {
rows,
width,
obj: vec![T::cell_zero(); width],
d: T::cell_one(),
obj_scale: T::cell_one(),
row_scale,
basis,
m,
n,
pivots_done: 0,
max_pivots: 10_000 + 50 * (m + n),
stall: 0,
})
}
#[inline]
fn rhs_col(&self) -> usize {
self.n + self.m
}
#[inline]
fn row(&self, i: usize) -> &[T] {
&self.rows[i * self.width..(i + 1) * self.width]
}
#[inline]
fn sign_of(&self, z: &T) -> std::cmp::Ordering {
use std::cmp::Ordering::*;
match (z.signum(), self.d.signum()) {
(Equal, _) => Equal,
(a, b) if a == b => Greater,
_ => Less,
}
}
#[inline]
fn value(&self, z: &T) -> Q {
Ratio::new(z.to_big(), self.d.to_big())
}
fn set_objective(&mut self, costs: &[Q]) -> Option<()> {
let width = self.width;
let s = denominator_lcm(costs.iter());
let int_cost = |q: &Q| T::from_big(&(q.numer() * (&s / q.denom())));
let mut obj = vec![T::cell_zero(); width];
for (o, c) in obj.iter_mut().zip(costs) {
if !Zero::is_zero(c) {
*o = int_cost(c)?.mul(&self.d)?;
}
}
for (i, &k) in self.basis.iter().enumerate() {
let f = int_cost(&costs[k])?;
if f.is_zero() {
continue;
}
let row = self.row(i);
for (o, r) in obj.iter_mut().zip(row) {
if !r.is_zero() {
*o = o.sub_mul(&f, r)?;
}
}
}
self.obj = obj;
self.obj_scale = T::from_big(&s)?;
Some(())
}
fn pivot(&mut self, r: usize, s: usize) -> Option<()> {
let w = self.width;
let prow: Vec<T> = self.rows[r * w..(r + 1) * w]
.iter_mut()
.map(|v| std::mem::replace(v, T::cell_zero()))
.collect();
let p = prow[s].clone();
let d = std::mem::replace(&mut self.d, p.clone());
let update_row = |row: &mut [T]| -> Option<()> {
let f = std::mem::replace(&mut row[s], T::cell_zero());
if f.is_zero() {
for v in row.iter_mut() {
if !v.is_zero() {
*v = T::rescale(v, &p, &d)?;
}
}
return Some(());
}
for (j, (v, pr)) in row.iter_mut().zip(prow.iter()).enumerate() {
if j == s {
continue;
}
if v.is_zero() && pr.is_zero() {
continue;
}
*v = T::pivot_update(v, &p, &f, pr, &d)?;
}
Some(())
};
for i in 0..self.m {
if i == r {
continue;
}
update_row(&mut self.rows[i * w..(i + 1) * w])?;
}
update_row(&mut self.obj)?;
self.rows[r * w..(r + 1) * w]
.iter_mut()
.zip(prow)
.for_each(|(slot, v)| *slot = v);
self.basis[r] = s;
self.pivots_done += 1;
Some(())
}
fn run(&mut self, limit: usize) -> Result<Step, Halt> {
use std::cmp::Ordering;
loop {
if self.pivots_done >= self.max_pivots {
return Err(failed(
"linprog",
format!(
"pivot cap of {} exceeded; the problem is degenerate beyond \
what the solver handles",
self.max_pivots
),
)
.into());
}
let neg = self.d.is_negative();
let oriented = |z: &T, j: usize| -> Option<T> {
let v = if neg { z.neg()? } else { z.clone() };
if j >= self.n {
v.mul(&self.row_scale[j - self.n])
} else {
Some(v)
}
};
let mut entering: Option<(usize, T)> = None;
for j in 0..limit {
if self.sign_of(&self.obj[j]) != Ordering::Less {
continue;
}
let v = oriented(&self.obj[j], j).ok_or(Halt::Overflow)?;
match &entering {
Some((_, best)) if v >= *best => {}
_ => entering = Some((j, v)),
}
if self.stall >= STALL_LIMIT {
break;
}
}
let Some((s, _)) = entering else {
return Ok(Step::Optimal);
};
let rhs = self.rhs_col();
let mut leaving: Option<usize> = None;
for i in 0..self.m {
let a = &self.row(i)[s];
if self.sign_of(a) != Ordering::Greater {
continue;
}
let better = match leaving {
None => true,
Some(l) => {
let (ri, rl) = (&self.row(i)[rhs], &self.row(l)[rhs]);
let al = &self.row(l)[s];
match T::cmp_products(ri, al, rl, a) {
Ordering::Less => true,
Ordering::Equal => self.basis[i] < self.basis[l],
Ordering::Greater => false,
}
}
};
if better {
leaving = Some(i);
}
}
let Some(r) = leaving else {
return Ok(Step::Unbounded);
};
if self.row(r)[rhs].is_zero() {
self.stall += 1;
} else {
self.stall = 0;
}
self.pivot(r, s).ok_or(Halt::Overflow)?;
}
}
fn solution(&self) -> Vec<Q> {
let rhs = self.rhs_col();
let mut z = vec![Q::zero(); self.n];
for (i, &k) in self.basis.iter().enumerate() {
if k < self.n {
z[k] = self.value(&self.row(i)[rhs]);
}
}
z
}
fn neg_objective(&self) -> Q {
Ratio::new(
self.obj[self.rhs_col()].to_big(),
self.d.to_big() * self.obj_scale.to_big(),
)
}
fn drive_out_artificials(&mut self) -> Option<()> {
for r in 0..self.m {
if self.basis[r] < self.n {
continue;
}
if let Some(s) = (0..self.n).find(|&j| !self.row(r)[j].is_zero()) {
self.pivot(r, s)?;
}
}
Some(())
}
fn phase1_costs(&self) -> Vec<Q> {
let mut costs = vec![Q::zero(); self.n + self.m];
for (c, s) in costs[self.n..].iter_mut().zip(&self.row_scale) {
*c = Ratio::new(<BigInt as One>::one(), s.to_big());
}
costs
}
fn duals(&self, phase1: bool) -> Vec<Q> {
let denom = self.d.to_big() * self.obj_scale.to_big();
(0..self.m)
.map(|i| {
let s = self.row_scale[i].to_big();
let reduced = Ratio::new(self.obj[self.n + i].to_big(), denom.clone());
let scaled_reduced = reduced * Ratio::from_integer(s);
if phase1 {
Q::one() - scaled_reduced
} else {
-scaled_reduced
}
})
.collect()
}
}
fn solve_lp(p: &LpProblem) -> Result<LpSolution, SymplexError> {
let sf = match standardize(p) {
Standardized::Ready(s) => s,
Standardized::BoundsInfeasible => return Ok(LpSolution::infeasible(None)),
};
match solve_standard::<i64>(p, &sf) {
Ok(sol) => return Ok(sol),
Err(Halt::Error(e)) => return Err(e),
Err(Halt::Overflow) => {}
}
match solve_standard::<i128>(p, &sf) {
Ok(sol) => return Ok(sol),
Err(Halt::Error(e)) => return Err(e),
Err(Halt::Overflow) => {}
}
tracing::debug!(target: "symplex::linprog", rows = sf.a.len(), cols = sf.c.len(), "i128 tableau overflowed; solving on BigInt");
match solve_standard::<BigInt>(p, &sf) {
Ok(sol) => Ok(sol),
Err(Halt::Error(e)) => Err(e),
Err(Halt::Overflow) => Err(failed("linprog", "internal: BigInt tableau overflowed")),
}
}
fn solve_standard<T: Cell>(p: &LpProblem, sf: &Standard) -> Result<LpSolution, Halt> {
let mut t = Tableau::<T>::new(sf).ok_or(Halt::Overflow)?;
let m = t.m;
let n = t.n;
let phase1 = t.phase1_costs();
t.set_objective(&phase1).ok_or(Halt::Overflow)?;
match t.run(n + m)? {
Step::Optimal => {}
Step::Unbounded => {
return Err(failed("linprog", "phase 1 reported unbounded").into());
}
}
let infeasibility = -t.neg_objective();
if infeasibility.is_positive() {
let y_std = t.duals(true);
let farkas: Vec<Q> = (0..sf.m_orig)
.map(|i| -(&sf.row_sign[i] * &y_std[i]))
.collect();
return Ok(LpSolution::infeasible(Some(farkas)));
}
t.drive_out_artificials().ok_or(Halt::Overflow)?;
let mut phase2 = vec![Q::zero(); n + m];
phase2[..n].clone_from_slice(&sf.c);
t.set_objective(&phase2).ok_or(Halt::Overflow)?;
match t.run(n)? {
Step::Optimal => {}
Step::Unbounded => return Ok(LpSolution::unbounded()),
}
if tracing::enabled!(target: "symplex::linprog::growth", tracing::Level::TRACE) {
let bits = t
.rows
.iter()
.chain(t.obj.iter())
.chain(std::iter::once(&t.d))
.map(|c| c.to_big().bits())
.max()
.unwrap_or(0);
tracing::trace!(target: "symplex::linprog::growth", rows = t.m, cols = t.n, pivots = t.pivots_done, max_bits = bits, "final tableau");
}
let z = t.solution();
let x: Vec<Q> = sf
.var_map
.iter()
.map(|vm| match vm {
VarMap::Shifted { col, lo } => lo + &z[*col],
VarMap::Mirrored { col, hi } => hi - &z[*col],
VarMap::Split { pos, neg } => &z[*pos] - &z[*neg],
})
.collect();
let objective: Q = p.c.iter().zip(x.iter()).map(|(c, v)| c * v).sum();
debug_assert_eq!(
{
let min_form: Q =
sf.c.iter().zip(z.iter()).map(|(c, v)| c * v).sum::<Q>() + &sf.constant;
match p.objective {
Objective::Minimize => min_form,
Objective::Maximize => -min_form,
}
},
objective
);
let y_std = t.duals(false);
let dir = match p.objective {
Objective::Minimize => Q::one(),
Objective::Maximize => -Q::one(),
};
let duals: Vec<Q> = (0..sf.m_orig)
.map(|i| &dir * &(&sf.row_sign[i] * &y_std[i]))
.collect();
Ok(LpSolution {
status: LpStatus::Optimal,
x,
objective: Some(objective),
duals,
farkas: None,
})
}
pub fn linprog(
c: &[Q],
a_ub: &[Vec<Q>],
b_ub: &[Q],
a_eq: &[Vec<Q>],
b_eq: &[Q],
bounds: &[(Option<Q>, Option<Q>)],
) -> Result<LpSolution, SymplexError> {
if a_ub.len() != b_ub.len() {
return Err(invalid(
"linprog",
format!(
"A_ub has {} rows but b_ub has {} entries",
a_ub.len(),
b_ub.len()
),
));
}
if a_eq.len() != b_eq.len() {
return Err(invalid(
"linprog",
format!(
"A_eq has {} rows but b_eq has {} entries",
a_eq.len(),
b_eq.len()
),
));
}
if !bounds.is_empty() && bounds.len() != c.len() {
return Err(invalid(
"linprog",
format!(
"bounds has {} entries but there are {} variables",
bounds.len(),
c.len()
),
));
}
let mut p = LpProblem::minimize(c.to_vec());
for (row, rhs) in a_ub.iter().zip(b_ub) {
p = p.le(row.clone(), rhs.clone());
}
for (row, rhs) in a_eq.iter().zip(b_eq) {
p = p.eq(row.clone(), rhs.clone());
}
for (j, (lo, hi)) in bounds.iter().enumerate() {
p = p.bounds(j, lo.clone(), hi.clone());
}
p.solve()
}
impl fmt::Display for LpSolution {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let list = |v: &[Q]| {
let parts: Vec<String> = v.iter().map(ToString::to_string).collect();
format!("({})", parts.join(", "))
};
match self.status {
LpStatus::Optimal => {
write!(f, "Optimal: x = {}", list(&self.x))?;
if let Some(obj) = &self.objective {
write!(f, ", objective = {obj}")?;
}
write!(f, ", duals = {}", list(&self.duals))
}
LpStatus::Infeasible => match &self.farkas {
Some(y) => write!(f, "Infeasible: Farkas certificate y = {}", list(y)),
None => write!(f, "Infeasible: contradictory bounds"),
},
LpStatus::Unbounded => write!(f, "Unbounded"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Feasibility {
Feasible(Vec<Q>),
Infeasible {
farkas: Option<Vec<Q>>,
},
}
impl Feasibility {
pub fn witness(&self) -> Option<&[Q]> {
match self {
Feasibility::Feasible(x) => Some(x),
Feasibility::Infeasible { .. } => None,
}
}
pub fn is_feasible(&self) -> bool {
matches!(self, Feasibility::Feasible(_))
}
}
pub fn feasible_nonneg(a_eq: &[Vec<Q>], b_eq: &[Q]) -> Result<Option<Vec<Q>>, SymplexError> {
let Some(first) = a_eq.first() else {
return Err(invalid(
"feasible_nonneg",
"need at least one equation to determine the number of variables",
));
};
let n = first.len();
if n == 0 {
return Err(invalid(
"feasible_nonneg",
"equations must have at least one variable",
));
}
let sol = linprog(&vec![Q::zero(); n], &[], &[], a_eq, b_eq, &[])?;
Ok(match sol.status {
LpStatus::Optimal => Some(sol.x),
LpStatus::Infeasible => None,
LpStatus::Unbounded => None,
})
}
pub fn feasible_nonneg_certified(a_eq: &[Vec<Q>], b_eq: &[Q]) -> Result<Feasibility, SymplexError> {
let Some(first) = a_eq.first() else {
return Err(invalid(
"feasible_nonneg_certified",
"need at least one equation to determine the number of variables",
));
};
let n = first.len();
if n == 0 {
return Err(invalid(
"feasible_nonneg_certified",
"equations must have at least one variable",
));
}
let sol = linprog(&vec![Q::zero(); n], &[], &[], a_eq, b_eq, &[])?;
Ok(match sol.status {
LpStatus::Optimal => Feasibility::Feasible(sol.x),
LpStatus::Infeasible => Feasibility::Infeasible { farkas: sol.farkas },
LpStatus::Unbounded => Feasibility::Infeasible { farkas: None },
})
}
pub fn nonneg_combination(vectors: &[Vec<Q>], target: &[Q]) -> Result<Feasibility, SymplexError> {
if vectors.is_empty() {
return Err(invalid("nonneg_combination", "need at least one vector"));
}
let m = target.len();
if m == 0 {
return Err(invalid(
"nonneg_combination",
"target must have at least one coordinate",
));
}
if let Some((j, v)) = vectors.iter().enumerate().find(|(_, v)| v.len() != m) {
return Err(invalid(
"nonneg_combination",
format!(
"vector {j} has {} coordinates but the target has {m}",
v.len()
),
));
}
let a_eq: Vec<Vec<Q>> = (0..m)
.map(|i| vectors.iter().map(|v| v[i].clone()).collect())
.collect();
feasible_nonneg_certified(&a_eq, target).map_err(|e| match e {
SymplexError::InvalidArgument { reason, .. } => invalid("nonneg_combination", reason),
other => other,
})
}
fn matrix_to_q(m: &Matrix, what: &str) -> Result<Vec<Vec<Q>>, SymplexError> {
if let Some(rows) = m.to_rational_rows() {
return Ok(rows);
}
let evaluated = m.eval();
evaluated.to_rational_rows().ok_or_else(|| {
let bad = evaluated
.iter()
.find(|e| e.as_rational().is_none())
.map(|e| e.to_string())
.unwrap_or_default();
invalid(
"linprog_matrix",
format!("{what} must contain only numeric literals; found `{bad}`"),
)
})
}
fn matrix_to_vec(m: &Matrix, what: &str) -> Result<Vec<Q>, SymplexError> {
if m.ncols() != 1 && m.nrows() != 1 {
return Err(invalid(
"linprog_matrix",
format!(
"{what} must be a row or column vector, got {}×{}",
m.nrows(),
m.ncols()
),
));
}
Ok(matrix_to_q(m, what)?.into_iter().flatten().collect())
}
fn add_matrix_block(
p: &mut LpProblem,
a: Option<&Matrix>,
b: Option<&Matrix>,
relation: Relation,
name: &str,
) -> Result<(), SymplexError> {
let n = p.num_vars();
match (a, b) {
(None, None) => Ok(()),
(Some(a), Some(b)) => {
if a.ncols() != n {
return Err(invalid(
"linprog_matrix",
format!("A_{name} has {} columns but c has {n} entries", a.ncols()),
));
}
let bv = matrix_to_vec(b, &format!("b_{name}"))?;
if bv.len() != a.nrows() {
return Err(invalid(
"linprog_matrix",
format!(
"A_{name} has {} rows but b_{name} has {} entries",
a.nrows(),
bv.len()
),
));
}
let rows = matrix_to_q(a, &format!("A_{name}"))?;
for (row, rhs) in rows.into_iter().zip(bv) {
p.add(row, rhs, relation);
}
Ok(())
}
_ => Err(invalid(
"linprog_matrix",
format!("A_{name} and b_{name} must be given together"),
)),
}
}
pub fn linprog_matrix(
objective: Objective,
c: &Matrix,
a_ub: Option<&Matrix>,
b_ub: Option<&Matrix>,
a_eq: Option<&Matrix>,
b_eq: Option<&Matrix>,
) -> Result<LpSolution, SymplexError> {
let cv = matrix_to_vec(c, "c")?;
let mut p = match objective {
Objective::Minimize => LpProblem::minimize(cv),
Objective::Maximize => LpProblem::maximize(cv),
};
add_matrix_block(&mut p, a_ub, b_ub, Relation::Le, "ub")?;
add_matrix_block(&mut p, a_eq, b_eq, Relation::Eq, "eq")?;
p.solve()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn textbook_max() {
let sol = LpProblem::maximize(vec![qi(3), qi(2)])
.le(vec![qi(1), qi(1)], qi(4))
.le(vec![qi(1), qi(3)], qi(6))
.solve()
.unwrap();
assert_eq!(sol.status, LpStatus::Optimal);
assert_eq!(sol.x, vec![qi(4), qi(0)]);
assert_eq!(sol.objective, Some(qi(12)));
}
#[test]
fn infeasible_has_certificate() {
let sol = LpProblem::minimize(vec![qi(1), qi(1)])
.le(vec![qi(1), qi(1)], qi(1))
.ge(vec![qi(1), qi(1)], qi(2))
.solve()
.unwrap();
assert_eq!(sol.status, LpStatus::Infeasible);
let y = sol.farkas.unwrap();
assert!(!y[0].is_negative());
assert!(!y[1].is_positive());
let g = &y[0] + &y[1];
assert!(!g.is_negative());
assert!((&y[0] + &(&y[1] * &qi(2))).is_negative());
}
#[test]
fn unbounded_detected() {
let sol = LpProblem::maximize(vec![qi(1), qi(0)])
.le(vec![qi(0), qi(1)], qi(1))
.solve()
.unwrap();
assert_eq!(sol.status, LpStatus::Unbounded);
}
#[test]
fn free_variable_negative_optimum() {
let sol = LpProblem::minimize(vec![qi(1)])
.ge(vec![qi(1)], qi(-3))
.free(0)
.solve()
.unwrap();
assert_eq!(sol.status, LpStatus::Optimal);
assert_eq!(sol.x, vec![qi(-3)]);
}
#[test]
fn negative_common_denominator_after_driving_out_artificials() {
let p = LpProblem::minimize(vec![qi(0), qi(0), qi(1)])
.eq(vec![qi(-1), qi(-1), qi(0)], qi(0))
.le(vec![qi(1), qi(0), qi(1)], qi(5));
let Standardized::Ready(sf) = standardize(&p) else {
panic!("bounds are fine");
};
fn walk<T: Cell>(sf: &Standard) -> (Vec<Q>, usize) {
let mut t = Tableau::<T>::new(sf).expect("fits");
let (m, n) = (t.m, t.n);
let phase1 = t.phase1_costs();
t.set_objective(&phase1).expect("fits");
assert!(matches!(t.run(n + m).ok().unwrap(), Step::Optimal));
assert!(t.neg_objective().is_zero(), "feasible");
assert!(t.basis[0] >= n, "artificial of row 0 still basic");
assert_eq!(t.d.signum(), std::cmp::Ordering::Greater);
t.drive_out_artificials().expect("fits");
assert!(t.basis[0] < n, "artificial driven out");
assert!(
t.d.is_negative(),
"pivot on a negative entry flips the common denominator (d = {:?})",
t.d
);
let pivots_before = t.pivots_done;
let mut phase2 = vec![Q::zero(); n + m];
phase2[..n].clone_from_slice(&sf.c);
t.set_objective(&phase2).expect("fits");
assert!(matches!(t.run(n).ok().unwrap(), Step::Optimal));
assert!(t.pivots_done > pivots_before, "phase 2 pivoted with d < 0");
let z = t.solution();
assert!(z.iter().all(|v| !v.is_negative()), "z = {z:?}");
(z, t.pivots_done)
}
assert_eq!(walk::<i64>(&sf), walk::<BigInt>(&sf));
let sol = p.solve().unwrap();
assert_eq!(sol.status, LpStatus::Optimal);
assert_eq!(sol.x, vec![qi(0), qi(0), qi(0)]);
assert_eq!(sol.objective, Some(qi(0)));
assert_eq!(sol.duals[1], qi(0));
}
#[test]
fn i256_intermediates_match_bigint() {
let mut state: u128 = 0x9E37_79B9_7F4A_7C15_F39C_C060_5CED_C834;
let mut next = |bits: u32| -> i128 {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
let mask = if bits >= 128 {
u128::MAX
} else {
(1u128 << bits) - 1
};
let v = (state & mask) as i128;
if state & (1 << 5) != 0 { -v } else { v }
};
for _ in 0..3000 {
let (a, b, c, d) = (next(120), next(120), next(120), next(120));
let big = |x: i128| BigInt::from(x);
let ab = I256::mul(a, b);
let cd = I256::mul(c, d);
assert_eq!(
ab.cmp(&cd),
(big(a) * big(b)).cmp(&(big(c) * big(d))),
"{a} {b} {c} {d}"
);
assert_eq!(
<i128 as Cell>::cmp_products(&a, &b, &c, &d),
(big(a) * big(b)).cmp(&(big(c) * big(d)))
);
let d0 = next(60);
if d0 != 0 {
let k = next(60);
let v = d0 * k;
let b2 = if k & 1 == 0 { next(64) } else { b };
let exp = big(k) * big(b2);
assert_eq!(
<i128 as Cell>::rescale(&v, &b2, &d0),
i128::try_from(&exp).ok(),
"{v} {b2} {d0}"
);
let f = d0 * next(30);
let pr = next(64);
let exp2 = big(k) * big(b2) - big(f) / big(d0) * big(pr);
assert_eq!(
<i128 as Cell>::pivot_update(&v, &b2, &f, &pr, &d0),
i128::try_from(&exp2).ok(),
"{v} {b2} {f} {pr} {d0}"
);
}
let diff = ab.sub(cd);
let exp = big(a) * big(b) - big(c) * big(d);
assert_eq!(diff.to_i128(), i128::try_from(&exp).ok(), "{exp}");
assert_eq!(diff.is_negative(), Signed::is_negative(&exp));
}
for &(x, d) in &[
(i128::MAX, 1i128),
(i128::MIN, 1),
(i128::MIN, -1),
(i128::MAX, i128::MAX),
(1 << 100, 1 << 40),
] {
let prod = I256::mul(x, d);
let got = prod.div_exact(d);
let expected =
i128::try_from(&(BigInt::from(x) * BigInt::from(d) / BigInt::from(d))).ok();
assert_eq!(got, expected, "{x} · {d} / {d}");
}
let over = I256::mul(i128::MAX, 4).sub(I256::from_i128(0));
assert_eq!(over.div_exact(2), None);
assert_eq!(I256::mul(i128::MAX, 4).div_exact(4), Some(i128::MAX));
}
#[test]
fn hybrid_arithmetic_falls_back_to_bigint_on_overflow() {
let big = |k: i64| qi(k) * qi(1 << 40);
let p = LpProblem::minimize(vec![qi(1), qi(1), qi(1)])
.ge(vec![big(3), big(1), big(2)], big(7))
.ge(vec![big(1), big(5), big(1)], big(11))
.le(vec![big(2), big(1), big(3)], big(40));
let Standardized::Ready(sf) = standardize(&p) else {
panic!("bounds are fine");
};
assert!(
matches!(solve_standard::<i64>(&p, &sf), Err(Halt::Overflow)),
"the i64 attempt must report overflow, not a wrong answer"
);
let sol = p.solve().unwrap();
let via_big = solve_standard::<BigInt>(&p, &sf).ok().unwrap();
assert_eq!(sol.status, LpStatus::Optimal);
assert_eq!(sol.x, via_big.x);
assert_eq!(sol.objective, via_big.objective);
assert_eq!(sol.duals, via_big.duals);
let obj = sol.objective.unwrap();
assert!(obj.is_positive());
let small = LpProblem::minimize(vec![qi(1), qi(2)])
.ge(vec![qi(1), qi(1)], qi(1))
.le(vec![qi(3), qi(1)], qi(6));
let Standardized::Ready(sf2) = standardize(&small) else {
panic!("bounds are fine");
};
assert!(solve_standard::<i64>(&small, &sf2).is_ok());
}
#[test]
fn malformed_inputs_are_errors() {
assert!(LpProblem::minimize(vec![]).solve().is_err());
assert!(
LpProblem::minimize(vec![qi(1)])
.le(vec![qi(1), qi(2)], qi(1))
.solve()
.is_err()
);
assert!(
LpProblem::minimize(vec![qi(1)])
.bounds(3, None, None)
.solve()
.is_err()
);
}
}