use std::fmt;
use std::time::Instant;
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::base::interval::Bounds;
use crate::domains::exact_kernel::{Cell, W256, pivot_row_update};
use crate::domains::matrix::Matrix;
pub use crate::base::numeric::{Q, q, qi};
fn invalid(operation: &'static str, reason: impl Into<String>) -> SymplexError {
SymplexError::invalid_argument(operation, reason)
}
fn failed(operation: &'static str, reason: impl Into<String>) -> SymplexError {
SymplexError::computation_failed(operation, reason)
}
#[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,
}
pub use crate::base::budget::{Budget, BudgetHit};
pub(crate) enum Stop {
Budget(BudgetHit),
Error(SymplexError),
}
impl From<SymplexError> for Stop {
fn from(e: SymplexError) -> Self {
Stop::Error(e)
}
}
pub(crate) struct LpMeter {
budget: Budget,
spent: usize,
}
impl LpMeter {
pub(crate) fn start(budget: &Budget) -> Self {
LpMeter {
budget: budget.start(),
spent: 0,
}
}
pub(crate) fn spent(&self) -> usize {
self.spent
}
pub(crate) fn remaining(&self) -> Budget {
Budget {
deadline: self.budget.deadline,
time_limit: None,
max_pivots: self.budget.max_pivots.map(|m| m.saturating_sub(self.spent)),
}
}
pub(crate) fn solve(&mut self, lp: LpProblem) -> Result<LpSolution, Stop> {
let report = lp.with_budget(self.remaining()).solve_report()?;
self.spent += report.pivots;
match report.budget_hit {
Some(hit) => Err(Stop::Budget(hit)),
None => Ok(report.solution),
}
}
}
#[derive(Clone, Debug)]
pub struct LpProblem {
objective: Objective,
c: Vec<Q>,
constraints: Vec<Constraint>,
bounds: Vec<Bounds<Q>>,
bad_var: Option<usize>,
budget: Budget,
}
impl LpProblem {
fn new(objective: Objective, c: Vec<Q>) -> Self {
let n = c.len();
LpProblem {
objective,
c,
constraints: Vec::new(),
bounds: vec![Bounds::at_least(Q::zero()); n],
bad_var: None,
budget: Budget::default(),
}
}
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, bounds: Bounds<Q>) -> Self {
match self.bounds.get_mut(var) {
Some(slot) => *slot = bounds,
None => self.bad_var = self.bad_var.or(Some(var)),
}
self
}
pub fn free(self, var: usize) -> Self {
self.bounds(var, Bounds::free())
}
pub fn num_vars(&self) -> usize {
self.c.len()
}
pub fn num_constraints(&self) -> usize {
self.constraints.len()
}
#[must_use]
pub fn with_budget(mut self, budget: Budget) -> Self {
self.budget = budget;
self
}
pub fn budget(&self) -> &Budget {
&self.budget
}
pub fn solve(&self) -> Result<LpSolution, SymplexError> {
self.solve_report().map(|r| r.solution)
}
pub(crate) fn solve_report(&self) -> Result<SolveReport, 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,
BudgetExhausted,
}
pub(crate) struct SolveReport {
pub(crate) solution: LpSolution,
pub(crate) pivots: usize,
pub(crate) budget_hit: Option<BudgetHit>,
}
#[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,
}
}
fn budget_exhausted() -> Self {
LpSolution {
status: LpStatus::BudgetExhausted,
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 bounds in &p.bounds {
match (&bounds.lower, &bounds.upper) {
(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();
if !lo.is_zero() {
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();
if !lo.is_zero() {
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<'a, 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,
budget: &'a Budget,
spent: &'a mut usize,
stall: usize,
}
const STALL_LIMIT: usize = 12;
enum Step {
Optimal,
Unbounded,
}
enum Halt {
Overflow,
Budget(BudgetHit),
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
.map(Ratio::denom)
.filter(|d| !d.is_one())
.fold(<BigInt as One>::one(), |l, d| l.lcm(d))
}
impl<'a, T: Cell> Tableau<'a, T> {
fn new(std: &Standard, budget: &'a Budget, spent: &'a mut usize) -> 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| {
if Zero::is_zero(q) {
Some(T::cell_zero())
} else {
T::from_ratio_scaled(q, &s)
}
};
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),
budget,
spent,
stall: 0,
})
}
#[inline]
fn check_deadline(&self) -> Result<(), Halt> {
if self.budget.deadline_passed() {
return Err(Halt::Budget(BudgetHit::Deadline));
}
Ok(())
}
#[inline]
fn check_pivot_budget(&self) -> Result<(), Halt> {
if let Some(cap) = self.budget.max_pivots
&& *self.spent >= cap
{
return Err(Halt::Budget(BudgetHit::MaxPivots));
}
Ok(())
}
#[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_ratio_scaled(q, &s);
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<()> {
*self.spent += 1;
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 = T::divisor(&std::mem::replace(&mut self.d, p.clone()));
for i in 0..self.m {
if i == r {
continue;
}
pivot_row_update(&mut self.rows[i * w..(i + 1) * w], &prow, s, &p, &d)?;
}
pivot_row_update(&mut self.obj, &prow, s, &p, &d)?;
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 {
self.check_deadline()?;
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);
};
self.check_pivot_budget()?;
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) -> Result<(), Halt> {
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.check_deadline()?;
self.check_pivot_budget()?;
self.pivot(r, s).ok_or(Halt::Overflow)?;
}
}
Ok(())
}
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<SolveReport, SymplexError> {
let prof = tracing::enabled!(target: "symplex::linprog::prof", tracing::Level::DEBUG);
let started = Instant::now();
let sf = match standardize(p) {
Standardized::Ready(s) => s,
Standardized::BoundsInfeasible => {
return Ok(SolveReport {
solution: LpSolution::infeasible(None),
pivots: 0,
budget_hit: None,
});
}
};
let budget = p.budget.start();
let mut spent = 0usize;
let settle =
|r: Result<LpSolution, Halt>, spent: usize| -> Option<Result<SolveReport, SymplexError>> {
match r {
Ok(solution) => Some(Ok(SolveReport {
solution,
pivots: spent,
budget_hit: None,
})),
Err(Halt::Budget(hit)) => Some(Ok(SolveReport {
solution: LpSolution::budget_exhausted(),
pivots: spent,
budget_hit: Some(hit),
})),
Err(Halt::Error(e)) => Some(Err(e)),
Err(Halt::Overflow) => None,
}
};
let attempt = |cell: &'static str, r: &Result<LpSolution, Halt>, spent: usize| {
if prof {
tracing::debug!(
target: "symplex::linprog::prof",
cell,
rows = sf.a.len(),
cols = sf.c.len(),
overflowed = matches!(r, Err(Halt::Overflow)),
spent,
micros = started.elapsed().as_micros() as u64,
"linprog attempt"
);
}
};
let r = solve_standard::<i64>(p, &sf, &budget, &mut spent);
attempt("i64", &r, spent);
if let Some(done) = settle(r, spent) {
return done;
}
let r = solve_standard::<i128>(p, &sf, &budget, &mut spent);
attempt("i128", &r, spent);
if let Some(done) = settle(r, spent) {
return done;
}
let r = solve_standard::<W256>(p, &sf, &budget, &mut spent);
attempt("W256", &r, spent);
if let Some(done) = settle(r, spent) {
return done;
}
tracing::debug!(target: "symplex::linprog", rows = sf.a.len(), cols = sf.c.len(), "256-bit tableau overflowed; solving on BigInt");
let r = solve_standard::<BigInt>(p, &sf, &budget, &mut spent);
attempt("BigInt", &r, spent);
settle(r, spent).unwrap_or_else(|| {
Err(failed(
"linprog",
"internal: fraction-free update was not exact on BigInt cells",
))
})
}
fn solve_standard<T: Cell>(
p: &LpProblem,
sf: &Standard,
budget: &Budget,
spent: &mut usize,
) -> Result<LpSolution, Halt> {
let mut t = Tableau::<T>::new(sf, budget, spent).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()?;
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: &[Bounds<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, b) in bounds.iter().enumerate() {
p = p.bounds(j, b.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"),
LpStatus::BudgetExhausted => write!(f, "Budget exhausted"),
}
}
}
#[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,
LpStatus::BudgetExhausted => {
return Err(failed("feasible_nonneg", "budget exhausted"));
}
})
}
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 },
LpStatus::BudgetExhausted => {
return Err(failed("feasible_nonneg_certified", "budget exhausted"));
}
})
}
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 std::time::Duration;
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 budget = Budget::default();
let mut spent = 0;
let mut t = Tableau::<T>::new(sf, &budget, &mut spent).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);
assert!(t.drive_out_artificials().is_ok(), "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 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, &Budget::default(), &mut 0),
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, &Budget::default(), &mut 0)
.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, &Budget::default(), &mut 0).is_ok());
}
#[test]
fn hybrid_arithmetic_uses_256_bit_cells_before_bigint() {
let big = |k: i64| qi(k) * Ratio::from_integer(BigInt::from(1u128 << 70));
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, &Budget::default(), &mut 0),
Err(Halt::Overflow)
));
assert!(matches!(
solve_standard::<i128>(&p, &sf, &Budget::default(), &mut 0),
Err(Halt::Overflow)
));
let via_w256 = solve_standard::<W256>(&p, &sf, &Budget::default(), &mut 0)
.ok()
.unwrap();
let via_big = solve_standard::<BigInt>(&p, &sf, &Budget::default(), &mut 0)
.ok()
.unwrap();
assert_eq!(via_w256.status, LpStatus::Optimal);
assert_eq!(via_w256.x, via_big.x);
assert_eq!(via_w256.objective, via_big.objective);
assert_eq!(via_w256.duals, via_big.duals);
let sol = p.solve().unwrap();
assert_eq!(sol.x, via_big.x);
assert_eq!(sol.duals, via_big.duals);
}
#[test]
fn budget_pivots_are_shared_across_cell_type_attempts() {
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 full = p.solve_report().unwrap();
assert_eq!(full.solution.status, LpStatus::Optimal);
assert!(full.pivots >= 2, "pivots = {}", full.pivots);
let Standardized::Ready(sf) = standardize(&p) else {
panic!("bounds are fine");
};
let mut wasted = 0usize;
assert!(matches!(
solve_standard::<i64>(&p, &sf, &Budget::default(), &mut wasted),
Err(Halt::Overflow)
));
let capped = p
.clone()
.with_budget(Budget::max_pivots(full.pivots - wasted))
.solve_report()
.unwrap();
assert_eq!(capped.solution.status, LpStatus::BudgetExhausted);
assert_eq!(capped.budget_hit, Some(BudgetHit::MaxPivots));
assert!(capped.solution.x.is_empty() && capped.solution.objective.is_none());
let exact = p
.with_budget(Budget::max_pivots(full.pivots))
.solve_report()
.unwrap();
assert_eq!(exact.solution.status, LpStatus::Optimal);
assert_eq!(exact.solution.x, full.solution.x);
assert_eq!(exact.pivots, full.pivots);
}
#[test]
fn budget_deadline_in_the_past_stops_before_the_first_pivot() {
let p = LpProblem::maximize(vec![qi(3), qi(2)])
.le(vec![qi(1), qi(1)], qi(4))
.le(vec![qi(1), qi(3)], qi(6));
let past = Instant::now() - Duration::from_secs(1);
let r = p
.clone()
.with_budget(Budget::deadline(past))
.solve_report()
.unwrap();
assert_eq!(r.solution.status, LpStatus::BudgetExhausted);
assert_eq!(r.budget_hit, Some(BudgetHit::Deadline));
assert_eq!(r.pivots, 0);
assert_eq!(r.solution.to_string(), "Budget exhausted");
let r = p
.with_budget(Budget::within(Duration::ZERO))
.solve_report()
.unwrap();
assert_eq!(r.budget_hit, Some(BudgetHit::Deadline));
}
#[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, Bounds::free())
.solve()
.is_err()
);
}
}