use std::fmt;
use num_bigint::BigInt;
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 {
rows: Vec<Vec<Q>>,
obj: Vec<Q>,
basis: Vec<usize>,
m: usize,
n: usize,
pivots_done: usize,
max_pivots: usize,
bland: bool,
}
enum Step {
Optimal,
Unbounded,
}
impl Tableau {
fn new(std: &Standard) -> Self {
let m = std.a.len();
let n = std.c.len();
let width = n + m + 1;
let zero = Q::zero();
let one = Q::one();
let mut rows = Vec::with_capacity(m);
for i in 0..m {
let mut row = Vec::with_capacity(width);
row.extend(std.a[i].iter().cloned());
row.extend((0..m).map(|k| if k == i { one.clone() } else { zero.clone() }));
row.push(std.b[i].clone());
rows.push(row);
}
let basis: Vec<usize> = (0..m).map(|i| n + i).collect();
Tableau {
rows,
obj: vec![zero; width],
basis,
m,
n,
pivots_done: 0,
max_pivots: 10_000 + 50 * (m + n),
bland: false,
}
}
#[inline]
fn rhs_col(&self) -> usize {
self.n + self.m
}
fn set_objective(&mut self, costs: &[Q]) {
let width = self.rhs_col() + 1;
let mut obj = vec![Q::zero(); width];
obj[..costs.len()].clone_from_slice(costs);
for (i, &k) in self.basis.iter().enumerate() {
let f = obj[k].clone();
if f.is_zero() {
continue;
}
let row = &self.rows[i];
for (o, r) in obj.iter_mut().zip(row.iter()) {
if !r.is_zero() {
*o -= &f * r;
}
}
}
self.obj = obj;
}
fn pivot(&mut self, r: usize, s: usize) {
let p = self.rows[r][s].clone();
if !p.is_one() {
for v in self.rows[r].iter_mut() {
if !v.is_zero() {
*v = std::mem::take(v) / &p;
}
}
}
let pivot_row = std::mem::take(&mut self.rows[r]);
for (i, row) in self.rows.iter_mut().enumerate() {
if i == r {
continue;
}
let f = row[s].clone();
if f.is_zero() {
continue;
}
for (v, pr) in row.iter_mut().zip(pivot_row.iter()) {
if !pr.is_zero() {
*v -= &f * pr;
}
}
}
let f = self.obj[s].clone();
if !f.is_zero() {
for (v, pr) in self.obj.iter_mut().zip(pivot_row.iter()) {
if !pr.is_zero() {
*v -= &f * pr;
}
}
}
self.rows[r] = pivot_row;
self.basis[r] = s;
self.pivots_done += 1;
}
fn run(&mut self, limit: usize) -> Result<Step, SymplexError> {
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
),
));
}
let mut entering: Option<usize> = None;
for j in 0..limit {
if !self.obj[j].is_negative() {
continue;
}
match entering {
None => entering = Some(j),
Some(e) if self.obj[j] < self.obj[e] => entering = Some(j),
Some(_) => {}
}
if self.bland {
break;
}
}
let Some(s) = entering else {
return Ok(Step::Optimal);
};
let rhs = self.rhs_col();
let mut leaving: Option<(usize, Q)> = None;
for i in 0..self.m {
let a = &self.rows[i][s];
if !a.is_positive() {
continue;
}
let ratio = &self.rows[i][rhs] / a;
let better = match &leaving {
None => true,
Some((lr, lratio)) => {
ratio < *lratio || (ratio == *lratio && self.basis[i] < self.basis[*lr])
}
};
if better {
leaving = Some((i, ratio));
}
}
let Some((r, ratio)) = leaving else {
return Ok(Step::Unbounded);
};
if ratio.is_zero() {
self.bland = true;
}
self.pivot(r, s);
}
}
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.rows[i][rhs].clone();
}
}
z
}
fn drive_out_artificials(&mut self) {
for r in 0..self.m {
if self.basis[r] < self.n {
continue;
}
if let Some(s) = (0..self.n).find(|&j| !self.rows[r][j].is_zero()) {
self.pivot(r, s);
}
}
}
fn duals(&self, art_cost: &Q) -> Vec<Q> {
(0..self.m)
.map(|i| art_cost - &self.obj[self.n + i])
.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)),
};
let mut t = Tableau::new(&sf);
let m = t.m;
let n = t.n;
let mut phase1 = vec![Q::zero(); n + m];
for v in phase1[n..].iter_mut() {
*v = Q::one();
}
t.set_objective(&phase1);
match t.run(n + m)? {
Step::Optimal => {}
Step::Unbounded => {
return Err(failed("linprog", "phase 1 reported unbounded"));
}
}
let infeasibility = -t.obj[t.rhs_col()].clone();
if infeasibility.is_positive() {
let y_std = t.duals(&Q::one());
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);
match t.run(n)? {
Step::Optimal => {}
Step::Unbounded => return Ok(LpSolution::unbounded()),
}
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(&Q::zero());
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 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()
);
}
}