pub mod constants;
mod constraints;
mod environment;
pub mod errors;
pub mod parameters;
mod solution;
mod variables;
pub use constraints::*;
pub use environment::*;
pub use errors::{Error, Result};
pub use ffi;
use ffi::{
cpxlp, CPX_STAT_INForUNBD, CPXaddmipstarts, CPXaddrows, CPXchgobj, CPXchgobjsen,
CPXchgprobtype, CPXcreateprob, CPXfreeprob, CPXgetobjval, CPXgetstat, CPXgetx, CPXlpopt,
CPXmipopt, CPXnewcols, CPXwriteprob, CPXMIP_UNBOUNDED, CPXPROB_LP, CPXPROB_MILP, CPX_MAX,
CPX_MIN, CPX_STAT_INFEASIBLE, CPX_STAT_UNBOUNDED,
};
use log::info;
pub use solution::*;
pub use variables::*;
use std::{
ffi::{c_int, CString},
time::Instant,
};
mod macros {
macro_rules! cpx_lp_result {
( unsafe { $func:ident ( $env:expr, $lp:expr $(, $b:expr)* $(,)? ) } ) => {
{
let status = unsafe { $func($env, $lp $(,$b)* ) };
if status != 0 {
Err(errors::Error::from(errors::Cplex::from_code($env, $lp, status)))
} else {
Ok(())
}
}
};
}
pub(super) use cpx_lp_result;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct VariableId(usize);
impl VariableId {
pub fn into_inner(self) -> usize {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ConstraintId(usize);
impl ConstraintId {
pub fn into_inner(self) -> usize {
self.0
}
}
pub struct Problem {
inner: *mut cpxlp,
env: Environment,
variables: Vec<Variable>,
constraints: Vec<Constraint>,
}
#[derive(Copy, Clone, Debug)]
pub enum ObjectiveType {
Maximize,
Minimize,
}
impl ObjectiveType {
fn into_raw(self) -> c_int {
match self {
ObjectiveType::Minimize => CPX_MIN as c_int,
ObjectiveType::Maximize => CPX_MAX as c_int,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ProblemType {
Linear,
MixedInteger,
}
impl ProblemType {
fn into_raw(self) -> c_int {
match self {
ProblemType::Linear => CPXPROB_LP as c_int,
ProblemType::MixedInteger => CPXPROB_MILP as c_int,
}
}
}
impl Problem {
pub fn new<S>(env: Environment, name: S) -> Result<Self>
where
S: AsRef<str>,
{
let mut status = 0;
let name =
CString::new(name.as_ref()).map_err(|e| errors::Input::from_message(e.to_string()))?;
let inner = unsafe { CPXcreateprob(env.0, &mut status, name.as_ptr()) };
if inner.is_null() {
Err(errors::Cplex::from_code(env.0, std::ptr::null(), status).into())
} else {
Ok(Problem {
inner,
env,
variables: vec![],
constraints: vec![],
})
}
}
pub fn add_variable(&mut self, var: Variable) -> Result<VariableId> {
let name = CString::new(var.name().as_bytes())
.map_err(|e| errors::Input::from_message(e.to_string()))?;
macros::cpx_lp_result!(unsafe {
CPXnewcols(
self.env.0,
self.inner,
1,
&var.weight(),
&var.lower_bound(),
&var.upper_bound(),
&var.type_().into_raw() as *const u8 as *const i8,
&mut (name.as_ptr() as *mut _),
)
})?;
let index = self.variables.len();
self.variables.push(var);
Ok(VariableId(index))
}
pub fn add_variables(&mut self, vars: Vec<Variable>) -> Result<Vec<VariableId>> {
let names = vars
.iter()
.map(|v| {
CString::new(v.name().as_bytes())
.map_err(|e| errors::Input::from_message(e.to_string()).into())
})
.collect::<Result<Vec<_>>>()?;
let mut name_ptrs = names
.iter()
.map(|n| n.as_ptr() as *mut _)
.collect::<Vec<_>>();
let objs = vars.iter().map(|v| v.weight()).collect::<Vec<_>>();
let lbs = vars.iter().map(|v| v.lower_bound()).collect::<Vec<_>>();
let ubs = vars.iter().map(|v| v.upper_bound()).collect::<Vec<_>>();
let types = vars
.iter()
.map(|v| v.type_().into_raw() as i8)
.collect::<Vec<_>>();
macros::cpx_lp_result!(unsafe {
CPXnewcols(
self.env.0,
self.inner,
vars.len() as i32,
objs.as_ptr(),
lbs.as_ptr(),
ubs.as_ptr(),
types.as_ptr(),
name_ptrs.as_mut_ptr(),
)
})?;
let indices: Vec<VariableId> = vars
.iter()
.enumerate()
.map(|(idx, _)| VariableId(idx + self.variables.len()))
.collect();
self.variables.extend(vars);
Ok(indices)
}
pub fn add_constraint(&mut self, constraint: Constraint) -> Result<ConstraintId> {
let (ind, val): (Vec<c_int>, Vec<f64>) = constraint
.weights()
.iter()
.filter(|(_, weight)| *weight != 0.0)
.map(|(var_id, weight)| (var_id.0 as c_int, weight))
.unzip();
let nz = val.len() as c_int;
let name = constraint
.name()
.map(|n| {
CString::new(n.as_bytes()).map_err(|e| errors::Input::from_message(e.to_string()))
})
.transpose()?;
macros::cpx_lp_result!(unsafe {
CPXaddrows(
self.env.0,
self.inner,
0,
1,
nz,
&constraint.rhs(),
&constraint.type_().into_raw(),
&0,
ind.as_ptr(),
val.as_ptr(),
std::ptr::null_mut(),
&mut (name
.as_ref()
.map(|n| n.as_ptr())
.unwrap_or(std::ptr::null()) as *mut _),
)
})?;
let index = self.constraints.len();
self.constraints.push(constraint);
Ok(ConstraintId(index))
}
pub fn add_constraints(&mut self, con: Vec<Constraint>) -> Result<Vec<ConstraintId>> {
if con.is_empty() {
return Err(errors::Input::from_message(
"Called add_constraints with 0 constaints".to_owned(),
)
.into());
}
let beg = std::iter::once(0)
.chain(con[..con.len() - 1].iter().map(|c| c.weights().len()))
.scan(0, |state, x| {
*state += x;
Some(*state as i32)
})
.collect::<Vec<_>>();
let (ind, val): (Vec<c_int>, Vec<f64>) = con
.iter()
.flat_map(|c| c.weights().iter())
.filter(|(_, weight)| *weight != 0.0)
.map(|(var_id, weight)| (var_id.0 as c_int, weight))
.unzip();
let nz = val.len() as c_int;
let names = con
.iter()
.map(|c| {
c.name()
.map(|n| {
CString::new(n.as_bytes())
.map_err(|e| errors::Input::from_message(e.to_string()).into())
})
.transpose()
})
.collect::<Result<Vec<_>>>()?;
let mut name_ptrs = names
.iter()
.map(|n| {
n.as_ref()
.map(|n| n.as_ptr())
.unwrap_or(std::ptr::null_mut()) as *mut _
})
.collect::<Vec<_>>();
let rhss = con.iter().map(|c| c.rhs()).collect::<Vec<_>>();
let senses = con.iter().map(|c| c.type_().into_raw()).collect::<Vec<_>>();
macros::cpx_lp_result!(unsafe {
CPXaddrows(
self.env.0,
self.inner,
0,
con.len() as i32,
nz,
rhss.as_ptr(),
senses.as_ptr(),
beg.as_ptr(),
ind.as_ptr(),
val.as_ptr(),
std::ptr::null_mut(),
name_ptrs.as_mut_ptr(),
)
})?;
let indices = con
.iter()
.enumerate()
.map(|(idx, _)| ConstraintId(idx + self.constraints.len()))
.collect();
self.constraints.extend(con);
Ok(indices)
}
pub fn set_objective(self, ty: ObjectiveType, obj: Vec<(VariableId, f64)>) -> Result<Self> {
let (ind, val): (Vec<c_int>, Vec<f64>) = obj
.into_iter()
.map(|(var_id, weight)| (var_id.0 as c_int, weight))
.unzip();
macros::cpx_lp_result!(unsafe {
CPXchgobj(
self.env.0,
self.inner,
ind.len() as c_int,
ind.as_ptr(),
val.as_ptr(),
)
})?;
self.set_objective_type(ty)
}
pub fn set_objective_type(self, ty: ObjectiveType) -> Result<Self> {
macros::cpx_lp_result!(unsafe { CPXchgobjsen(self.env.0, self.inner, ty.into_raw()) })?;
Ok(self)
}
pub fn write<S>(&self, name: S) -> Result<()>
where
S: AsRef<str>,
{
let name =
CString::new(name.as_ref()).map_err(|e| errors::Input::from_message(e.to_string()))?;
macros::cpx_lp_result!(unsafe {
CPXwriteprob(self.env.0, self.inner, name.as_ptr(), std::ptr::null())
})
}
pub fn add_initial_soln(&mut self, vars: &[VariableId], values: &[f64]) -> Result<()> {
if values.len() != vars.len() {
return Err(errors::Input::from_message(
"number of solution variables and values does not match".to_string(),
)
.into());
}
let vars = vars.iter().map(|&u| u.0 as c_int).collect::<Vec<_>>();
macros::cpx_lp_result!(unsafe {
CPXaddmipstarts(
self.env.0,
self.inner,
1,
vars.len() as c_int,
&0,
vars.as_ptr(),
values.as_ptr(),
&0,
&mut std::ptr::null_mut(),
)
})
}
pub fn solve_as(self, pt: ProblemType) -> Result<Solution> {
macros::cpx_lp_result!(unsafe { CPXchgprobtype(self.env.0, self.inner, pt.into_raw()) })?;
let start_optim = Instant::now();
match pt {
ProblemType::MixedInteger => {
macros::cpx_lp_result!(unsafe { CPXmipopt(self.env.0, self.inner) })?
}
ProblemType::Linear => {
macros::cpx_lp_result!(unsafe { CPXlpopt(self.env.0, self.inner) })?
}
};
let elapsed = start_optim.elapsed();
info!("CPLEX model solution took: {:?}", elapsed);
let code = unsafe { CPXgetstat(self.env.0, self.inner) };
if code as u32 == CPX_STAT_INFEASIBLE || code as u32 == CPX_STAT_INForUNBD {
return Err(crate::errors::Cplex::Unfeasible {
code,
message: "Unfeasible problem".to_string(),
}
.into());
}
if code as u32 == CPX_STAT_UNBOUNDED || code as u32 == CPXMIP_UNBOUNDED {
return Err(crate::errors::Cplex::Unbounded {
code,
message: "Unbounded problem".to_string(),
}
.into());
}
let mut objective_value: f64 = 0.0;
macros::cpx_lp_result!(unsafe {
CPXgetobjval(self.env.0, self.inner, &mut objective_value)
})?;
let mut variable_values = vec![0f64; self.variables.len()];
macros::cpx_lp_result!(unsafe {
CPXgetx(
self.env.0,
self.inner,
variable_values.as_mut_ptr(),
0,
self.variables.len() as c_int - 1,
)
})?;
Ok(Solution::new(variable_values, objective_value))
}
}
impl Drop for Problem {
fn drop(&mut self) {
unsafe {
assert_eq!(CPXfreeprob(self.env.0, &mut self.inner), 0);
}
}
}
#[cfg(test)]
mod test {
use constants::INFINITY;
use constraints::ConstraintType;
use super::*;
use variables::{Variable, VariableType};
#[test]
fn mipex1() {
let env = Environment::new().unwrap();
let mut problem = Problem::new(env, "mipex1").unwrap();
let x0 = problem
.add_variable(Variable::new(
VariableType::Continuous,
1.0,
0.0,
40.0,
"x0",
))
.unwrap();
let x1 = problem
.add_variable(Variable::new(
VariableType::Continuous,
2.0,
0.0,
INFINITY,
"x1",
))
.unwrap();
let x2 = problem
.add_variable(Variable::new(
VariableType::Continuous,
3.0,
0.0,
INFINITY,
"x2",
))
.unwrap();
let x3 = problem
.add_variable(Variable::new(VariableType::Integer, 1.0, 2.0, 3.0, "x3"))
.unwrap();
assert_eq!(x0, VariableId(0));
assert_eq!(x1, VariableId(1));
assert_eq!(x2, VariableId(2));
assert_eq!(x3, VariableId(3));
let c0 = problem
.add_constraint(Constraint::new(
ConstraintType::LessThanEq,
20.0,
None,
vec![(x0, -1.0), (x1, 1.0), (x2, 1.0), (x3, 10.0)],
))
.unwrap();
let c1 = problem
.add_constraint(Constraint::new(
ConstraintType::LessThanEq,
30.0,
None,
vec![(x0, 1.0), (x1, -3.0), (x2, 1.0)],
))
.unwrap();
let c2 = problem
.add_constraint(Constraint::new(
ConstraintType::Eq,
0.0,
None,
vec![(x1, 1.0), (x3, -3.5)],
))
.unwrap();
assert_eq!(c0, ConstraintId(0));
assert_eq!(c1, ConstraintId(1));
assert_eq!(c2, ConstraintId(2));
let problem = problem.set_objective_type(ObjectiveType::Maximize).unwrap();
let solution = problem.solve_as(ProblemType::MixedInteger).unwrap();
assert_eq!(solution.objective_value(), 122.5);
}
#[test]
fn mipex1_batch() {
let env = Environment::new().unwrap();
let mut problem = Problem::new(env, "mipex1").unwrap();
let vars = problem
.add_variables(vec![
Variable::new(VariableType::Continuous, 1.0, 0.0, 40.0, "x0"),
Variable::new(VariableType::Continuous, 2.0, 0.0, INFINITY, "x1"),
Variable::new(VariableType::Continuous, 3.0, 0.0, INFINITY, "x2"),
Variable::new(VariableType::Integer, 1.0, 2.0, 3.0, "x3"),
])
.unwrap();
assert_eq!(
vars,
vec![VariableId(0), VariableId(1), VariableId(2), VariableId(3)]
);
let cons = problem
.add_constraints(vec![
Constraint::new(
ConstraintType::LessThanEq,
20.0,
None,
vec![
(vars[0], -1.0),
(vars[1], 1.0),
(vars[2], 1.0),
(vars[3], 10.0),
],
),
Constraint::new(
ConstraintType::LessThanEq,
30.0,
None,
vec![(vars[0], 1.0), (vars[1], -3.0), (vars[2], 1.0)],
),
Constraint::new(
ConstraintType::Eq,
0.0,
None,
vec![(vars[1], 1.0), (vars[3], -3.5)],
),
])
.unwrap();
assert_eq!(
cons,
vec![ConstraintId(0), ConstraintId(1), ConstraintId(2)]
);
let problem = problem.set_objective_type(ObjectiveType::Maximize).unwrap();
let solution = problem.solve_as(ProblemType::MixedInteger).unwrap();
assert_eq!(solution.objective_value(), 122.5);
}
#[test]
fn unfeasible() {
let env = Environment::new().unwrap();
let mut problem = Problem::new(env, "unfeasible").unwrap();
let vars = problem
.add_variables(vec![
Variable::new(VariableType::Continuous, 1.0, 0.0, 1.0, "x0"),
Variable::new(VariableType::Continuous, 1.0, 0.0, 1.0, "x1"),
])
.unwrap();
assert_eq!(vars, vec![VariableId(0), VariableId(1)]);
let cons = problem
.add_constraints(vec![
Constraint::new(
ConstraintType::Eq,
0.0,
None,
vec![(vars[0], 1.0), (vars[1], 1.0)],
),
Constraint::new(
ConstraintType::Eq,
1.0,
None,
vec![(vars[0], 1.0), (vars[1], 1.0)],
),
])
.unwrap();
assert_eq!(cons, vec![ConstraintId(0), ConstraintId(1)]);
let problem = problem.set_objective_type(ObjectiveType::Maximize).unwrap();
assert!(matches!(
problem.solve_as(ProblemType::Linear),
Err(errors::Error::Cplex(errors::Cplex::Unfeasible { .. }))
));
}
#[test]
fn unbounded() {
let env = Environment::new().unwrap();
let mut problem = Problem::new(env, "unbounded").unwrap();
problem
.add_variable(Variable::new(
VariableType::Integer,
1.0,
0.0,
INFINITY,
"x0",
))
.unwrap();
let problem = problem.set_objective_type(ObjectiveType::Maximize).unwrap();
assert!(matches!(
problem.solve_as(ProblemType::MixedInteger),
Err(errors::Error::Cplex(errors::Cplex::Unbounded { .. }))
));
}
}