#![forbid(missing_docs)]
use std::convert::{TryFrom, TryInto};
use std::ffi::{c_void, CString};
use std::num::TryFromIntError;
use std::ops::{Bound, Index, RangeBounds};
use std::os::raw::c_int;
use std::ptr::null;
use highs_sys::*;
pub use matrix_col::{ColMatrix, Row};
pub use matrix_row::{Col, RowMatrix};
pub use status::{HighsModelStatus, HighsSolutionStatus, HighsStatus};
use crate::options::HighsOptionValue;
pub type RowProblem = Problem<RowMatrix>;
pub type ColProblem = Problem<ColMatrix>;
mod matrix_col;
mod matrix_row;
mod options;
mod status;
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Problem<MATRIX = ColMatrix> {
colcost: Vec<f64>,
collower: Vec<f64>,
colupper: Vec<f64>,
rowlower: Vec<f64>,
rowupper: Vec<f64>,
integrality: Option<Vec<HighsInt>>,
matrix: MATRIX,
}
impl<MATRIX: Default> Problem<MATRIX>
where
Problem<ColMatrix>: From<Problem<MATRIX>>,
{
pub fn num_cols(&self) -> usize {
self.colcost.len()
}
pub fn num_rows(&self) -> usize {
self.rowlower.len()
}
fn add_row_inner<N: Into<f64> + Copy, B: RangeBounds<N>>(&mut self, bounds: B) -> Row {
let r = Row(self.num_rows().try_into().expect("too many rows"));
let low = bound_value(bounds.start_bound()).unwrap_or(f64::NEG_INFINITY);
let high = bound_value(bounds.end_bound()).unwrap_or(f64::INFINITY);
self.rowlower.push(low);
self.rowupper.push(high);
r
}
fn add_column_inner<N: Into<f64> + Copy, B: RangeBounds<N>>(
&mut self,
col_factor: f64,
bounds: B,
is_integral: bool,
) {
if is_integral && self.integrality.is_none() {
self.integrality = Some(vec![0; self.num_cols()]);
}
if let Some(integrality) = &mut self.integrality {
integrality.push(if is_integral { 1 } else { 0 });
}
self.colcost.push(col_factor);
let low = bound_value(bounds.start_bound()).unwrap_or(f64::NEG_INFINITY);
let high = bound_value(bounds.end_bound()).unwrap_or(f64::INFINITY);
self.collower.push(low);
self.colupper.push(high);
}
pub fn optimise(self, sense: Sense) -> Model {
self.try_optimise(sense).expect("invalid problem")
}
pub fn try_optimise(self, sense: Sense) -> Result<Model, HighsStatus> {
let mut m = Model::try_new(self)?;
m.set_sense(sense);
Ok(m)
}
pub fn new() -> Self {
Self::default()
}
}
fn bound_value<N: Into<f64> + Copy>(b: Bound<&N>) -> Option<f64> {
match b {
Bound::Included(v) | Bound::Excluded(v) => Some((*v).into()),
Bound::Unbounded => None,
}
}
fn c(n: usize) -> HighsInt {
n.try_into().expect("size too large for HiGHS")
}
macro_rules! highs_call {
($function_name:ident ($($param:expr),+)) => {
try_handle_status(
$function_name($($param),+),
stringify!($function_name)
)
}
}
#[derive(Debug)]
pub struct Model {
highs: HighsPtr,
}
#[derive(Debug)]
pub struct SolvedModel {
highs: HighsPtr,
}
#[repr(C)]
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub enum Sense {
Maximise = OBJECTIVE_SENSE_MAXIMIZE as isize,
Minimise = OBJECTIVE_SENSE_MINIMIZE as isize,
}
impl Model {
pub fn as_ptr(&self) -> *const c_void {
self.highs.ptr()
}
pub fn as_mut_ptr(&mut self) -> *mut c_void {
self.highs.mut_ptr()
}
pub fn set_sense(&mut self, sense: Sense) {
let ret = unsafe { Highs_changeObjectiveSense(self.highs.mut_ptr(), sense as c_int) };
assert_eq!(ret, STATUS_OK, "changeObjectiveSense failed");
}
pub fn new<P: Into<Problem<ColMatrix>>>(problem: P) -> Self {
Self::try_new(problem).expect("incoherent problem")
}
pub fn try_new<P: Into<Problem<ColMatrix>>>(problem: P) -> Result<Self, HighsStatus> {
let mut highs = HighsPtr::default();
highs.make_quiet();
let problem = problem.into();
log::debug!(
"Adding a problem with {} variables and {} constraints to HiGHS",
problem.num_cols(),
problem.num_rows()
);
let offset = 0.0;
unsafe {
if let Some(integrality) = &problem.integrality {
highs_call!(Highs_passMip(
highs.mut_ptr(),
c(problem.num_cols()),
c(problem.num_rows()),
c(problem.matrix.avalue.len()),
MATRIX_FORMAT_COLUMN_WISE,
OBJECTIVE_SENSE_MINIMIZE,
offset,
problem.colcost.as_ptr(),
problem.collower.as_ptr(),
problem.colupper.as_ptr(),
problem.rowlower.as_ptr(),
problem.rowupper.as_ptr(),
problem.matrix.astart.as_ptr(),
problem.matrix.aindex.as_ptr(),
problem.matrix.avalue.as_ptr(),
integrality.as_ptr()
))
} else {
highs_call!(Highs_passLp(
highs.mut_ptr(),
c(problem.num_cols()),
c(problem.num_rows()),
c(problem.matrix.avalue.len()),
MATRIX_FORMAT_COLUMN_WISE,
OBJECTIVE_SENSE_MINIMIZE,
offset,
problem.colcost.as_ptr(),
problem.collower.as_ptr(),
problem.colupper.as_ptr(),
problem.rowlower.as_ptr(),
problem.rowupper.as_ptr(),
problem.matrix.astart.as_ptr(),
problem.matrix.aindex.as_ptr(),
problem.matrix.avalue.as_ptr()
))
}
.map(|_| Self { highs })
}
}
pub fn make_quiet(&mut self) {
self.highs.make_quiet()
}
pub fn set_option<STR: Into<Vec<u8>>, V: HighsOptionValue>(&mut self, option: STR, value: V) {
self.highs.set_option(option, value)
}
pub fn solve(self) -> SolvedModel {
self.try_solve().expect("HiGHS error: invalid problem")
}
pub fn try_solve(mut self) -> Result<SolvedModel, HighsStatus> {
unsafe { highs_call!(Highs_run(self.highs.mut_ptr())) }
.map(|_| SolvedModel { highs: self.highs })
}
pub fn add_row(
&mut self,
bounds: impl RangeBounds<f64>,
row_factors: impl IntoIterator<Item = (Col, f64)>,
) -> Row {
self.try_add_row(bounds, row_factors)
.unwrap_or_else(|e| panic!("HiGHS error: {e:?}"))
}
pub fn try_add_row(
&mut self,
bounds: impl RangeBounds<f64>,
row_factors: impl IntoIterator<Item = (Col, f64)>,
) -> Result<Row, HighsStatus> {
let (cols, factors): (Vec<_>, Vec<_>) = row_factors.into_iter().unzip();
unsafe {
highs_call!(Highs_addRow(
self.highs.mut_ptr(),
bound_value(bounds.start_bound()).unwrap_or(f64::NEG_INFINITY),
bound_value(bounds.end_bound()).unwrap_or(f64::INFINITY),
cols.len().try_into().unwrap(),
cols.into_iter()
.map(|c| c.0.try_into().unwrap())
.collect::<Vec<_>>()
.as_ptr(),
factors.as_ptr()
))
}?;
Ok(Row((self.highs.num_rows()? - 1) as c_int))
}
pub fn add_col(
&mut self,
col_factor: f64,
bounds: impl RangeBounds<f64>,
row_factors: impl IntoIterator<Item = (Row, f64)>,
) -> Col {
self.try_add_column(col_factor, bounds, row_factors)
.unwrap_or_else(|e| panic!("HiGHS error: {e:?}"))
}
pub fn try_add_column(
&mut self,
col_factor: f64,
bounds: impl RangeBounds<f64>,
row_factors: impl IntoIterator<Item = (Row, f64)>,
) -> Result<Col, HighsStatus> {
let (rows, factors): (Vec<_>, Vec<_>) = row_factors.into_iter().unzip();
unsafe {
highs_call!(Highs_addCol(
self.highs.mut_ptr(),
col_factor,
bound_value(bounds.start_bound()).unwrap_or(f64::NEG_INFINITY),
bound_value(bounds.end_bound()).unwrap_or(f64::INFINITY),
rows.len().try_into().unwrap(),
rows.into_iter().map(|r| r.0).collect::<Vec<_>>().as_ptr(),
factors.as_ptr()
))
}?;
Ok(Col(self.highs.num_cols()? - 1))
}
pub fn set_solution(
&mut self,
cols: Option<&[f64]>,
rows: Option<&[f64]>,
col_duals: Option<&[f64]>,
row_duals: Option<&[f64]>,
) {
self.try_set_solution(cols, rows, col_duals, row_duals)
.unwrap_or_else(|e| panic!("HiGHS error: {e:?}"))
}
pub fn try_set_solution(
&mut self,
cols: Option<&[f64]>,
rows: Option<&[f64]>,
col_duals: Option<&[f64]>,
row_duals: Option<&[f64]>,
) -> Result<(), HighsStatus> {
let num_cols = self.highs.num_cols()?;
let num_rows = self.highs.num_rows()?;
if let Some(cols) = cols {
if cols.len() != num_cols {
return Err(HighsStatus::Error);
}
}
if let Some(rows) = rows {
if rows.len() != num_rows {
return Err(HighsStatus::Error);
}
}
if let Some(col_duals) = col_duals {
if col_duals.len() != num_cols {
return Err(HighsStatus::Error);
}
}
if let Some(row_duals) = row_duals {
if row_duals.len() != num_rows {
return Err(HighsStatus::Error);
}
}
unsafe {
highs_call!(Highs_setSolution(
self.highs.mut_ptr(),
cols.map(|x| { x.as_ptr() }).unwrap_or(null()),
rows.map(|x| { x.as_ptr() }).unwrap_or(null()),
col_duals.map(|x| { x.as_ptr() }).unwrap_or(null()),
row_duals.map(|x| { x.as_ptr() }).unwrap_or(null())
))
}?;
Ok(())
}
}
impl From<SolvedModel> for Model {
fn from(solved: SolvedModel) -> Self {
Self {
highs: solved.highs,
}
}
}
#[derive(Debug)]
struct HighsPtr(*mut c_void);
impl Drop for HighsPtr {
fn drop(&mut self) {
unsafe { Highs_destroy(self.0) }
}
}
impl Default for HighsPtr {
fn default() -> Self {
Self(unsafe { Highs_create() })
}
}
impl HighsPtr {
#[allow(dead_code)]
const fn ptr(&self) -> *const c_void {
self.0
}
unsafe fn unsafe_mut_ptr(&self) -> *mut c_void {
self.0
}
fn mut_ptr(&mut self) -> *mut c_void {
self.0
}
pub fn make_quiet(&mut self) {
self.set_option(&b"output_flag"[..], false);
self.set_option(&b"log_to_console"[..], false);
}
pub fn set_option<STR: Into<Vec<u8>>, V: HighsOptionValue>(&mut self, option: STR, value: V) {
let c_str = CString::new(option).expect("invalid option name");
let status = unsafe { value.apply_to_highs(self.mut_ptr(), c_str.as_ptr()) };
try_handle_status(status, "Highs_setOptionValue")
.expect("An error was encountered in HiGHS.");
}
fn num_cols(&self) -> Result<usize, TryFromIntError> {
let n = unsafe { Highs_getNumCols(self.0) };
n.try_into()
}
fn num_rows(&self) -> Result<usize, TryFromIntError> {
let n = unsafe { Highs_getNumRows(self.0) };
n.try_into()
}
}
impl SolvedModel {
pub fn as_ptr(&self) -> *const c_void {
self.highs.ptr()
}
pub fn as_mut_ptr(&mut self) -> *mut c_void {
self.highs.mut_ptr()
}
pub fn objective_value(&self) -> f64 {
unsafe { highs_sys::Highs_getObjectiveValue(self.as_ptr()) }
}
pub fn status(&self) -> HighsModelStatus {
let model_status = unsafe { Highs_getModelStatus(self.highs.unsafe_mut_ptr()) };
HighsModelStatus::try_from(model_status).unwrap()
}
pub fn mip_gap(&self) -> f64 {
let name = CString::new("mip_gap").unwrap();
let gap: &mut f64 = &mut -1.0;
let status =
unsafe { Highs_getDoubleInfoValue(self.highs.unsafe_mut_ptr(), name.as_ptr(), gap) };
try_handle_status(status, "Highs_getDoubleInfoValue")
.map(|_| *gap)
.unwrap()
}
pub fn primal_solution_status(&self) -> HighsSolutionStatus {
let name = CString::new("primal_solution_status").unwrap();
let solution_status: &mut HighsInt = &mut -1;
let status = unsafe {
Highs_getIntInfoValue(self.highs.unsafe_mut_ptr(), name.as_ptr(), solution_status)
};
try_handle_status(status, "Highs_getIntInfoValue")
.map(|_| HighsSolutionStatus::try_from(*solution_status).unwrap())
.unwrap()
}
pub fn get_solution(&self) -> Solution {
let cols = self.num_cols();
let rows = self.num_rows();
let mut colvalue: Vec<f64> = vec![0.; cols];
let mut coldual: Vec<f64> = vec![0.; cols];
let mut rowvalue: Vec<f64> = vec![0.; rows];
let mut rowdual: Vec<f64> = vec![0.; rows];
unsafe {
Highs_getSolution(
self.highs.unsafe_mut_ptr(),
colvalue.as_mut_ptr(),
coldual.as_mut_ptr(),
rowvalue.as_mut_ptr(),
rowdual.as_mut_ptr(),
);
}
Solution {
colvalue,
coldual,
rowvalue,
rowdual,
}
}
fn num_cols(&self) -> usize {
self.highs.num_cols().expect("invalid number of columns")
}
fn num_rows(&self) -> usize {
self.highs.num_rows().expect("invalid number of rows")
}
}
#[derive(Clone, Debug)]
pub struct Solution {
colvalue: Vec<f64>,
coldual: Vec<f64>,
rowvalue: Vec<f64>,
rowdual: Vec<f64>,
}
impl Solution {
pub fn columns(&self) -> &[f64] {
&self.colvalue
}
pub fn dual_columns(&self) -> &[f64] {
&self.coldual
}
pub fn rows(&self) -> &[f64] {
&self.rowvalue
}
pub fn dual_rows(&self) -> &[f64] {
&self.rowdual
}
}
impl Index<Col> for Solution {
type Output = f64;
fn index(&self, col: Col) -> &f64 {
&self.colvalue[col.0]
}
}
fn try_handle_status(status: c_int, msg: &str) -> Result<HighsStatus, HighsStatus> {
let status_enum = HighsStatus::try_from(status)
.expect("HiGHS returned an unexpected status value. Please report it as a bug to https://github.com/rust-or/highs/issues");
match status_enum {
status @ HighsStatus::OK => Ok(status),
status @ HighsStatus::Warning => {
log::warn!("HiGHS emitted a warning: {msg}");
Ok(status)
}
error => Err(error),
}
}
#[cfg(test)]
mod test {
use super::*;
fn test_coefs(coefs: [f64; 2]) {
let mut problem = RowProblem::new();
let x = problem.add_column(1., -1..);
let y = problem.add_column(1., 0..);
problem.add_row(..1, [x, y].iter().copied().zip(coefs)); let solution = problem.optimise(Sense::Minimise).solve().get_solution();
assert_eq!([-1., 0.], solution.columns());
}
#[test]
fn test_single_zero_coef() {
test_coefs([1.0, 0.0]);
test_coefs([0.0, 1.0]);
}
#[test]
fn test_all_zero_coefs() {
test_coefs([0.0, 0.0])
}
#[test]
fn test_no_zero_coefs() {
test_coefs([1.0, 1.0])
}
#[test]
fn test_infeasible_empty_row() {
let mut problem = RowProblem::new();
let row_factors: &[(Col, f64)] = &[];
problem.add_row(2..3, row_factors);
let _ = problem.optimise(Sense::Minimise).try_solve();
}
#[test]
fn test_add_row_and_col() {
let mut model = Model::new::<Problem<ColMatrix>>(Problem::default());
let col = model.add_col(1., 1.0.., vec![]);
model.add_row(..1.0, vec![(col, 1.0)]);
let solved = model.solve();
assert_eq!(solved.status(), HighsModelStatus::Optimal);
let solution = solved.get_solution();
assert_eq!(solution.columns(), vec![1.0]);
let mut model = Model::from(solved);
let new_col = model.add_col(1., ..1.0, vec![]);
model.add_row(2.0.., vec![(new_col, 1.0)]);
let solved = model.solve();
assert_eq!(solved.status(), HighsModelStatus::Infeasible);
}
#[test]
fn test_initial_solution() {
use crate::status::HighsModelStatus::Optimal;
use crate::{Model, RowProblem, Sense};
let mut p = RowProblem::default();
p.add_column(1., 0..50);
let mut m = Model::new(p);
m.make_quiet();
m.set_sense(Sense::Maximise);
m.set_option("time_limit", 0);
m.set_solution(Some(&[50.0]), Some(&[]), Some(&[1.0]), Some(&[]));
let solved = m.solve();
assert_eq!(solved.status(), Optimal);
assert_eq!(solved.get_solution().columns(), &[50.0]);
}
#[test]
fn test_mip_gap() {
use crate::status::HighsModelStatus::Optimal;
use crate::{Model, RowProblem, Sense};
let mut p = RowProblem::default();
p.add_integer_column(1., 0..50);
let mut m = Model::new(p);
m.make_quiet();
m.set_sense(Sense::Maximise);
let solved = m.solve();
println!("{:?}", solved.get_solution());
assert_eq!(solved.status(), Optimal);
assert_eq!(solved.mip_gap(), 0.0);
assert_eq!(solved.get_solution().columns(), &[50.0]);
}
#[test]
fn test_inf_mip_gap() {
use crate::status::HighsModelStatus::Optimal;
use crate::{Model, RowProblem, Sense};
let mut p = RowProblem::default();
p.add_column(1., 0..50);
let mut m = Model::new(p);
m.make_quiet();
m.set_sense(Sense::Maximise);
let solved = m.solve();
println!("{:?}", solved.get_solution());
assert_eq!(solved.status(), Optimal);
assert_eq!(solved.mip_gap(), f64::INFINITY);
assert_eq!(solved.get_solution().columns(), &[50.0]);
}
#[test]
fn test_objective_value() {
use crate::status::HighsModelStatus::Optimal;
use crate::{Model, RowProblem, Sense};
let mut p = RowProblem::default();
p.add_column(1., 0..50);
let mut m = Model::new(p);
m.make_quiet();
m.set_sense(Sense::Maximise);
let solved = m.solve();
assert_eq!(solved.status(), Optimal);
assert_eq!(solved.objective_value(), 50.0);
}
#[test]
fn test_objective_value_empty_model() {
use crate::{Model, RowProblem};
let m = Model::new(RowProblem::default());
let solved = m.solve();
assert_eq!(solved.objective_value(), 0.0);
}
}