use std::os::raw::c_void;
use std::time::Instant;
use super::config::{HighsProfile, default_options};
use crate::{
DEFAULT_PROFILE_HEURISTIC_SENTINEL, DEFAULT_PROFILE_IPM_UNBOUNDED_SENTINEL, ffi,
types::{SolutionView, SolverError, SolverStatistics},
};
pub struct HighsSolver {
pub(super) handle: *mut c_void,
pub(super) col_value: Vec<f64>,
pub(super) col_dual: Vec<f64>,
pub(super) row_value: Vec<f64>,
pub(super) row_dual: Vec<f64>,
pub(super) scratch_i32: Vec<i32>,
pub(super) basis_col_i32: Vec<i32>,
pub(super) basis_row_i32: Vec<i32>,
pub(super) terminal_status_dual_scratch: Vec<f64>,
pub(super) terminal_status_primal_scratch: Vec<f64>,
pub(super) num_cols: usize,
pub(super) num_rows: usize,
pub(super) has_model: bool,
pub(super) stats: SolverStatistics,
pub(super) current_profile: HighsProfile,
}
unsafe impl Send for HighsSolver {}
impl HighsSolver {
pub fn new() -> Result<Self, SolverError> {
let handle = unsafe { ffi::cobre_highs_create() };
if handle.is_null() {
return Err(SolverError::InternalError {
message: "HiGHS instance creation failed: Highs_create() returned null".to_string(),
error_code: None,
});
}
if let Err(e) = Self::apply_default_config(handle) {
unsafe { ffi::cobre_highs_destroy(handle) };
return Err(e);
}
Ok(Self {
handle,
col_value: Vec::new(),
col_dual: Vec::new(),
row_value: Vec::new(),
row_dual: Vec::new(),
scratch_i32: Vec::new(),
basis_col_i32: Vec::new(),
basis_row_i32: Vec::new(),
terminal_status_dual_scratch: Vec::new(),
terminal_status_primal_scratch: Vec::new(),
num_cols: 0,
num_rows: 0,
has_model: false,
stats: SolverStatistics {
retry_level_histogram: vec![0u64; 12],
..SolverStatistics::default()
},
current_profile: HighsProfile::default(),
})
}
fn apply_default_config(handle: *mut c_void) -> Result<(), SolverError> {
for opt in &default_options() {
let status = unsafe { opt.apply(handle) };
if status == ffi::HIGHS_STATUS_ERROR {
return Err(SolverError::InternalError {
message: format!(
"HiGHS configuration failed: {}",
opt.name.to_str().unwrap_or("?")
),
error_code: Some(status),
});
}
}
Ok(())
}
pub(super) fn extract_solution_view(&mut self, solve_time_seconds: f64) -> SolutionView<'_> {
let status = unsafe {
ffi::cobre_highs_get_solution(
self.handle,
self.col_value.as_mut_ptr(),
self.col_dual.as_mut_ptr(),
self.row_value.as_mut_ptr(),
self.row_dual.as_mut_ptr(),
)
};
debug_assert_ne!(
status,
ffi::HIGHS_STATUS_ERROR,
"cobre_highs_get_solution failed after optimal solve; HiGHS invariant violation"
);
let objective = unsafe { ffi::cobre_highs_get_objective_value(self.handle) };
#[allow(clippy::cast_sign_loss)]
let iterations =
unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
SolutionView {
objective,
primal: &self.col_value[..self.num_cols],
dual: &self.row_dual[..self.num_rows],
reduced_costs: &self.col_dual[..self.num_cols],
iterations,
solve_time_seconds,
}
}
pub(super) fn apply_profile_tolerances(&mut self) {
unsafe {
ffi::cobre_highs_set_double_option(
self.handle,
c"primal_feasibility_tolerance".as_ptr(),
self.current_profile.primal_feasibility_tolerance,
);
ffi::cobre_highs_set_double_option(
self.handle,
c"dual_feasibility_tolerance".as_ptr(),
self.current_profile.dual_feasibility_tolerance,
);
ffi::cobre_highs_set_int_option(
self.handle,
c"simplex_dual_edge_weight_strategy".as_ptr(),
self.current_profile.simplex_dual_edge_weight_strategy,
);
ffi::cobre_highs_set_int_option(
self.handle,
c"simplex_scale_strategy".as_ptr(),
self.current_profile.simplex_scale_strategy,
);
ffi::cobre_highs_set_int_option(
self.handle,
c"simplex_price_strategy".as_ptr(),
self.current_profile.simplex_price_strategy,
);
}
}
pub(super) fn restore_default_settings(&mut self) {
for opt in &default_options() {
let status = unsafe { opt.apply(self.handle) };
debug_assert_eq!(
status,
ffi::HIGHS_STATUS_OK,
"restore_default_settings: option {:?} failed with status {status}",
opt.name,
);
}
}
pub(super) fn run_once(&mut self) -> i32 {
let run_status = unsafe { ffi::cobre_highs_run(self.handle) };
if run_status == ffi::HIGHS_STATUS_ERROR {
return ffi::HIGHS_MODEL_STATUS_SOLVE_ERROR;
}
unsafe { ffi::cobre_highs_get_model_status(self.handle) }
}
pub(super) fn set_iteration_limits(&mut self) {
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let simplex_iter_limit: i32 =
if self.current_profile.simplex_iteration_limit == DEFAULT_PROFILE_HEURISTIC_SENTINEL {
let heuristic = self.num_cols.saturating_mul(50).max(100_000);
(heuristic.min(i32::MAX as usize)) as i32
} else {
(self
.current_profile
.simplex_iteration_limit
.min(i32::MAX as u32)) as i32
};
#[allow(clippy::cast_possible_wrap)]
let ipm_iter_limit: i32 =
if self.current_profile.ipm_iteration_limit == DEFAULT_PROFILE_IPM_UNBOUNDED_SENTINEL {
i32::MAX } else {
(self
.current_profile
.ipm_iteration_limit
.min(i32::MAX as u32)) as i32
};
unsafe {
ffi::cobre_highs_set_int_option(
self.handle,
c"simplex_iteration_limit".as_ptr(),
simplex_iter_limit,
);
ffi::cobre_highs_set_int_option(
self.handle,
c"ipm_iteration_limit".as_ptr(),
ipm_iter_limit,
);
}
}
pub(super) fn restore_iteration_limits(&mut self) {
unsafe {
ffi::cobre_highs_set_int_option(
self.handle,
c"simplex_iteration_limit".as_ptr(),
i32::MAX,
);
ffi::cobre_highs_set_int_option(self.handle, c"ipm_iteration_limit".as_ptr(), i32::MAX);
}
}
pub(super) fn interpret_terminal_status(
&mut self,
status: i32,
solve_time_seconds: f64,
) -> Option<SolverError> {
match status {
ffi::HIGHS_MODEL_STATUS_OPTIMAL => {
None
}
ffi::HIGHS_MODEL_STATUS_INFEASIBLE => Some(SolverError::Infeasible),
ffi::HIGHS_MODEL_STATUS_UNBOUNDED_OR_INFEASIBLE => {
let mut has_dual_ray: i32 = 0;
self.terminal_status_dual_scratch.resize(self.num_rows, 0.0);
let dual_status = unsafe {
ffi::cobre_highs_get_dual_ray(
self.handle,
&raw mut has_dual_ray,
self.terminal_status_dual_scratch.as_mut_ptr(),
)
};
if dual_status != ffi::HIGHS_STATUS_ERROR && has_dual_ray != 0 {
return Some(SolverError::Infeasible);
}
let mut has_primal_ray: i32 = 0;
self.terminal_status_primal_scratch
.resize(self.num_cols, 0.0);
let primal_status = unsafe {
ffi::cobre_highs_get_primal_ray(
self.handle,
&raw mut has_primal_ray,
self.terminal_status_primal_scratch.as_mut_ptr(),
)
};
if primal_status != ffi::HIGHS_STATUS_ERROR && has_primal_ray != 0 {
return Some(SolverError::Unbounded);
}
Some(SolverError::Infeasible)
}
ffi::HIGHS_MODEL_STATUS_UNBOUNDED => Some(SolverError::Unbounded),
ffi::HIGHS_MODEL_STATUS_TIME_LIMIT => Some(SolverError::TimeLimitExceeded {
elapsed_seconds: solve_time_seconds,
}),
ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT => {
#[allow(clippy::cast_sign_loss)]
let iterations =
unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
Some(SolverError::IterationLimit { iterations })
}
ffi::HIGHS_MODEL_STATUS_SOLVE_ERROR | ffi::HIGHS_MODEL_STATUS_UNKNOWN => {
None
}
other => Some(SolverError::InternalError {
message: format!("HiGHS returned unexpected model status {other}"),
error_code: Some(other),
}),
}
}
pub(super) fn convert_to_i32_scratch(&mut self, source: &[usize]) -> &[i32] {
if source.len() > self.scratch_i32.len() {
self.scratch_i32.resize(source.len(), 0);
}
for (i, &v) in source.iter().enumerate() {
debug_assert!(
i32::try_from(v).is_ok(),
"usize index {v} overflows i32::MAX at position {i}"
);
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
{
self.scratch_i32[i] = v as i32;
}
}
&self.scratch_i32[..source.len()]
}
pub(super) fn solve_inner(&mut self) -> Result<SolutionView<'_>, SolverError> {
self.set_iteration_limits();
let t0 = Instant::now();
let model_status = self.run_once();
let solve_time = t0.elapsed().as_secs_f64();
self.stats.solve_count += 1;
if model_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL {
#[allow(clippy::cast_sign_loss)]
let iterations =
unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
self.stats.success_count += 1;
self.stats.first_try_successes += 1;
self.stats.total_iterations += iterations;
self.stats.total_solve_time_seconds += solve_time;
self.restore_iteration_limits();
return Ok(self.extract_solution_view(solve_time));
}
let is_unbounded = model_status == ffi::HIGHS_MODEL_STATUS_UNBOUNDED;
let is_infeasible = model_status == ffi::HIGHS_MODEL_STATUS_INFEASIBLE;
let initial_retryable = is_unbounded
|| is_infeasible
|| model_status == ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT
|| model_status == ffi::HIGHS_MODEL_STATUS_TIME_LIMIT
|| solve_time > 15.0;
if !initial_retryable
&& let Some(terminal_err) = self.interpret_terminal_status(model_status, solve_time)
{
self.restore_iteration_limits();
self.stats.failure_count += 1;
return Err(terminal_err);
}
match self.retry_escalation(is_unbounded) {
Ok(outcome) => {
self.stats.retry_count += outcome.attempts;
self.stats.success_count += 1;
self.stats.total_iterations += outcome.iterations;
self.stats.total_solve_time_seconds += outcome.solve_time;
self.stats.retry_level_histogram[outcome.level as usize] += 1;
Ok(self.extract_solution_view(outcome.solve_time))
}
Err((attempts, err)) => {
self.stats.retry_count += attempts;
self.stats.failure_count += 1;
Err(err)
}
}
}
}
impl Drop for HighsSolver {
fn drop(&mut self) {
unsafe { ffi::cobre_highs_destroy(self.handle) };
}
}
#[must_use]
pub fn highs_version() -> String {
let major = unsafe { crate::ffi::cobre_highs_version_major() };
let minor = unsafe { crate::ffi::cobre_highs_version_minor() };
let patch = unsafe { crate::ffi::cobre_highs_version_patch() };
format!("{major}.{minor}.{patch}")
}
#[cfg(feature = "test-support")]
impl HighsSolver {
#[must_use]
pub fn raw_handle(&self) -> *mut std::os::raw::c_void {
self.handle
}
pub fn apply_retry_level_options_for_test(&mut self, level: u32) {
self.apply_retry_level_options(level);
}
pub fn apply_extended_retry_options_for_test(&mut self, level: u32) {
self.apply_extended_retry_options(level);
}
pub fn restore_defaults_then_apply_profile_for_test(&mut self) {
self.restore_default_settings();
self.apply_profile_tolerances();
}
#[must_use]
pub fn get_double_option(&self, option: &std::ffi::CStr) -> Option<f64> {
let mut out = 0.0_f64;
let status = unsafe {
ffi::cobre_highs_get_double_option(self.handle, option.as_ptr(), &raw mut out)
};
if status == ffi::HIGHS_STATUS_ERROR {
None
} else {
Some(out)
}
}
#[must_use]
pub fn get_int_option(&self, option: &std::ffi::CStr) -> Option<i32> {
let mut out = 0_i32;
let status =
unsafe { ffi::cobre_highs_get_int_option(self.handle, option.as_ptr(), &raw mut out) };
if status == ffi::HIGHS_STATUS_ERROR {
None
} else {
Some(out)
}
}
}