#![deny(missing_debug_implementations, missing_docs)]
#[macro_use]
extern crate log;
mod helpers;
mod lu;
mod mps;
mod ordering;
pub 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,
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::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 fn solve(&self) -> Result<Solution, Error> {
let deadline = self.time_limit.map(|d| Instant::now() + d);
let mut solver = Solver::try_new(
&self.obj_coeffs,
&self.var_mins,
&self.var_maxs,
&self.constraints,
&self.var_domains,
deadline,
)?;
let mut stop_reason = solver.initial_solve()?;
if stop_reason == StopReason::Finished && self.has_integer_vars() {
let non_integer_solution = Solution {
num_vars: self.obj_coeffs.len(),
direction: self.direction,
solver: solver.clone(),
stop_reason,
};
stop_reason = solver.solve_integer(non_integer_solution, self.direction)?;
let solution = Solution {
num_vars: self.obj_coeffs.len(),
direction: self.direction,
solver,
stop_reason,
};
Ok(solution)
} else {
Ok(Solution {
num_vars: self.obj_coeffs.len(),
direction: self.direction,
solver,
stop_reason,
})
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StopReason {
Limit,
Finished,
}
#[derive(Clone)]
pub struct Solution {
direction: OptimizationDirection,
num_vars: usize,
solver: solver::Solver,
stop_reason: StopReason,
}
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("num_constraints", &self.solver.num_constraints())
.field("objective", &self.objective())
.finish()
}
}
impl Solution {
pub fn stop_reason(&self) -> &StopReason {
&self.stop_reason
}
pub fn objective(&self) -> f64 {
match self.direction {
OptimizationDirection::Minimize => self.solver.cur_obj_val,
OptimizationDirection::Maximize => -self.solver.cur_obj_val,
}
}
pub fn resume(mut self, time_limit: Option<Duration>) -> Result<Self, Error> {
if self.stop_reason == StopReason::Finished {
return Ok(self);
}
self.solver.deadline = time_limit.map(|d| Instant::now() + d);
let has_bb_state = self.solver.bb_state.is_some();
let stop_reason = if has_bb_state {
let bb_state = self.solver.bb_state.take();
let cur_solution = Solution {
num_vars: self.num_vars,
direction: self.direction,
solver: self.solver.clone(),
stop_reason: StopReason::Finished,
};
self.solver.bb_state = bb_state;
self.solver.solve_integer(cur_solution, self.direction)?
} else {
let mut sr = self.solver.initial_solve()?;
if sr == StopReason::Finished && self.solver.has_integer_vars() {
let cur_solution = Solution {
num_vars: self.num_vars,
direction: self.direction,
solver: self.solver.clone(),
stop_reason: StopReason::Finished,
};
sr = self.solver.solve_integer(cur_solution, self.direction)?;
}
sr
};
self.stop_reason = stop_reason;
Ok(self)
}
pub fn var_value_raw(&self, var: Variable) -> &f64 {
assert!(var.0 < self.num_vars);
self.solver.get_value(var.0)
}
pub fn var_value(&self, var: Variable) -> f64 {
let val = self.var_value_raw(var);
let domain = &self.solver.orig_var_domains[var.0];
if *domain == VarDomain::Integer || *domain == VarDomain::Boolean {
let rounded = val.round();
assert!(
f64::abs(rounded - val) < 1e-5,
"Variable was expected to be an integer, got {}",
val
);
rounded
} else {
*val
}
}
pub fn iter(&self) -> SolutionIter<'_> {
SolutionIter {
solution: self,
var_idx: 0,
}
}
pub fn add_constraint(
mut self,
expr: impl Into<LinearExpr>,
cmp_op: ComparisonOp,
rhs: f64,
) -> Result<Self, Error> {
let expr = expr.into();
let stop_reason = self.solver.add_constraint(
CsVec::new_from_unsorted(self.num_vars, expr.vars, expr.coeffs)
.map_err(|v| Error::InternalError(v.2.to_string()))?,
cmp_op,
rhs,
)?;
self.stop_reason = stop_reason;
Ok(self)
}
pub fn fix_var(mut self, var: Variable, val: f64) -> Result<Self, Error> {
assert!(var.0 < self.num_vars);
let stop_reason = self.solver.fix_var(var.0, val)?;
self.stop_reason = stop_reason;
Ok(self)
}
pub fn unfix_var(mut self, var: Variable) -> (Self, bool) {
assert!(var.0 < self.num_vars);
let res = self.solver.unfix_var(var.0);
(self, res)
}
pub fn add_gomory_cut(mut self, var: Variable) -> Result<Self, Error> {
assert!(var.0 < self.num_vars);
let stop_reason = self.solver.add_gomory_cut(var.0)?;
self.stop_reason = stop_reason;
Ok(self)
}
}
impl std::ops::Index<Variable> for Solution {
type Output = f64;
fn index(&self, var: Variable) -> &Self::Output {
self.var_value_raw(var)
}
}
#[derive(Debug, Clone)]
pub struct SolutionIter<'a> {
solution: &'a Solution,
var_idx: usize,
}
impl<'a> Iterator for SolutionIter<'a> {
type Item = (Variable, &'a f64);
fn next(&mut self) -> Option<Self::Item> {
if self.var_idx < self.solution.num_vars {
let var_idx = self.var_idx;
self.var_idx += 1;
Some((Variable(var_idx), self.solution.solver.get_value(var_idx)))
} else {
None
}
}
}
impl<'a> IntoIterator for &'a Solution {
type Item = (Variable, &'a f64);
type IntoIter = SolutionIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
pub use mps::MpsFile;