#![deny(missing_debug_implementations, missing_docs)]
#[macro_use]
extern crate log;
mod helpers;
mod lu;
mod mip;
mod ordering;
#[cfg(test)]
mod problems_solvers;
mod solver;
mod sparse;
mod tests;
use solver::Solver;
use sprs::errors::StructureError;
use core::time::Duration;
use web_time::Instant;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum OptimizationDirection {
Minimize,
Maximize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Variable(pub(crate) usize);
impl Variable {
pub fn idx(&self) -> usize {
self.0
}
}
#[derive(Clone, Debug)]
pub struct LinearExpr {
vars: Vec<usize>,
coeffs: Vec<f64>,
}
impl LinearExpr {
pub fn empty() -> Self {
Self {
vars: vec![],
coeffs: vec![],
}
}
pub fn add(&mut self, var: Variable, coeff: f64) {
self.vars.push(var.0);
self.coeffs.push(coeff);
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug)]
pub struct LinearTerm(Variable, f64);
impl From<(Variable, f64)> for LinearTerm {
fn from(term: (Variable, f64)) -> Self {
LinearTerm(term.0, term.1)
}
}
impl<'a> From<&'a (Variable, f64)> for LinearTerm {
fn from(term: &'a (Variable, f64)) -> Self {
LinearTerm(term.0, term.1)
}
}
impl<I: IntoIterator<Item = impl Into<LinearTerm>>> From<I> for LinearExpr {
fn from(iter: I) -> Self {
let mut expr = LinearExpr::empty();
for term in iter {
let LinearTerm(var, coeff) = term.into();
expr.add(var, coeff);
}
expr
}
}
impl std::iter::FromIterator<(Variable, f64)> for LinearExpr {
fn from_iter<I: IntoIterator<Item = (Variable, f64)>>(iter: I) -> Self {
let mut expr = LinearExpr::empty();
for term in iter {
expr.add(term.0, term.1)
}
expr
}
}
impl std::iter::Extend<(Variable, f64)> for LinearExpr {
fn extend<I: IntoIterator<Item = (Variable, f64)>>(&mut self, iter: I) {
for term in iter {
self.add(term.0, term.1)
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum ComparisonOp {
Eq,
Le,
Ge,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Error {
Infeasible,
Unbounded,
InvalidOptions(String),
InvalidOperation(String),
InternalError(String),
}
impl From<StructureError> for Error {
fn from(err: StructureError) -> Self {
Error::InternalError(err.to_string())
}
}
impl From<sparse::Error> for Error {
fn from(value: sparse::Error) -> Self {
Error::InternalError(value.to_string())
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let msg = match self {
Error::Infeasible => "problem is infeasible",
Error::Unbounded => "problem is unbounded",
Error::InvalidOptions(msg)
| Error::InvalidOperation(msg)
| Error::InternalError(msg) => msg,
};
msg.fmt(f)
}
}
impl std::error::Error for Error {}
#[derive(Clone)]
pub struct Problem {
direction: OptimizationDirection,
obj_coeffs: Vec<f64>,
var_mins: Vec<f64>,
var_maxs: Vec<f64>,
var_domains: Vec<VarDomain>,
constraints: Vec<(CsVec, ComparisonOp, f64)>,
time_limit: Option<Duration>,
}
impl std::fmt::Debug for Problem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Problem")
.field("direction", &self.direction)
.field("num_vars", &self.obj_coeffs.len())
.field("num_constraints", &self.constraints.len())
.finish()
}
}
type CsVec = sprs::CsVecI<f64, usize>;
#[derive(Clone, Debug, PartialEq)]
pub enum VarDomain {
Integer,
Real,
Boolean,
}
impl Problem {
pub fn new(direction: OptimizationDirection) -> Self {
Problem {
direction,
obj_coeffs: vec![],
var_mins: vec![],
var_maxs: vec![],
var_domains: vec![],
constraints: vec![],
time_limit: None,
}
}
pub fn set_time_limit(&mut self, duration: Duration) {
self.time_limit = Some(duration);
}
pub fn add_var(&mut self, obj_coeff: f64, (min, max): (f64, f64)) -> Variable {
self.internal_add_var(obj_coeff, (min, max), VarDomain::Real)
}
pub fn add_integer_var(&mut self, obj_coeff: f64, (min, max): (i32, i32)) -> Variable {
self.internal_add_var(obj_coeff, (min as f64, max as f64), VarDomain::Integer)
}
pub fn has_integer_vars(&self) -> bool {
self.var_domains
.iter()
.any(|v| *v == VarDomain::Integer || *v == VarDomain::Boolean)
}
pub fn add_binary_var(&mut self, obj_coeff: f64) -> Variable {
self.internal_add_var(obj_coeff, (0.0, 1.0), VarDomain::Boolean)
}
pub(crate) fn internal_add_var(
&mut self,
obj_coeff: f64,
(min, max): (f64, f64),
var_type: VarDomain,
) -> Variable {
let var = Variable(self.obj_coeffs.len());
let obj_coeff = match self.direction {
OptimizationDirection::Minimize => obj_coeff,
OptimizationDirection::Maximize => -obj_coeff,
};
self.obj_coeffs.push(obj_coeff);
self.var_mins.push(min);
self.var_maxs.push(max);
self.var_domains.push(var_type);
var
}
pub fn add_constraint(&mut self, expr: impl Into<LinearExpr>, cmp_op: ComparisonOp, rhs: f64) {
let expr = expr.into();
self.constraints.push((
CsVec::new_from_unsorted(self.obj_coeffs.len(), expr.vars, expr.coeffs).unwrap(),
cmp_op,
rhs,
));
}
}
pub use mip::{SolveOptions, Stats, Status, Tolerances};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum StopReason {
Limit,
Finished,
}
fn status_after_stop(stop: StopReason, finished: Status) -> Status {
match stop {
StopReason::Finished => finished,
StopReason::Limit => Status::Interrupted,
}
}
fn timed_lp_call<T>(
solver: &mut Solver,
time_limit: Option<Duration>,
call: impl FnOnce(&mut Solver) -> Result<T, Error>,
) -> Result<T, Error> {
let started = Instant::now();
solver.deadline = time_limit.map(|duration| started + duration);
let result = call(solver);
solver.elapsed += started.elapsed();
result
}
fn ensure_lp_editable(status: Status) -> Result<(), Error> {
if status == Status::Interrupted {
Err(Error::InvalidOperation(
"cannot edit an interrupted solution; resume() it first".to_string(),
))
} else {
Ok(())
}
}
impl Problem {
pub(crate) fn build_solver(&self, deadline: solver::Deadline) -> Result<Solver, Error> {
Solver::try_new(
&self.obj_coeffs,
&self.var_mins,
&self.var_maxs,
&self.constraints,
&self.var_domains,
deadline,
)
}
pub fn solve(&self) -> Result<Solution, Error> {
let options = SolveOptions {
time_limit: self.time_limit,
..SolveOptions::default()
};
self.solve_with(options)
}
pub fn solve_with(&self, options: SolveOptions) -> Result<Solution, Error> {
options.validate()?;
let num_vars = self.obj_coeffs.len();
if self.has_integer_vars() {
let run = mip::run(self, options)?;
Ok(Solution::from_mip_run(self.direction, num_vars, run))
} else {
let started = Instant::now();
let deadline = options.time_limit.map(|d| started + d);
let mut solver = self.build_solver(deadline)?;
solver.operation_time_limit = options.time_limit;
let solve_result = solver.initial_solve();
solver.elapsed += started.elapsed();
let status = status_after_stop(solve_result?, Status::Optimal);
Ok(Solution::from_lp(
self.direction,
num_vars,
status,
Box::new(solver),
))
}
}
}
#[derive(Clone)]
pub struct Solution {
direction: OptimizationDirection,
num_vars: usize,
status: Status,
kind: SolutionKind,
}
#[derive(Clone)]
enum SolutionKind {
Lp(Box<solver::Solver>),
Mip(Box<mip::MipState>),
}
impl std::fmt::Debug for Solution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Solution")
.field("direction", &self.direction)
.field("num_vars", &self.num_vars)
.field("status", &self.status)
.field("objective", &self.objective())
.finish()
}
}
impl Solution {
fn from_lp(
direction: OptimizationDirection,
num_vars: usize,
status: Status,
solver: Box<Solver>,
) -> Self {
Self {
direction,
num_vars,
status,
kind: SolutionKind::Lp(solver),
}
}
fn from_mip_run(direction: OptimizationDirection, num_vars: usize, run: mip::MipRun) -> Self {
Self {
direction,
num_vars,
status: mip::status_of(run.outcome, &run.state),
kind: SolutionKind::Mip(Box::new(run.state)),
}
}
pub fn status(&self) -> Status {
self.status
}
pub fn objective(&self) -> f64 {
let internal = match &self.kind {
SolutionKind::Lp(solver) => solver.cur_obj_val,
SolutionKind::Mip(state) => state.current_objective(),
};
match self.direction {
OptimizationDirection::Minimize => internal,
OptimizationDirection::Maximize => -internal,
}
}
pub fn var_value_raw(&self, var: Variable) -> f64 {
assert!(var.0 < self.num_vars);
match &self.kind {
SolutionKind::Lp(solver) => *solver.get_value(var.0),
SolutionKind::Mip(state) => match &state.incumbent {
Some(incumbent) => incumbent.values[var.0],
None => *state.solver.get_value(var.0),
},
}
}
pub fn var_value(&self, var: Variable) -> f64 {
let val = self.var_value_raw(var);
if self.status == Status::Interrupted {
return val;
}
let domain = match &self.kind {
SolutionKind::Lp(solver) => &solver.orig_var_domains[var.0],
SolutionKind::Mip(state) => &state.solver.orig_var_domains[var.0],
};
if *domain == VarDomain::Integer || *domain == VarDomain::Boolean {
let rounded = val.round();
let tol = Tolerances::default().integrality_rounding;
assert!(
f64::abs(rounded - val) < tol,
"Variable was expected to be an integer, got {}",
val
);
rounded
} else {
val
}
}
pub fn gap(&self) -> Option<f64> {
match &self.kind {
SolutionKind::Lp(_) => (self.status == Status::Optimal).then_some(0.0),
SolutionKind::Mip(state) => state.stats.gap,
}
}
pub fn stats(&self) -> Stats {
match &self.kind {
SolutionKind::Lp(solver) => Stats {
lp_iterations: solver.lp_iterations,
elapsed: solver.elapsed,
best_bound: (self.status == Status::Optimal).then(|| self.objective()),
gap: (self.status == Status::Optimal).then_some(0.0),
..Stats::default()
},
SolutionKind::Mip(state) => state.stats,
}
}
pub fn iter(&self) -> SolutionIter<'_> {
SolutionIter {
solution: self,
var_idx: 0,
}
}
pub fn resume(mut self, time_limit: Option<Duration>) -> Result<Self, Error> {
if self.status == Status::Optimal {
return Ok(self);
}
match &mut self.kind {
SolutionKind::Lp(solver) => {
let stop = timed_lp_call(solver, time_limit, Solver::initial_solve)?;
self.status = status_after_stop(stop, Status::Optimal);
}
SolutionKind::Mip(state) => {
let outcome = mip::resume_run(state, time_limit)?;
self.status = mip::status_of(outcome, state);
}
}
Ok(self)
}
pub fn add_constraint(
self,
expr: impl Into<LinearExpr>,
cmp_op: ComparisonOp,
rhs: f64,
) -> Result<Self, Error> {
let Solution {
direction,
num_vars,
status,
kind,
} = self;
match kind {
SolutionKind::Lp(mut solver) => {
ensure_lp_editable(status)?;
let expr = expr.into();
let time_limit = solver.operation_time_limit;
let stop = timed_lp_call(&mut solver, time_limit, move |solver| {
solver.add_constraint(
CsVec::new_from_unsorted(num_vars, expr.vars, expr.coeffs)
.map_err(|error| Error::InternalError(error.2.to_string()))?,
cmp_op,
rhs,
)
})?;
Ok(Self::from_lp(
direction,
num_vars,
status_after_stop(stop, Status::Optimal),
solver,
))
}
SolutionKind::Mip(mut state) => {
let expr = expr.into();
let coeffs = CsVec::new_from_unsorted(num_vars, expr.vars, expr.coeffs)
.map_err(|v| Error::InternalError(v.2.to_string()))?;
state.base.constraints.push((coeffs, cmp_op, rhs));
let run = mip::reedit_and_resolve(state)?;
Ok(Self::from_mip_run(direction, num_vars, run))
}
}
}
pub fn fix_var(self, var: Variable, val: f64) -> Result<Self, Error> {
let Solution {
direction,
num_vars,
status,
kind,
} = self;
assert!(var.0 < num_vars);
if !val.is_finite() {
return Err(Error::Infeasible);
}
match kind {
SolutionKind::Lp(mut solver) => {
ensure_lp_editable(status)?;
let time_limit = solver.operation_time_limit;
let stop =
timed_lp_call(&mut solver, time_limit, |solver| solver.fix_var(var.0, val))?;
Ok(Self::from_lp(
direction,
num_vars,
status_after_stop(stop, Status::Optimal),
solver,
))
}
SolutionKind::Mip(mut state) => {
if val < state.base.var_mins[var.0] || val > state.base.var_maxs[var.0] {
return Err(Error::Infeasible);
}
state.fixed.insert(var.0, val);
let run = mip::reedit_and_resolve(state)?;
Ok(Self::from_mip_run(direction, num_vars, run))
}
}
}
pub fn unfix_var(self, var: Variable) -> Result<(Self, bool), Error> {
let Solution {
direction,
num_vars,
status,
kind,
} = self;
assert!(var.0 < num_vars);
match kind {
SolutionKind::Lp(mut solver) => {
ensure_lp_editable(status)?;
let time_limit = solver.operation_time_limit;
let (res, stop) =
timed_lp_call(&mut solver, time_limit, |solver| solver.unfix_var(var.0))?;
Ok((
Self::from_lp(direction, num_vars, status_after_stop(stop, status), solver),
res,
))
}
SolutionKind::Mip(mut state) => {
if state.fixed.remove(&var.0).is_none() {
return Ok((
Solution {
direction,
num_vars,
status,
kind: SolutionKind::Mip(state),
},
false,
));
}
let run = mip::reedit_and_resolve(state)?;
Ok((Self::from_mip_run(direction, num_vars, run), true))
}
}
}
}
impl std::ops::Index<Variable> for Solution {
type Output = f64;
fn index(&self, var: Variable) -> &Self::Output {
assert!(var.0 < self.num_vars);
match &self.kind {
SolutionKind::Lp(solver) => solver.get_value(var.0),
SolutionKind::Mip(state) => match &state.incumbent {
Some(incumbent) => &incumbent.values[var.0],
None => state.solver.get_value(var.0),
},
}
}
}
#[derive(Debug, Clone)]
pub struct SolutionIter<'a> {
solution: &'a Solution,
var_idx: usize,
}
impl<'a> Iterator for SolutionIter<'a> {
type Item = (Variable, f64);
fn next(&mut self) -> Option<Self::Item> {
if self.var_idx < self.solution.num_vars {
let var = Variable(self.var_idx);
self.var_idx += 1;
Some((var, self.solution.var_value(var)))
} else {
None
}
}
}
impl<'a> IntoIterator for &'a Solution {
type Item = (Variable, f64);
type IntoIter = SolutionIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}