use std::fmt;
use std::time::{Duration, 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::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,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Budget {
pub deadline: Option<Instant>,
pub max_pivots: Option<usize>,
}
impl Budget {
pub fn deadline(at: Instant) -> Self {
Budget {
deadline: Some(at),
max_pivots: None,
}
}
pub fn max_pivots(n: usize) -> Self {
Budget {
deadline: None,
max_pivots: Some(n),
}
}
pub fn within(duration: Duration) -> Self {
Budget {
deadline: Instant::now().checked_add(duration),
max_pivots: None,
}
}
#[must_use]
pub fn with_deadline(mut self, at: Instant) -> Self {
self.deadline = Some(at);
self
}
#[must_use]
pub fn with_max_pivots(mut self, n: usize) -> Self {
self.max_pivots = Some(n);
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum BudgetHit {
Deadline,
MaxPivots,
}
impl fmt::Display for BudgetHit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
BudgetHit::Deadline => "deadline",
BudgetHit::MaxPivots => "max_pivots",
})
}
}
pub(crate) fn deadline_from(
deadline: Option<Instant>,
time_limit: Option<Duration>,
) -> Option<Instant> {
let from_limit = time_limit.and_then(|t| Instant::now().checked_add(t));
match (deadline, from_limit) {
(Some(a), Some(b)) => Some(a.min(b)),
(a, b) => a.or(b),
}
}
pub(crate) fn deadline_passed(deadline: Option<Instant>) -> bool {
deadline.is_some_and(|d| Instant::now() >= d)
}
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 {
deadline: Option<Instant>,
max_pivots: Option<usize>,
spent: usize,
}
impl LpMeter {
pub(crate) fn start(
deadline: Option<Instant>,
time_limit: Option<Duration>,
max_pivots: Option<usize>,
) -> Self {
LpMeter {
deadline: deadline_from(deadline, time_limit),
max_pivots,
spent: 0,
}
}
pub(crate) fn spent(&self) -> usize {
self.spent
}
pub(crate) fn remaining(&self) -> Budget {
Budget {
deadline: self.deadline,
max_pivots: self.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;
trait Cell: Clone + PartialEq + Eq + Ord + fmt::Debug {
type Divisor;
fn divisor(d: &Self) -> Self::Divisor;
fn cell_zero() -> Self;
fn cell_one() -> Self;
fn from_big(v: &BigInt) -> Option<Self>;
fn from_ratio_scaled(q: &Q, s: &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::Divisor) -> Option<Self>;
fn rescale(v: &Self, p: &Self, d: &Self::Divisor) -> 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;
}
fn big_div_exact(t: BigInt, d: &BigInt) -> Option<BigInt> {
let (q, r) = t.div_rem(d);
debug_assert!(Zero::is_zero(&r), "integer pivoting: inexact division");
Zero::is_zero(&r).then_some(q)
}
impl Cell for BigInt {
type Divisor = BigInt;
fn divisor(d: &Self) -> BigInt {
d.clone()
}
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 from_ratio_scaled(q: &Q, s: &BigInt) -> Option<Self> {
Some(q.numer() * (s / q.denom()))
}
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: &BigInt) -> Option<Self> {
let t = if Zero::is_zero(pr) {
v * p
} else if Zero::is_zero(v) {
-(f * pr)
} else {
v * p - f * pr
};
big_div_exact(t, d)
}
fn rescale(v: &Self, p: &Self, d: &BigInt) -> Option<Self> {
big_div_exact(v * p, 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))
}
}
#[derive(Clone, Copy, Debug)]
struct Div64 {
d: i64,
shift: u32,
inv: u64,
}
impl Div64 {
fn new(d: i64) -> Self {
debug_assert!(d != 0, "pivot on a zero entry");
let shift = d.trailing_zeros();
let d_odd = (d >> shift) as u64;
Div64 {
d,
shift,
inv: inverse_mod_2_64(d_odd),
}
}
#[inline]
fn div_exact(&self, t: i128) -> Option<i64> {
debug_assert!(
t % i128::from(self.d) == 0,
"integer pivoting: inexact division"
);
let q = ((t >> self.shift) as u64).wrapping_mul(self.inv) as i64;
(i128::from(q) * i128::from(self.d) == t).then_some(q)
}
}
fn inverse_mod_2_64(d: u64) -> u64 {
debug_assert!(d & 1 == 1);
let mut x = d;
for _ in 0..5 {
x = x.wrapping_mul(2u64.wrapping_sub(d.wrapping_mul(x)));
}
debug_assert_eq!(d.wrapping_mul(x), 1);
x
}
#[inline]
fn ratio_scaled_i128(q: &Q, s: &BigInt) -> Option<i128> {
let numer = i64::try_from(q.numer()).ok()?;
let denom = i64::try_from(q.denom()).ok()?;
let s = i64::try_from(s).ok()?;
Some(i128::from(numer) * i128::from(s / denom))
}
impl Cell for i64 {
type Divisor = Div64;
fn divisor(d: &Self) -> Div64 {
Div64::new(*d)
}
fn cell_zero() -> Self {
0
}
fn cell_one() -> Self {
1
}
fn from_big(v: &BigInt) -> Option<Self> {
i64::try_from(v).ok()
}
fn from_ratio_scaled(q: &Q, s: &BigInt) -> Option<Self> {
match ratio_scaled_i128(q, s) {
Some(v) => i64::try_from(v).ok(),
None => i64::try_from(q.numer() * (s / q.denom())).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: &Div64) -> Option<Self> {
let t = i128::from(*v) * i128::from(*p) - i128::from(*f) * i128::from(*pr);
d.div_exact(t)
}
fn rescale(v: &Self, p: &Self, d: &Div64) -> Option<Self> {
d.div_exact(i128::from(*v) * i128::from(*p))
}
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: &Div128) -> Option<u128> {
debug_assert!(!self.is_negative());
let hi = self.hi as u128;
let k = d.shift;
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(d.inv))
}
fn div_exact_by(self, d: &Div128) -> Option<i128> {
let neg = self.is_negative() != (d.d < 0);
let mag = if self.is_negative() { self.neg() } else { self };
let q = mag.div_exact_unsigned(d)?;
if neg {
if q > (1u128 << 127) {
return None;
}
Some((q as i128).wrapping_neg())
} else {
i128::try_from(q).ok()
}
}
#[cfg(test)]
fn div_exact(self, d: i128) -> Option<i128> {
self.div_exact_by(&Div128::new(d))
}
}
#[derive(Clone, Copy, Debug)]
struct Div128 {
d: i128,
shift: u32,
odd: u128,
inv: u128,
}
impl Div128 {
fn new(d: i128) -> Self {
debug_assert!(d != 0, "pivot on a zero entry");
let abs = d.unsigned_abs();
let shift = abs.trailing_zeros();
let odd = abs >> shift;
Div128 {
d,
shift,
odd,
inv: inverse_mod_2_128(odd),
}
}
}
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 {
type Divisor = Div128;
fn divisor(d: &Self) -> Div128 {
Div128::new(*d)
}
fn cell_zero() -> Self {
0
}
fn cell_one() -> Self {
1
}
fn from_big(v: &BigInt) -> Option<Self> {
i128::try_from(v).ok()
}
fn from_ratio_scaled(q: &Q, s: &BigInt) -> Option<Self> {
match ratio_scaled_i128(q, s) {
Some(v) => Some(v),
None => i128::try_from(q.numer() * (s / q.denom())).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: &Div128) -> Option<Self> {
let t = I256::mul(*v, *p).sub(I256::mul(*f, *pr));
t.div_exact_by(d)
}
fn rescale(v: &Self, p: &Self, d: &Div128) -> Option<Self> {
I256::mul(*v, *p).div_exact_by(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))
}
}
mod limbs {
use std::cmp::Ordering;
#[inline]
pub(super) fn sub<const N: usize>(a: &[u64; N], b: &[u64; N]) -> ([u64; N], bool) {
let mut out = [0u64; N];
let mut borrow = false;
for i in 0..N {
let (s, b1) = a[i].overflowing_sub(b[i]);
let (s, b2) = s.overflowing_sub(u64::from(borrow));
out[i] = s;
borrow = b1 || b2;
}
(out, borrow)
}
#[inline]
pub(super) fn neg<const N: usize>(a: &[u64; N]) -> [u64; N] {
sub(&[0u64; N], a).0
}
#[inline]
pub(super) fn is_negative<const N: usize>(a: &[u64; N]) -> bool {
a[N - 1] >> 63 == 1
}
#[inline]
pub(super) fn cmp_signed<const N: usize>(a: &[u64; N], b: &[u64; N]) -> Ordering {
match (is_negative(a), is_negative(b)) {
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
_ => a.iter().rev().cmp(b.iter().rev()),
}
}
#[inline]
pub(super) fn mul_low<const N: usize>(a: &[u64; N], b: &[u64; N]) -> [u64; N] {
let mut out = [0u64; N];
for i in 0..N {
let mut carry = 0u128;
for j in 0..N - i {
let t = u128::from(a[i]) * u128::from(b[j]) + u128::from(out[i + j]) + carry;
out[i + j] = t as u64;
carry = t >> 64;
}
}
out
}
#[inline]
pub(super) fn mul_4x4(a: &[u64; 4], b: &[u64; 4]) -> [u64; 8] {
let mut out = [0u64; 8];
for i in 0..4 {
let mut carry = 0u128;
for j in 0..4 {
let t = u128::from(a[i]) * u128::from(b[j]) + u128::from(out[i + j]) + carry;
out[i + j] = t as u64;
carry = t >> 64;
}
out[i + 4] = carry as u64;
}
out
}
#[inline]
pub(super) fn shr_signed<const N: usize>(a: &[u64; N], k: u32) -> [u64; N] {
let fill = if is_negative(a) { u64::MAX } else { 0 };
let limbs = (k / 64) as usize;
let bits = k % 64;
let mut out = [fill; N];
for i in 0..N - limbs {
let lo = a[i + limbs] >> bits;
let hi = if bits == 0 {
0
} else if i + limbs + 1 < N {
a[i + limbs + 1] << (64 - bits)
} else {
fill << (64 - bits)
};
out[i] = lo | hi;
}
out
}
#[inline]
pub(super) fn trailing_zeros<const N: usize>(a: &[u64; N]) -> u32 {
let mut n = 0;
for limb in a {
if *limb == 0 {
n += 64;
} else {
return n + limb.trailing_zeros();
}
}
n
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
struct W256([u64; 4]);
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
struct W512([u64; 8]);
impl W256 {
const ZERO: W256 = W256([0; 4]);
const ONE: W256 = W256([1, 0, 0, 0]);
fn from_i128(v: i128) -> Self {
let fill = if v < 0 { u64::MAX } else { 0 };
let u = v as u128;
W256([u as u64, (u >> 64) as u64, fill, fill])
}
fn from_big(v: &BigInt) -> Option<Self> {
let mut mag = [0u64; 4];
for (i, digit) in v.iter_u64_digits().enumerate() {
*mag.get_mut(i)? = digit;
}
if mag[3] >> 63 == 1 {
return None;
}
Some(W256(if Signed::is_negative(v) {
limbs::neg(&mag)
} else {
mag
}))
}
#[inline]
fn is_zero(&self) -> bool {
self.0 == [0; 4]
}
#[inline]
fn is_negative(&self) -> bool {
limbs::is_negative(&self.0)
}
#[inline]
fn mul_full(a: &W256, b: &W256) -> W512 {
let neg = a.is_negative() != b.is_negative();
let ua = if a.is_negative() {
limbs::neg(&a.0)
} else {
a.0
};
let ub = if b.is_negative() {
limbs::neg(&b.0)
} else {
b.0
};
let prod = limbs::mul_4x4(&ua, &ub);
W512(if neg { limbs::neg(&prod) } else { prod })
}
#[inline]
fn widen(&self) -> W512 {
let fill = if self.is_negative() { u64::MAX } else { 0 };
let a = &self.0;
W512([a[0], a[1], a[2], a[3], fill, fill, fill, fill])
}
}
impl PartialOrd for W256 {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for W256 {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
limbs::cmp_signed(&self.0, &other.0)
}
}
impl W512 {
#[inline]
fn sub(&self, o: &W512) -> W512 {
W512(limbs::sub(&self.0, &o.0).0)
}
#[inline]
fn narrow(&self) -> Option<W256> {
let a = &self.0;
let fill = if a[3] >> 63 == 1 { u64::MAX } else { 0 };
(a[4] == fill && a[5] == fill && a[6] == fill && a[7] == fill)
.then(|| W256([a[0], a[1], a[2], a[3]]))
}
#[inline]
fn cmp(&self, o: &W512) -> std::cmp::Ordering {
limbs::cmp_signed(&self.0, &o.0)
}
}
#[derive(Clone, Copy, Debug)]
struct Div256 {
d: W256,
shift: u32,
inv: W256,
}
impl Div256 {
fn new(d: W256) -> Self {
debug_assert!(!d.is_zero(), "pivot on a zero entry");
let shift = limbs::trailing_zeros(&d.0);
let d_odd = limbs::shr_signed(&d.0, shift);
let mut x = d_odd;
for _ in 0..7 {
let dx = limbs::mul_low(&d_odd, &x);
let two_minus = limbs::sub(&[2, 0, 0, 0], &dx).0;
x = limbs::mul_low(&x, &two_minus);
}
debug_assert_eq!(limbs::mul_low(&d_odd, &x), [1, 0, 0, 0]);
Div256 {
d,
shift,
inv: W256(x),
}
}
#[inline]
fn div_exact(&self, t: &W512) -> Option<W256> {
let shifted = limbs::shr_signed(&t.0, self.shift);
let low = [shifted[0], shifted[1], shifted[2], shifted[3]];
let q = W256(limbs::mul_low(&low, &self.inv.0));
(W256::mul_full(&q, &self.d) == *t).then_some(q)
}
}
impl Cell for W256 {
type Divisor = Div256;
fn divisor(d: &Self) -> Div256 {
Div256::new(*d)
}
fn cell_zero() -> Self {
W256::ZERO
}
fn cell_one() -> Self {
W256::ONE
}
fn from_big(v: &BigInt) -> Option<Self> {
W256::from_big(v)
}
fn from_ratio_scaled(q: &Q, s: &BigInt) -> Option<Self> {
match ratio_scaled_i128(q, s) {
Some(v) => Some(W256::from_i128(v)),
None => W256::from_big(&(q.numer() * (s / q.denom()))),
}
}
fn to_big(&self) -> BigInt {
let neg = self.is_negative();
let mag = if neg { limbs::neg(&self.0) } else { self.0 };
let mut bytes = [0u8; 32];
for (i, limb) in mag.iter().enumerate() {
bytes[8 * i..8 * i + 8].copy_from_slice(&limb.to_le_bytes());
}
BigInt::from_bytes_le(
if neg {
num_bigint::Sign::Minus
} else {
num_bigint::Sign::Plus
},
&bytes,
)
}
fn is_zero(&self) -> bool {
W256::is_zero(self)
}
fn is_negative(&self) -> bool {
W256::is_negative(self)
}
fn signum(&self) -> std::cmp::Ordering {
if self.is_zero() {
std::cmp::Ordering::Equal
} else if self.is_negative() {
std::cmp::Ordering::Less
} else {
std::cmp::Ordering::Greater
}
}
fn neg(&self) -> Option<Self> {
let n = W256(limbs::neg(&self.0));
(n.is_negative() != self.is_negative() || self.is_zero()).then_some(n)
}
fn mul(&self, o: &Self) -> Option<Self> {
W256::mul_full(self, o).narrow()
}
fn pivot_update(v: &Self, p: &Self, f: &Self, pr: &Self, d: &Div256) -> Option<Self> {
let t = W256::mul_full(v, p).sub(&W256::mul_full(f, pr));
d.div_exact(&t)
}
fn rescale(v: &Self, p: &Self, d: &Div256) -> Option<Self> {
d.div_exact(&W256::mul_full(v, p))
}
fn sub_mul(&self, f: &Self, r: &Self) -> Option<Self> {
self.widen().sub(&W256::mul_full(f, r)).narrow()
}
fn cmp_products(a: &Self, b: &Self, c: &Self, d: &Self) -> std::cmp::Ordering {
W256::mul_full(a, b).cmp(&W256::mul_full(c, d))
}
}
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 deadline_passed(self.budget.deadline) {
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()));
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 {
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 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, &mut spent);
attempt("i64", &r, spent);
if let Some(done) = settle(r, spent) {
return done;
}
let r = solve_standard::<i128>(p, &sf, &mut spent);
attempt("i128", &r, spent);
if let Some(done) = settle(r, spent) {
return done;
}
let r = solve_standard::<W256>(p, &sf, &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, &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,
spent: &mut usize,
) -> Result<LpSolution, Halt> {
let mut t = Tableau::<T>::new(sf, &p.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 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 i64_exact_division_matches_bigint() {
let check = |t: i128, d: i64| {
let want = i64::try_from(t / i128::from(d)).ok();
let got = Div64::new(d).div_exact(t);
assert_eq!(got, want, "{t} / {d}");
};
let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
let mut next = |bits: u32| -> i64 {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
let v = (state & ((1u64 << bits) - 1)) as i64;
if state & (1 << 5) != 0 { -v } else { v }
};
for _ in 0..20_000 {
let d = next(40);
if d == 0 {
continue;
}
let q_small = next(62);
check(i128::from(q_small) * i128::from(d), d);
let q_big = i128::from(next(62)) * 4 + i128::from(i64::MAX);
check(q_big * i128::from(d), d);
let (v, p, f, pr) = (next(20) * d, next(32), next(20) * d, next(32));
let dv = Div64::new(d);
let t = i128::from(v) * i128::from(p) - i128::from(f) * i128::from(pr);
assert_eq!(
<i64 as Cell>::pivot_update(&v, &p, &f, &pr, &dv),
i64::try_from(t / i128::from(d)).ok()
);
assert_eq!(
<i64 as Cell>::rescale(&v, &p, &dv),
i64::try_from(i128::from(v) * i128::from(p) / i128::from(d)).ok()
);
}
for &d in &[
1i64,
-1,
2,
-2,
6,
-6,
1 << 40,
-(1 << 40),
i64::MAX,
i64::MIN,
3,
-3,
] {
for &q in &[
0i64,
1,
-1,
i64::MAX,
i64::MIN,
i64::MAX - 1,
i64::MIN + 1,
12345,
] {
check(i128::from(q) * i128::from(d), d);
}
check((i128::from(i64::MAX) + 1) * i128::from(d), d);
check((i128::from(i64::MIN) - 1) * i128::from(d), d);
let dv = Div64::new(d);
assert_eq!(
((d >> dv.shift) as u64).wrapping_mul(dv.inv),
1,
"inverse of the odd part of {d}"
);
}
}
#[test]
fn w256_cells_match_bigint() {
let mut state: u128 = 0x243F_6A88_85A3_08D3_1319_8A2E_0370_7344;
let mut word = || -> u64 {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state as u64
};
let mut rand_big = |bits: u32| -> BigInt {
let mut v = BigInt::from(0);
let mut left = bits;
while left > 0 {
let take = left.min(64);
let w = word() & (u64::MAX >> (64 - take));
v = (v << take) + BigInt::from(w);
left -= take;
}
if word() & 1 == 1 { -v } else { v }
};
let min256: BigInt = -(BigInt::from(1) << 255u32);
let fits256 = |v: &BigInt| v.bits() <= 255 && *v != min256;
let to_opt = |v: &BigInt| -> Option<BigInt> { fits256(v).then(|| v.clone()) };
for round in 0..4000 {
let bits = [60, 120, 127, 128, 200, 250, 254, 255][round % 8];
let (a, b, c, d) = (
rand_big(bits),
rand_big(bits),
rand_big(bits),
rand_big(255 - bits.min(254)),
);
let (wa, wb, wc, wd) = (
W256::from_big(&a).expect("fits"),
W256::from_big(&b).expect("fits"),
W256::from_big(&c).expect("fits"),
W256::from_big(&d).expect("fits"),
);
assert_eq!(wa.to_big(), a);
assert_eq!(<W256 as Cell>::signum(&wa), a.cmp(&BigInt::from(0)));
assert_eq!(wa.cmp(&wb), a.cmp(&b), "{a} vs {b}");
assert_eq!(<W256 as Cell>::neg(&wa).map(|v| v.to_big()), to_opt(&-&a));
assert_eq!(
<W256 as Cell>::mul(&wa, &wb).map(|v| v.to_big()),
to_opt(&(&a * &b)),
"{a} · {b}"
);
assert_eq!(
<W256 as Cell>::cmp_products(&wa, &wb, &wc, &wd),
(&a * &b).cmp(&(&c * &d))
);
assert_eq!(
<W256 as Cell>::sub_mul(&wa, &wb, &wc).map(|v| v.to_big()),
to_opt(&(&a - &b * &c))
);
if !Zero::is_zero(&d) {
let dv = <W256 as Cell>::divisor(&wd);
let k = rand_big(bits.min(250));
let v = &k * &d;
if let Some(wv) = W256::from_big(&v) {
let want = &k * &b;
assert_eq!(
<W256 as Cell>::rescale(&wv, &wb, &dv).map(|q| q.to_big()),
to_opt(&want),
"({v} · {b}) / {d}"
);
let f = &rand_big(20) * &d;
let wf = W256::from_big(&f).expect("fits");
let want2 = &k * &b - (&f / &d) * &c;
assert_eq!(
<W256 as Cell>::pivot_update(&wv, &wb, &wf, &wc, &dv).map(|q| q.to_big()),
to_opt(&want2),
"({v} · {b} − {f} · {c}) / {d}"
);
}
}
}
let two255: BigInt = BigInt::from(1) << 255u32;
assert!(W256::from_big(&two255).is_none());
assert!(W256::from_big(&-&two255).is_none());
let max = &two255 - 1;
let wmax = W256::from_big(&max).expect("fits");
assert_eq!(wmax.to_big(), max);
assert_eq!(W256::from_big(&(-&max)).map(|v| v.to_big()), Some(-&max));
assert_eq!(<W256 as Cell>::neg(&wmax).map(|v| v.to_big()), Some(-&max));
let dv = <W256 as Cell>::divisor(&W256::from_i128(-1));
let wmin =
<W256 as Cell>::pivot_update(&wmax, &W256::ONE, &W256::from_i128(-1), &W256::ONE, &dv)
.expect("(max − (−1)·1) / −1 = −(max + 1) = min");
assert_eq!(wmin.to_big(), -&two255);
assert!(<W256 as Cell>::neg(&wmin).is_none());
assert_eq!(<W256 as Cell>::signum(&wmin), std::cmp::Ordering::Less);
assert_eq!(
<W256 as Cell>::from_ratio_scaled(&q(5, 3), &BigInt::from(6)).map(|v| v.to_big()),
Some(BigInt::from(10))
);
let huge = Ratio::new(&two255 / 2, BigInt::from(3)); assert_eq!(
<W256 as Cell>::from_ratio_scaled(&huge, &BigInt::from(3)).map(|v| v.to_big()),
Some(&two255 / 2)
);
assert!(<W256 as Cell>::from_ratio_scaled(&huge, &BigInt::from(6)).is_none());
}
#[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);
let dv = <i128 as Cell>::divisor(&d0);
assert_eq!(
<i128 as Cell>::rescale(&v, &b2, &dv),
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, &dv),
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, &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, &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, &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, &mut 0),
Err(Halt::Overflow)
));
assert!(matches!(
solve_standard::<i128>(&p, &sf, &mut 0),
Err(Halt::Overflow)
));
let via_w256 = solve_standard::<W256>(&p, &sf, &mut 0).ok().unwrap();
let via_big = solve_standard::<BigInt>(&p, &sf, &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, &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()
);
}
}