use std::ffi::CString;
use std::os::raw::c_void;
use super::{LinearProgram, OpfError, OptStatus, Solution, Solver};
#[allow(
non_camel_case_types,
non_snake_case,
non_upper_case_globals,
dead_code,
clippy::all
)]
mod bindings {
include!(concat!(env!("OUT_DIR"), "/highs_bindings.rs"));
}
type Int = bindings::HighsInt;
pub struct HighsSolver {
handle: *mut c_void,
}
impl HighsSolver {
pub fn new() -> Result<Self, OpfError> {
let handle = unsafe { bindings::Highs_create() };
if handle.is_null() {
return Err(OpfError::Backend("Highs_create returned null".to_string()));
}
let solver = Self { handle };
solver.set_output(false)?;
Ok(solver)
}
pub fn set_output(&self, enabled: bool) -> Result<(), OpfError> {
let option = CString::new("output_flag").expect("literal has no interior nul");
let status = unsafe {
bindings::Highs_setBoolOptionValue(self.handle, option.as_ptr(), Int::from(enabled))
};
check(status, "Highs_setBoolOptionValue(output_flag)")
}
pub fn set_tolerance(&self, primal: f64, dual: f64) -> Result<(), OpfError> {
for (name, value) in [
("primal_feasibility_tolerance", primal),
("dual_feasibility_tolerance", dual),
] {
let option = CString::new(name).expect("literal has no interior nul");
let status = unsafe {
bindings::Highs_setDoubleOptionValue(self.handle, option.as_ptr(), value)
};
check(status, name)?;
}
Ok(())
}
pub fn version() -> (i32, i32, i32) {
unsafe {
(
bindings::Highs_versionMajor() as i32,
bindings::Highs_versionMinor() as i32,
bindings::Highs_versionPatch() as i32,
)
}
}
}
impl Drop for HighsSolver {
fn drop(&mut self) {
unsafe { bindings::Highs_destroy(self.handle) };
}
}
impl Solver for HighsSolver {
fn solve(&mut self, problem: &LinearProgram) -> Result<Solution, OpfError> {
problem.validate()?;
let n_col = problem.n_vars;
let n_row = problem.n_rows;
let a = Csc::from_triplets(&problem.rows, n_col, n_row);
let status = unsafe {
bindings::Highs_passLp(
self.handle,
n_col as Int,
n_row as Int,
a.values.len() as Int,
bindings::kHighsMatrixFormatColwise,
bindings::kHighsObjSenseMinimize,
problem.offset,
problem.col_cost.as_ptr(),
problem.col_lower.as_ptr(),
problem.col_upper.as_ptr(),
problem.row_lower.as_ptr(),
problem.row_upper.as_ptr(),
a.starts.as_ptr(),
a.indices.as_ptr(),
a.values.as_ptr(),
)
};
check(status, "Highs_passLp")?;
if let Some(hessian) = &problem.hessian {
let q = Csc::from_triplets_transposed(hessian, n_col);
let status = unsafe {
bindings::Highs_passHessian(
self.handle,
n_col as Int,
q.values.len() as Int,
bindings::kHighsHessianFormatTriangular,
q.starts.as_ptr(),
q.indices.as_ptr(),
q.values.as_ptr(),
)
};
check(status, "Highs_passHessian")?;
}
if problem.has_integers() {
let integrality: Vec<Int> = (0..n_col)
.map(|c| {
if problem.is_integral(c) {
bindings::kHighsVarTypeInteger
} else {
bindings::kHighsVarTypeContinuous
}
})
.collect();
let status = unsafe {
bindings::Highs_changeColsIntegralityByRange(
self.handle,
0,
n_col as Int - 1,
integrality.as_ptr(),
)
};
check(status, "Highs_changeColsIntegralityByRange")?;
}
let status = unsafe { bindings::Highs_run(self.handle) };
check(status, "Highs_run")?;
let model_status = unsafe { bindings::Highs_getModelStatus(self.handle) };
let status = map_status(model_status);
if status != OptStatus::Optimal {
return Ok(Solution::failed(status));
}
let mut primal = vec![0.0; n_col];
let mut col_dual = vec![0.0; n_col];
let mut row_activity = vec![0.0; n_row];
let mut row_dual = vec![0.0; n_row];
let get = unsafe {
bindings::Highs_getSolution(
self.handle,
primal.as_mut_ptr(),
col_dual.as_mut_ptr(),
row_activity.as_mut_ptr(),
row_dual.as_mut_ptr(),
)
};
check(get, "Highs_getSolution")?;
let (col_dual, row_dual) = if problem.has_integers() {
(vec![0.0; n_col], vec![0.0; n_row])
} else {
(col_dual, row_dual)
};
let objective = unsafe { bindings::Highs_getObjectiveValue(self.handle) };
Ok(Solution {
status,
objective,
primal,
row_activity,
col_dual,
row_dual,
})
}
}
fn check(status: Int, call: &str) -> Result<(), OpfError> {
if status == bindings::kHighsStatusOk {
return Ok(());
}
if status == bindings::kHighsStatusWarning {
return Ok(());
}
let (major, minor, patch) = HighsSolver::version();
Err(OpfError::Backend(format!(
"{call} failed with HiGHS status {status} (library {major}.{minor}.{patch})"
)))
}
fn map_status(status: Int) -> OptStatus {
if status == bindings::kHighsModelStatusOptimal {
OptStatus::Optimal
} else if status == bindings::kHighsModelStatusInfeasible {
OptStatus::Infeasible
} else if status == bindings::kHighsModelStatusUnbounded {
OptStatus::Unbounded
} else if status == bindings::kHighsModelStatusUnboundedOrInfeasible {
OptStatus::Other("unbounded or infeasible (HiGHS did not distinguish)")
} else if status == bindings::kHighsModelStatusIterationLimit {
OptStatus::Other("iteration limit")
} else if status == bindings::kHighsModelStatusTimeLimit {
OptStatus::Other("time limit")
} else {
OptStatus::Other("no optimal solution")
}
}
struct Csc {
starts: Vec<Int>,
indices: Vec<Int>,
values: Vec<f64>,
}
impl Csc {
fn from_triplets(triplets: &[(usize, usize, f64)], n_col: usize, _n_row: usize) -> Self {
Self::build(n_col, triplets.iter().map(|&(row, col, v)| (col, row, v)))
}
fn from_triplets_transposed(triplets: &[(usize, usize, f64)], n_col: usize) -> Self {
Self::build(n_col, triplets.iter().map(|&(i, j, v)| (j, i, v)))
}
fn build(n_col: usize, entries: impl Iterator<Item = (usize, usize, f64)>) -> Self {
let mut by_col: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n_col];
for (col, row, value) in entries {
by_col[col].push((row, value));
}
let mut starts = Vec::with_capacity(n_col + 1);
let mut indices = Vec::new();
let mut values = Vec::new();
for column in &mut by_col {
starts.push(indices.len() as Int);
column.sort_unstable_by_key(|&(row, _)| row);
let mut last_row: Option<usize> = None;
for &(row, value) in column.iter() {
if last_row == Some(row) {
*values.last_mut().expect("last_row implies a value") += value;
} else {
indices.push(row as Int);
values.push(value);
last_row = Some(row);
}
}
}
starts.push(indices.len() as Int);
Self {
starts,
indices,
values,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn csc_orders_by_column_then_row() {
let triplets = vec![(1, 1, 5.0), (0, 0, 1.0), (1, 0, 2.0), (0, 1, 4.0)];
let csc = Csc::from_triplets(&triplets, 2, 2);
assert_eq!(csc.starts, vec![0, 2, 4]);
assert_eq!(csc.indices, vec![0, 1, 0, 1]);
assert_eq!(csc.values, vec![1.0, 2.0, 4.0, 5.0]);
}
#[test]
fn csc_sums_duplicate_entries() {
let triplets = vec![(0, 0, 1.5), (0, 0, 2.5), (1, 0, 1.0)];
let csc = Csc::from_triplets(&triplets, 1, 2);
assert_eq!(csc.starts, vec![0, 2]);
assert_eq!(csc.indices, vec![0, 1]);
assert_eq!(csc.values, vec![4.0, 1.0]);
}
#[test]
fn csc_keeps_empty_columns() {
let triplets = vec![(0, 2, 7.0)];
let csc = Csc::from_triplets(&triplets, 3, 1);
assert_eq!(csc.starts, vec![0, 0, 0, 1]);
assert_eq!(csc.indices, vec![0]);
}
#[test]
fn the_linked_library_reports_a_plausible_version() {
let (major, _, _) = HighsSolver::version();
assert!(major >= 1, "expected HiGHS 1.x or newer, got major {major}");
}
}