use std::ffi::CStr;
use std::os::raw::c_void;
use std::time::Instant;
use crate::{
SolverInterface, ffi,
types::{RowBatch, SolutionView, SolverError, SolverStatistics, StageTemplate},
};
enum OptionValue {
Str(&'static CStr),
Int(i32),
Bool(i32),
Double(f64),
}
struct DefaultOption {
name: &'static CStr,
value: OptionValue,
}
impl DefaultOption {
unsafe fn apply(&self, handle: *mut c_void) -> i32 {
unsafe {
match &self.value {
OptionValue::Str(val) => {
ffi::cobre_highs_set_string_option(handle, self.name.as_ptr(), val.as_ptr())
}
OptionValue::Int(val) => {
ffi::cobre_highs_set_int_option(handle, self.name.as_ptr(), *val)
}
OptionValue::Bool(val) => {
ffi::cobre_highs_set_bool_option(handle, self.name.as_ptr(), *val)
}
OptionValue::Double(val) => {
ffi::cobre_highs_set_double_option(handle, self.name.as_ptr(), *val)
}
}
}
}
}
fn default_options() -> [DefaultOption; 13] {
[
DefaultOption {
name: c"solver",
value: OptionValue::Str(c"simplex"),
},
DefaultOption {
name: c"simplex_strategy",
value: OptionValue::Int(1), },
DefaultOption {
name: c"simplex_scale_strategy",
value: OptionValue::Int(0), },
DefaultOption {
name: c"presolve",
value: OptionValue::Str(c"off"),
},
DefaultOption {
name: c"parallel",
value: OptionValue::Str(c"off"),
},
DefaultOption {
name: c"output_flag",
value: OptionValue::Bool(0),
},
DefaultOption {
name: c"primal_feasibility_tolerance",
value: OptionValue::Double(1e-7),
},
DefaultOption {
name: c"dual_feasibility_tolerance",
value: OptionValue::Double(1e-7),
},
DefaultOption {
name: c"simplex_dual_edge_weight_strategy",
value: OptionValue::Int(1), },
DefaultOption {
name: c"dual_simplex_cost_perturbation_multiplier",
value: OptionValue::Double(0.0), },
DefaultOption {
name: c"simplex_initial_condition_check",
value: OptionValue::Bool(0), },
DefaultOption {
name: c"simplex_price_strategy",
value: OptionValue::Int(1), },
DefaultOption {
name: c"rebuild_refactor_solution_error_tolerance",
value: OptionValue::Double(1e-6), },
]
}
pub struct HighsSolver {
handle: *mut c_void,
col_value: Vec<f64>,
col_dual: Vec<f64>,
row_value: Vec<f64>,
row_dual: Vec<f64>,
scratch_i32: Vec<i32>,
basis_col_i32: Vec<i32>,
basis_row_i32: Vec<i32>,
terminal_status_dual_scratch: Vec<f64>,
terminal_status_primal_scratch: Vec<f64>,
num_cols: usize,
num_rows: usize,
has_model: bool,
stats: SolverStatistics,
}
unsafe impl Send for HighsSolver {}
struct RetryOutcome {
attempts: u64,
solve_time: f64,
iterations: u64,
level: u32,
}
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()
},
})
}
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(())
}
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,
}
}
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,
);
}
}
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) }
}
fn set_iteration_limits(&mut self) {
let simplex_iter_limit = self.num_cols.saturating_mul(50).max(100_000);
unsafe {
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
ffi::cobre_highs_set_int_option(
self.handle,
c"simplex_iteration_limit".as_ptr(),
simplex_iter_limit as i32,
);
ffi::cobre_highs_set_int_option(self.handle, c"ipm_iteration_limit".as_ptr(), 10_000);
}
}
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);
}
}
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),
}),
}
}
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()]
}
fn retry_escalation(&mut self, is_unbounded: bool) -> Result<RetryOutcome, (u64, SolverError)> {
let phase1_wall_budget = 15.0_f64;
let phase2_wall_budget = 30.0_f64;
let overall_budget = 120.0_f64;
let num_retry_levels = 12_u32;
let retry_start = Instant::now();
let mut retry_attempts: u64 = 0;
let mut terminal_err: Option<SolverError> = None;
let mut found_optimal = false;
let mut optimal_time = 0.0_f64;
let mut optimal_iterations: u64 = 0;
let mut optimal_level = 0_u32;
for level in 0..num_retry_levels {
if retry_start.elapsed().as_secs_f64() >= overall_budget {
break;
}
self.apply_retry_level_options(level);
retry_attempts += 1;
let t_retry = Instant::now();
let retry_status = self.run_once();
let retry_time = t_retry.elapsed().as_secs_f64();
if retry_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL {
#[allow(clippy::cast_sign_loss)]
let iters =
unsafe { ffi::cobre_highs_get_simplex_iteration_count(self.handle) } as u64;
found_optimal = true;
optimal_time = retry_time;
optimal_iterations = iters;
optimal_level = level;
break;
}
let level_budget = if level <= 4 {
phase1_wall_budget
} else {
phase2_wall_budget
};
let budget_exceeded = retry_time > level_budget;
let retryable = retry_status == ffi::HIGHS_MODEL_STATUS_UNBOUNDED
|| retry_status == ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT
|| budget_exceeded;
if !retryable {
if let Some(e) = self.interpret_terminal_status(retry_status, retry_time) {
terminal_err = Some(e);
break;
}
}
}
self.restore_default_settings();
self.restore_iteration_limits();
unsafe {
ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), 0);
ffi::cobre_highs_set_int_option(self.handle, c"user_bound_scale".as_ptr(), 0);
}
if found_optimal {
return Ok(RetryOutcome {
attempts: retry_attempts,
solve_time: optimal_time,
iterations: optimal_iterations,
level: optimal_level,
});
}
Err((
retry_attempts,
terminal_err.unwrap_or_else(|| {
if is_unbounded {
SolverError::Unbounded
} else {
SolverError::NumericalDifficulty {
message:
"HiGHS failed to reach optimality after all retry escalation levels"
.to_string(),
}
}
}),
))
}
fn apply_retry_level_options(&mut self, level: u32) {
match level {
0 => {
unsafe {
ffi::cobre_highs_clear_solver(self.handle);
ffi::cobre_highs_set_double_option(
self.handle,
c"dual_simplex_cost_perturbation_multiplier".as_ptr(),
1.0,
);
}
self.set_iteration_limits();
}
1 => unsafe {
ffi::cobre_highs_set_string_option(
self.handle,
c"presolve".as_ptr(),
c"on".as_ptr(),
);
},
2 => unsafe {
ffi::cobre_highs_set_int_option(self.handle, c"simplex_strategy".as_ptr(), 1);
},
3 => unsafe {
ffi::cobre_highs_set_double_option(
self.handle,
c"primal_feasibility_tolerance".as_ptr(),
1e-6,
);
ffi::cobre_highs_set_double_option(
self.handle,
c"dual_feasibility_tolerance".as_ptr(),
1e-6,
);
},
4 => unsafe {
ffi::cobre_highs_set_string_option(
self.handle,
c"solver".as_ptr(),
c"ipm".as_ptr(),
);
},
_ => self.apply_extended_retry_options(level),
}
}
fn apply_extended_retry_options(&mut self, level: u32) {
self.restore_default_settings();
self.set_iteration_limits();
unsafe {
ffi::cobre_highs_set_string_option(self.handle, c"presolve".as_ptr(), c"on".as_ptr());
}
match level {
5 => unsafe {
ffi::cobre_highs_set_int_option(self.handle, c"simplex_scale_strategy".as_ptr(), 3);
},
6 => unsafe {
ffi::cobre_highs_set_int_option(self.handle, c"simplex_strategy".as_ptr(), 1);
ffi::cobre_highs_set_int_option(self.handle, c"simplex_scale_strategy".as_ptr(), 4);
},
7 => unsafe {
ffi::cobre_highs_set_int_option(self.handle, c"simplex_scale_strategy".as_ptr(), 3);
ffi::cobre_highs_set_double_option(
self.handle,
c"primal_feasibility_tolerance".as_ptr(),
1e-6,
);
ffi::cobre_highs_set_double_option(
self.handle,
c"dual_feasibility_tolerance".as_ptr(),
1e-6,
);
},
8 => unsafe {
ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), -10);
},
9 => unsafe {
ffi::cobre_highs_set_int_option(self.handle, c"simplex_strategy".as_ptr(), 1);
ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), -10);
ffi::cobre_highs_set_int_option(self.handle, c"user_bound_scale".as_ptr(), -5);
},
10 => unsafe {
ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), -13);
ffi::cobre_highs_set_int_option(self.handle, c"user_bound_scale".as_ptr(), -8);
ffi::cobre_highs_set_double_option(
self.handle,
c"primal_feasibility_tolerance".as_ptr(),
1e-6,
);
ffi::cobre_highs_set_double_option(
self.handle,
c"dual_feasibility_tolerance".as_ptr(),
1e-6,
);
},
11 => unsafe {
ffi::cobre_highs_set_string_option(
self.handle,
c"solver".as_ptr(),
c"ipm".as_ptr(),
);
ffi::cobre_highs_set_int_option(self.handle, c"user_objective_scale".as_ptr(), -10);
ffi::cobre_highs_set_int_option(self.handle, c"user_bound_scale".as_ptr(), -5);
ffi::cobre_highs_set_double_option(
self.handle,
c"primal_feasibility_tolerance".as_ptr(),
1e-6,
);
ffi::cobre_highs_set_double_option(
self.handle,
c"dual_feasibility_tolerance".as_ptr(),
1e-6,
);
},
_ => unreachable!(),
}
}
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 initial_retryable = is_unbounded
|| model_status == ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT
|| model_status == ffi::HIGHS_MODEL_STATUS_TIME_LIMIT
|| solve_time > 15.0;
if !initial_retryable {
if 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}")
}
impl SolverInterface for HighsSolver {
fn name(&self) -> &'static str {
"HiGHS"
}
fn solver_name_version(&self) -> String {
format!("HiGHS {}", highs_version())
}
fn load_model(&mut self, template: &StageTemplate) {
let t0 = Instant::now();
assert!(
i32::try_from(template.num_cols).is_ok(),
"num_cols {} overflows i32: LP exceeds HiGHS API limit",
template.num_cols
);
assert!(
i32::try_from(template.num_rows).is_ok(),
"num_rows {} overflows i32: LP exceeds HiGHS API limit",
template.num_rows
);
assert!(
i32::try_from(template.num_nz).is_ok(),
"num_nz {} overflows i32: LP exceeds HiGHS API limit",
template.num_nz
);
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let num_col = template.num_cols as i32;
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let num_row = template.num_rows as i32;
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let num_nz = template.num_nz as i32;
let status = unsafe {
ffi::cobre_highs_pass_lp(
self.handle,
num_col,
num_row,
num_nz,
ffi::HIGHS_MATRIX_FORMAT_COLWISE,
ffi::HIGHS_OBJ_SENSE_MINIMIZE,
0.0, template.objective.as_ptr(),
template.col_lower.as_ptr(),
template.col_upper.as_ptr(),
template.row_lower.as_ptr(),
template.row_upper.as_ptr(),
template.col_starts.as_ptr(),
template.row_indices.as_ptr(),
template.values.as_ptr(),
)
};
assert_ne!(
status,
ffi::HIGHS_STATUS_ERROR,
"cobre_highs_pass_lp failed with status {status}"
);
self.num_cols = template.num_cols;
self.num_rows = template.num_rows;
self.has_model = true;
self.col_value.resize(self.num_cols, 0.0);
self.col_dual.resize(self.num_cols, 0.0);
self.row_value.resize(self.num_rows, 0.0);
self.row_dual.resize(self.num_rows, 0.0);
self.basis_col_i32.resize(self.num_cols, 0);
self.basis_row_i32.resize(self.num_rows, 0);
self.stats.total_load_model_time_seconds += t0.elapsed().as_secs_f64();
self.stats.load_model_count += 1;
}
fn add_rows(&mut self, rows: &RowBatch) {
assert!(
i32::try_from(rows.num_rows).is_ok(),
"rows.num_rows {} overflows i32: RowBatch exceeds HiGHS API limit",
rows.num_rows
);
assert!(
i32::try_from(rows.col_indices.len()).is_ok(),
"rows nnz {} overflows i32: RowBatch exceeds HiGHS API limit",
rows.col_indices.len()
);
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let num_new_row = rows.num_rows as i32;
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let num_new_nz = rows.col_indices.len() as i32;
let status = unsafe {
ffi::cobre_highs_add_rows(
self.handle,
num_new_row,
rows.row_lower.as_ptr(),
rows.row_upper.as_ptr(),
num_new_nz,
rows.row_starts.as_ptr(),
rows.col_indices.as_ptr(),
rows.values.as_ptr(),
)
};
assert_ne!(
status,
ffi::HIGHS_STATUS_ERROR,
"cobre_highs_add_rows failed with status {status}"
);
self.num_rows += rows.num_rows;
self.row_value.resize(self.num_rows, 0.0);
self.row_dual.resize(self.num_rows, 0.0);
self.basis_row_i32.resize(self.num_rows, 0);
}
fn set_row_bounds(&mut self, indices: &[usize], lower: &[f64], upper: &[f64]) {
assert!(
indices.len() == lower.len() && indices.len() == upper.len(),
"set_row_bounds: indices ({}), lower ({}), and upper ({}) must have equal length",
indices.len(),
lower.len(),
upper.len()
);
if indices.is_empty() {
return;
}
assert!(
i32::try_from(indices.len()).is_ok(),
"set_row_bounds: indices.len() {} overflows i32",
indices.len()
);
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let num_entries = indices.len() as i32;
let t0 = Instant::now();
let status = unsafe {
ffi::cobre_highs_change_rows_bounds_by_set(
self.handle,
num_entries,
self.convert_to_i32_scratch(indices).as_ptr(),
lower.as_ptr(),
upper.as_ptr(),
)
};
assert_ne!(
status,
ffi::HIGHS_STATUS_ERROR,
"cobre_highs_change_rows_bounds_by_set failed with status {status}"
);
self.stats.total_set_bounds_time_seconds += t0.elapsed().as_secs_f64();
}
fn set_col_bounds(&mut self, indices: &[usize], lower: &[f64], upper: &[f64]) {
assert!(
indices.len() == lower.len() && indices.len() == upper.len(),
"set_col_bounds: indices ({}), lower ({}), and upper ({}) must have equal length",
indices.len(),
lower.len(),
upper.len()
);
if indices.is_empty() {
return;
}
assert!(
i32::try_from(indices.len()).is_ok(),
"set_col_bounds: indices.len() {} overflows i32",
indices.len()
);
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let num_entries = indices.len() as i32;
let t0 = Instant::now();
let status = unsafe {
ffi::cobre_highs_change_cols_bounds_by_set(
self.handle,
num_entries,
self.convert_to_i32_scratch(indices).as_ptr(),
lower.as_ptr(),
upper.as_ptr(),
)
};
assert_ne!(
status,
ffi::HIGHS_STATUS_ERROR,
"cobre_highs_change_cols_bounds_by_set failed with status {status}"
);
self.stats.total_set_bounds_time_seconds += t0.elapsed().as_secs_f64();
}
fn solve(
&mut self,
basis: Option<&crate::types::Basis>,
) -> Result<SolutionView<'_>, SolverError> {
assert!(
self.has_model,
"solve called without a loaded model — call load_model first"
);
if let Some(basis) = basis {
assert!(
basis.col_status.len() == self.num_cols,
"basis column count {} does not match LP column count {}",
basis.col_status.len(),
self.num_cols
);
debug_assert!(
basis.row_status.len() >= self.num_rows,
"solve(Some(&basis)): basis.row_status.len() ({}) < self.num_rows ({}); \
callers introducing new rows must reconcile basis (e.g. extend with \
NONBASIC_AT_LOWER for fresh inequality rows) before calling solve. \
The defensive BASIC padding below is incorrect for inequality slacks.",
basis.row_status.len(),
self.num_rows
);
self.stats.basis_offered += 1;
self.basis_col_i32[..self.num_cols].copy_from_slice(&basis.col_status);
let basis_rows = basis.row_status.len();
let lp_rows = self.num_rows;
let copy_len = basis_rows.min(lp_rows);
self.basis_row_i32[..copy_len].copy_from_slice(&basis.row_status[..copy_len]);
if lp_rows > basis_rows {
self.basis_row_i32[basis_rows..lp_rows].fill(ffi::HIGHS_BASIS_STATUS_BASIC);
}
let basis_set_start = Instant::now();
let set_status = unsafe {
ffi::cobre_highs_set_basis_non_alien(
self.handle,
self.basis_col_i32.as_ptr(),
self.basis_row_i32.as_ptr(),
)
};
if set_status == ffi::HIGHS_STATUS_ERROR {
self.stats.basis_consistency_failures += 1;
#[allow(clippy::cast_possible_wrap)]
let col_basic = self.basis_col_i32[..self.num_cols]
.iter()
.filter(|&&s| s == ffi::HIGHS_BASIS_STATUS_BASIC)
.count() as i64;
#[allow(clippy::cast_possible_wrap)]
let row_basic = self.basis_row_i32[..self.num_rows]
.iter()
.filter(|&&s| s == ffi::HIGHS_BASIS_STATUS_BASIC)
.count() as i64;
self.stats.total_basis_set_time_seconds += basis_set_start.elapsed().as_secs_f64();
#[allow(clippy::cast_possible_wrap)]
return Err(SolverError::BasisInconsistent {
num_row: self.num_rows as i64,
total_basic: col_basic + row_basic,
col_basic,
row_basic,
});
}
self.stats.total_basis_set_time_seconds += basis_set_start.elapsed().as_secs_f64();
}
self.solve_inner()
}
fn get_basis(&mut self, out: &mut crate::types::Basis) {
assert!(
self.has_model,
"get_basis called without a loaded model — call load_model first"
);
out.col_status.resize(self.num_cols, 0);
out.row_status.resize(self.num_rows, 0);
let get_status = unsafe {
ffi::cobre_highs_get_basis(
self.handle,
out.col_status.as_mut_ptr(),
out.row_status.as_mut_ptr(),
)
};
assert_ne!(
get_status,
ffi::HIGHS_STATUS_ERROR,
"cobre_highs_get_basis failed: basis must exist after a successful solve (programming error)"
);
}
fn statistics(&self) -> SolverStatistics {
self.stats.clone()
}
fn record_reconstruction_stats(&mut self) {
self.stats.basis_reconstructions += 1;
}
}
#[cfg(feature = "test-support")]
impl HighsSolver {
#[must_use]
pub fn raw_handle(&self) -> *mut std::os::raw::c_void {
self.handle
}
}
#[cfg(test)]
mod tests {
use super::HighsSolver;
use crate::{
SolverInterface,
types::{Basis, RowBatch, StageTemplate},
};
fn make_fixture_stage_template() -> StageTemplate {
StageTemplate {
num_cols: 3,
num_rows: 2,
num_nz: 3,
col_starts: vec![0_i32, 2, 2, 3],
row_indices: vec![0_i32, 1, 1],
values: vec![1.0, 2.0, 1.0],
col_lower: vec![0.0, 0.0, 0.0],
col_upper: vec![10.0, f64::INFINITY, 8.0],
objective: vec![0.0, 1.0, 50.0],
row_lower: vec![6.0, 14.0],
row_upper: vec![6.0, 14.0],
n_state: 1,
n_transfer: 0,
n_dual_relevant: 1,
n_hydro: 1,
max_par_order: 0,
col_scale: Vec::new(),
row_scale: Vec::new(),
}
}
fn make_fixture_row_batch() -> RowBatch {
RowBatch {
num_rows: 2,
row_starts: vec![0_i32, 2, 4],
col_indices: vec![0_i32, 1, 0, 1],
values: vec![-5.0, 1.0, 3.0, 1.0],
row_lower: vec![20.0, 80.0],
row_upper: vec![f64::INFINITY, f64::INFINITY],
}
}
#[test]
fn test_highs_solver_create_and_name() {
let solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
assert_eq!(solver.name(), "HiGHS");
}
#[test]
fn test_highs_solver_send_bound() {
fn assert_send<T: Send>() {}
assert_send::<HighsSolver>();
}
#[test]
fn test_highs_solver_statistics_initial() {
let solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let stats = solver.statistics();
assert_eq!(stats.solve_count, 0);
assert_eq!(stats.success_count, 0);
assert_eq!(stats.failure_count, 0);
assert_eq!(stats.total_iterations, 0);
assert_eq!(stats.retry_count, 0);
assert_eq!(stats.total_solve_time_seconds, 0.0);
}
#[test]
fn test_highs_load_model_updates_dimensions() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let template = make_fixture_stage_template();
solver.load_model(&template);
assert_eq!(solver.num_cols, 3, "num_cols must be 3 after load_model");
assert_eq!(solver.num_rows, 2, "num_rows must be 2 after load_model");
assert_eq!(
solver.col_value.len(),
3,
"col_value buffer must be resized to num_cols"
);
assert_eq!(
solver.col_dual.len(),
3,
"col_dual buffer must be resized to num_cols"
);
assert_eq!(
solver.row_value.len(),
2,
"row_value buffer must be resized to num_rows"
);
assert_eq!(
solver.row_dual.len(),
2,
"row_dual buffer must be resized to num_rows"
);
}
#[test]
fn test_highs_add_rows_updates_dimensions() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let template = make_fixture_stage_template();
let cuts = make_fixture_row_batch();
solver.load_model(&template);
solver.add_rows(&cuts);
assert_eq!(solver.num_rows, 4, "num_rows must be 4 after add_rows");
assert_eq!(
solver.row_dual.len(),
4,
"row_dual buffer must be resized to 4 after add_rows"
);
assert_eq!(
solver.row_value.len(),
4,
"row_value buffer must be resized to 4 after add_rows"
);
assert_eq!(solver.num_cols, 3, "num_cols must be unchanged by add_rows");
}
#[test]
fn test_highs_set_row_bounds_no_panic() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let template = make_fixture_stage_template();
solver.load_model(&template);
solver.set_row_bounds(&[0], &[4.0], &[4.0]);
}
#[test]
fn test_highs_set_col_bounds_no_panic() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let template = make_fixture_stage_template();
solver.load_model(&template);
solver.set_col_bounds(&[1], &[10.0], &[f64::INFINITY]);
}
#[test]
fn test_highs_set_bounds_empty_no_panic() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let template = make_fixture_stage_template();
solver.load_model(&template);
solver.set_row_bounds(&[], &[], &[]);
solver.set_col_bounds(&[], &[], &[]);
}
#[test]
fn test_highs_solve_basic_lp() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let template = make_fixture_stage_template();
solver.load_model(&template);
let solution = solver
.solve(None)
.expect("solve() must succeed on a feasible LP");
assert!(
(solution.objective - 100.0).abs() < 1e-8,
"objective must be 100.0, got {}",
solution.objective
);
assert_eq!(solution.primal.len(), 3, "primal must have 3 elements");
assert!(
(solution.primal[0] - 6.0).abs() < 1e-8,
"primal[0] (x0) must be 6.0, got {}",
solution.primal[0]
);
assert!(
(solution.primal[1] - 0.0).abs() < 1e-8,
"primal[1] (x1) must be 0.0, got {}",
solution.primal[1]
);
assert!(
(solution.primal[2] - 2.0).abs() < 1e-8,
"primal[2] (x2) must be 2.0, got {}",
solution.primal[2]
);
}
#[test]
fn test_highs_solve_with_cuts() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let template = make_fixture_stage_template();
let cuts = make_fixture_row_batch();
solver.load_model(&template);
solver.add_rows(&cuts);
let solution = solver
.solve(None)
.expect("solve() must succeed on a feasible LP with cuts");
assert!(
(solution.objective - 162.0).abs() < 1e-8,
"objective must be 162.0, got {}",
solution.objective
);
assert!(
(solution.primal[0] - 6.0).abs() < 1e-8,
"primal[0] must be 6.0, got {}",
solution.primal[0]
);
assert!(
(solution.primal[1] - 62.0).abs() < 1e-8,
"primal[1] must be 62.0, got {}",
solution.primal[1]
);
assert!(
(solution.primal[2] - 2.0).abs() < 1e-8,
"primal[2] must be 2.0, got {}",
solution.primal[2]
);
}
#[test]
fn test_highs_solve_after_rhs_patch() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let template = make_fixture_stage_template();
let cuts = make_fixture_row_batch();
solver.load_model(&template);
solver.add_rows(&cuts);
solver.set_row_bounds(&[0], &[4.0], &[4.0]);
let solution = solver
.solve(None)
.expect("solve() must succeed after RHS patch");
assert!(
(solution.objective - 368.0).abs() < 1e-8,
"objective must be 368.0, got {}",
solution.objective
);
}
#[test]
fn test_highs_solve_statistics_increment() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let template = make_fixture_stage_template();
solver.load_model(&template);
solver.solve(None).expect("first solve must succeed");
solver.solve(None).expect("second solve must succeed");
let stats = solver.statistics();
assert_eq!(stats.solve_count, 2, "solve_count must be 2");
assert_eq!(stats.success_count, 2, "success_count must be 2");
assert_eq!(stats.failure_count, 0, "failure_count must be 0");
assert!(
stats.total_iterations > 0,
"total_iterations must be positive"
);
}
#[test]
fn test_highs_solve_preserves_stats() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let template = make_fixture_stage_template();
solver.load_model(&template);
solver.solve(None).expect("solve must succeed");
let stats = solver.statistics();
assert_eq!(
stats.solve_count, 1,
"solve_count must be 1 after one solve"
);
assert_eq!(
stats.success_count, 1,
"success_count must be 1 after one successful solve"
);
assert!(
stats.total_iterations > 0,
"total_iterations must be positive after a successful solve"
);
}
#[test]
fn test_highs_solve_iterations_positive() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let template = make_fixture_stage_template();
solver.load_model(&template);
let solution = solver.solve(None).expect("solve must succeed");
assert!(
solution.iterations > 0,
"iterations must be positive, got {}",
solution.iterations
);
}
#[test]
fn test_highs_solve_time_positive() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let template = make_fixture_stage_template();
solver.load_model(&template);
let solution = solver.solve(None).expect("solve must succeed");
assert!(
solution.solve_time_seconds > 0.0,
"solve_time_seconds must be positive, got {}",
solution.solve_time_seconds
);
}
#[test]
fn test_highs_solve_statistics_single() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let template = make_fixture_stage_template();
solver.load_model(&template);
solver.solve(None).expect("solve must succeed");
let stats = solver.statistics();
assert_eq!(stats.solve_count, 1, "solve_count must be 1");
assert_eq!(stats.success_count, 1, "success_count must be 1");
assert_eq!(stats.failure_count, 0, "failure_count must be 0");
assert!(
stats.total_iterations > 0,
"total_iterations must be positive after a successful solve"
);
}
#[test]
fn test_get_basis_valid_status_codes() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let template = make_fixture_stage_template();
solver.load_model(&template);
solver
.solve(None)
.expect("solve must succeed before get_basis");
let mut basis = Basis::new(0, 0);
solver.get_basis(&mut basis);
for &code in &basis.col_status {
assert!(
(0..=4).contains(&code),
"col_status code {code} is outside valid HiGHS range 0..=4"
);
}
for &code in &basis.row_status {
assert!(
(0..=4).contains(&code),
"row_status code {code} is outside valid HiGHS range 0..=4"
);
}
}
#[test]
fn test_get_basis_resizes_output() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let template = make_fixture_stage_template();
solver.load_model(&template);
solver
.solve(None)
.expect("solve must succeed before get_basis");
let mut basis = Basis::new(0, 0);
assert_eq!(
basis.col_status.len(),
0,
"initial col_status must be empty"
);
assert_eq!(
basis.row_status.len(),
0,
"initial row_status must be empty"
);
solver.get_basis(&mut basis);
assert_eq!(
basis.col_status.len(),
3,
"col_status must be resized to 3 (num_cols of SS1.1)"
);
assert_eq!(
basis.row_status.len(),
2,
"row_status must be resized to 2 (num_rows of SS1.1)"
);
}
#[test]
fn test_solve_warm_start_reproduces_cold_objective() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let template = make_fixture_stage_template();
solver.load_model(&template);
solver.solve(None).expect("cold-start solve must succeed");
let mut basis = Basis::new(0, 0);
solver.get_basis(&mut basis);
solver.load_model(&template);
let result = solver
.solve(Some(&basis))
.expect("warm-start solve must succeed");
assert!(
(result.objective - 100.0).abs() < 1e-8,
"warm-start objective must be 100.0, got {}",
result.objective
);
assert!(
result.iterations <= 1,
"warm-start from exact basis must use at most 1 iteration, got {}",
result.iterations
);
let stats = solver.statistics();
assert_eq!(
stats.basis_consistency_failures, 0,
"basis_consistency_failures must be 0 when raw basis is accepted, got {}",
stats.basis_consistency_failures
);
assert_eq!(
stats.basis_offered, 1,
"basis_offered must be 1 after one warm-start call"
);
}
#[cfg(not(debug_assertions))]
#[test]
fn test_solve_warm_start_extends_missing_rows_as_basic() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let template = make_fixture_stage_template();
let cuts = make_fixture_row_batch();
solver.load_model(&template);
solver.solve(None).expect("SS1.1 solve must succeed");
let mut basis = Basis::new(0, 0);
solver.get_basis(&mut basis);
assert_eq!(
basis.row_status.len(),
2,
"captured basis must have 2 row statuses"
);
solver.load_model(&template);
solver.add_rows(&cuts);
assert_eq!(solver.num_rows, 4, "LP must have 4 rows after add_rows");
let result = solver
.solve(Some(&basis))
.expect("solve with dimension-mismatched basis must succeed");
assert!(
(result.objective - 162.0).abs() < 1e-8,
"objective with both cuts active must be 162.0, got {}",
result.objective
);
}
#[test]
fn test_solve_warm_start_non_alien_success() {
let template = make_fixture_stage_template();
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
solver.load_model(&template);
let _ = solver.solve(None).expect("cold-start solve must succeed");
let mut basis = Basis::new(template.num_cols, template.num_rows);
solver.get_basis(&mut basis);
solver.load_model(&template);
let before = solver.statistics();
let _ = solver
.solve(Some(&basis))
.expect("warm-start solve must succeed with self-extracted basis");
let after = solver.statistics();
assert_eq!(
after.basis_consistency_failures - before.basis_consistency_failures,
0,
"non-alien path should accept a self-extracted basis; consistency failures delta must be 0"
);
}
#[test]
fn test_solve_warm_start_rejects_inconsistent_basis() {
use crate::ffi;
use crate::types::SolverError;
let template = make_fixture_stage_template();
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
solver.load_model(&template);
let mut bad_basis = Basis::new(template.num_cols, template.num_rows);
bad_basis
.col_status
.iter_mut()
.for_each(|v| *v = ffi::HIGHS_BASIS_STATUS_BASIC);
bad_basis
.row_status
.iter_mut()
.for_each(|v| *v = ffi::HIGHS_BASIS_STATUS_BASIC);
let before = solver.statistics();
let err_variant: Result<(), SolverError> = solver.solve(Some(&bad_basis)).map(|_| ());
let after = solver.statistics();
assert_eq!(
after.basis_consistency_failures - before.basis_consistency_failures,
1,
"basis_consistency_failures must increment by 1 for an overcounted basis"
);
match err_variant {
Err(SolverError::BasisInconsistent {
num_row,
total_basic,
col_basic,
row_basic,
}) => {
assert_eq!(num_row, 2, "num_row must match LP row count");
assert_eq!(total_basic, 5, "total_basic must be col_basic + row_basic");
assert_eq!(col_basic, 3, "col_basic must count BASIC columns");
assert_eq!(row_basic, 2, "row_basic must count BASIC rows");
}
other => panic!(
"expected Err(SolverError::BasisInconsistent {{ num_row: 2, total_basic: 5, \
col_basic: 3, row_basic: 2 }}), got {other:?}"
),
}
}
#[test]
fn interpret_terminal_status_reuses_scratch() {
let template = make_fixture_stage_template();
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
assert_eq!(
solver.terminal_status_dual_scratch.capacity(),
0,
"dual scratch must start with capacity 0 (Vec::new() in constructor)"
);
assert_eq!(
solver.terminal_status_primal_scratch.capacity(),
0,
"primal scratch must start with capacity 0 (Vec::new() in constructor)"
);
solver.load_model(&template);
solver
.terminal_status_dual_scratch
.resize(solver.num_rows, 0.0);
solver
.terminal_status_primal_scratch
.resize(solver.num_cols, 0.0);
let cap_dual_after_first = solver.terminal_status_dual_scratch.capacity();
let cap_primal_after_first = solver.terminal_status_primal_scratch.capacity();
assert!(
cap_dual_after_first >= solver.num_rows,
"dual scratch capacity {cap_dual_after_first} must be >= num_rows {} after first resize",
solver.num_rows,
);
assert!(
cap_primal_after_first >= solver.num_cols,
"primal scratch capacity {cap_primal_after_first} must be >= num_cols {} after first resize",
solver.num_cols,
);
solver
.terminal_status_dual_scratch
.resize(solver.num_rows, 0.0);
solver
.terminal_status_primal_scratch
.resize(solver.num_cols, 0.0);
let cap_dual_after_second = solver.terminal_status_dual_scratch.capacity();
let cap_primal_after_second = solver.terminal_status_primal_scratch.capacity();
assert!(
cap_dual_after_second >= cap_dual_after_first,
"dual scratch capacity must not decrease: {cap_dual_after_second} < {cap_dual_after_first}",
);
assert!(
cap_primal_after_second >= cap_primal_after_first,
"primal scratch capacity must not decrease: {cap_primal_after_second} < {cap_primal_after_first}",
);
}
}
#[cfg(test)]
#[allow(clippy::doc_markdown)]
mod research_tests {
unsafe fn research_load_ss11_lp(highs: *mut std::os::raw::c_void) {
use crate::ffi;
let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
let row_lower: [f64; 2] = [6.0, 14.0];
let row_upper: [f64; 2] = [6.0, 14.0];
let a_start: [i32; 4] = [0, 2, 2, 3];
let a_index: [i32; 3] = [0, 1, 1];
let a_value: [f64; 3] = [1.0, 2.0, 1.0];
let status = unsafe {
ffi::cobre_highs_pass_lp(
highs,
3,
2,
3,
ffi::HIGHS_MATRIX_FORMAT_COLWISE,
ffi::HIGHS_OBJ_SENSE_MINIMIZE,
0.0,
col_cost.as_ptr(),
col_lower.as_ptr(),
col_upper.as_ptr(),
row_lower.as_ptr(),
row_upper.as_ptr(),
a_start.as_ptr(),
a_index.as_ptr(),
a_value.as_ptr(),
)
};
assert_eq!(
status,
ffi::HIGHS_STATUS_OK,
"research_load_ss11_lp pass_lp failed"
);
}
#[test]
fn test_research_probe_limit_status_on_ss11_lp() {
use crate::ffi;
let highs = unsafe { ffi::cobre_highs_create() };
assert!(!highs.is_null());
unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
unsafe { research_load_ss11_lp(highs) };
let _ = unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
let run_status = unsafe { ffi::cobre_highs_run(highs) };
let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
eprintln!(
"SS1.1 + time_limit=0: run_status={run_status}, model_status={model_status}, obj={obj}"
);
unsafe { ffi::cobre_highs_destroy(highs) };
let highs = unsafe { ffi::cobre_highs_create() };
assert!(!highs.is_null());
unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
unsafe { research_load_ss11_lp(highs) };
let _ = unsafe {
ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
};
let run_status = unsafe { ffi::cobre_highs_run(highs) };
let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
eprintln!(
"SS1.1 + iteration_limit=0: run_status={run_status}, model_status={model_status}, obj={obj}"
);
unsafe { ffi::cobre_highs_destroy(highs) };
}
unsafe fn research_load_larger_lp(highs: *mut std::os::raw::c_void) {
use crate::ffi;
let col_cost: [f64; 5] = [1.0, 1.0, 1.0, 1.0, 1.0];
let col_lower: [f64; 5] = [0.0; 5];
let col_upper: [f64; 5] = [100.0; 5];
let row_lower: [f64; 4] = [10.0, 8.0, 6.0, 4.0];
let row_upper: [f64; 4] = [f64::INFINITY; 4];
let a_start: [i32; 6] = [0, 1, 3, 5, 7, 8];
let a_index: [i32; 8] = [0, 0, 1, 1, 2, 2, 3, 3];
let a_value: [f64; 8] = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
let status = unsafe {
ffi::cobre_highs_pass_lp(
highs,
5,
4,
8,
ffi::HIGHS_MATRIX_FORMAT_COLWISE,
ffi::HIGHS_OBJ_SENSE_MINIMIZE,
0.0,
col_cost.as_ptr(),
col_lower.as_ptr(),
col_upper.as_ptr(),
row_lower.as_ptr(),
row_upper.as_ptr(),
a_start.as_ptr(),
a_index.as_ptr(),
a_value.as_ptr(),
)
};
assert_eq!(
status,
ffi::HIGHS_STATUS_OK,
"research_load_larger_lp pass_lp failed"
);
}
#[test]
fn test_research_time_limit_zero_triggers_time_limit_status() {
use crate::ffi;
let highs = unsafe { ffi::cobre_highs_create() };
assert!(!highs.is_null());
unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
unsafe { research_load_larger_lp(highs) };
let opt_status =
unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
assert_eq!(opt_status, ffi::HIGHS_STATUS_OK);
let run_status = unsafe { ffi::cobre_highs_run(highs) };
let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
eprintln!(
"time_limit=0 on larger LP: run_status={run_status}, model_status={model_status}"
);
assert_eq!(
run_status,
ffi::HIGHS_STATUS_WARNING,
"time_limit=0 must return HIGHS_STATUS_WARNING (1), got {run_status}"
);
assert_eq!(
model_status,
ffi::HIGHS_MODEL_STATUS_TIME_LIMIT,
"time_limit=0 must give MODEL_STATUS_TIME_LIMIT (13), got {model_status}"
);
unsafe { ffi::cobre_highs_destroy(highs) };
}
#[test]
fn test_research_iteration_limit_zero_triggers_iteration_limit_status() {
use crate::ffi;
let highs = unsafe { ffi::cobre_highs_create() };
assert!(!highs.is_null());
unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
unsafe { ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr()) };
unsafe { research_load_larger_lp(highs) };
let opt_status = unsafe {
ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
};
assert_eq!(opt_status, ffi::HIGHS_STATUS_OK);
let run_status = unsafe { ffi::cobre_highs_run(highs) };
let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
eprintln!(
"iteration_limit=0 on larger LP: run_status={run_status}, model_status={model_status}"
);
assert_eq!(
run_status,
ffi::HIGHS_STATUS_WARNING,
"iteration_limit=0 must return HIGHS_STATUS_WARNING (1), got {run_status}"
);
assert_eq!(
model_status,
ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT,
"iteration_limit=0 must give MODEL_STATUS_ITERATION_LIMIT (14), got {model_status}"
);
unsafe { ffi::cobre_highs_destroy(highs) };
}
#[test]
fn test_research_partial_solution_availability() {
use crate::ffi;
{
let highs = unsafe { ffi::cobre_highs_create() };
assert!(!highs.is_null());
unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
unsafe { research_load_larger_lp(highs) };
unsafe { ffi::cobre_highs_set_double_option(highs, c"time_limit".as_ptr(), 0.0) };
unsafe { ffi::cobre_highs_run(highs) };
let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
assert_eq!(model_status, ffi::HIGHS_MODEL_STATUS_TIME_LIMIT);
eprintln!("TIME_LIMIT: obj={obj}, finite={}", obj.is_finite());
unsafe { ffi::cobre_highs_destroy(highs) };
}
{
let highs = unsafe { ffi::cobre_highs_create() };
assert!(!highs.is_null());
unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
unsafe {
ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr())
};
unsafe { research_load_larger_lp(highs) };
unsafe {
ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0)
};
unsafe { ffi::cobre_highs_run(highs) };
let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
assert_eq!(model_status, ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT);
eprintln!("ITERATION_LIMIT: obj={obj}, finite={}", obj.is_finite());
unsafe { ffi::cobre_highs_destroy(highs) };
}
}
#[test]
fn test_research_restore_defaults_allows_subsequent_optimal_solve() {
use crate::ffi;
let highs = unsafe { ffi::cobre_highs_create() };
assert!(!highs.is_null());
unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
unsafe {
ffi::cobre_highs_set_string_option(highs, c"solver".as_ptr(), c"simplex".as_ptr());
ffi::cobre_highs_set_int_option(highs, c"simplex_strategy".as_ptr(), 1);
ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr());
ffi::cobre_highs_set_string_option(highs, c"parallel".as_ptr(), c"off".as_ptr());
ffi::cobre_highs_set_double_option(
highs,
c"primal_feasibility_tolerance".as_ptr(),
1e-7,
);
ffi::cobre_highs_set_double_option(highs, c"dual_feasibility_tolerance".as_ptr(), 1e-7);
}
let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
let row_lower: [f64; 2] = [6.0, 14.0];
let row_upper: [f64; 2] = [6.0, 14.0];
let a_start: [i32; 4] = [0, 2, 2, 3];
let a_index: [i32; 3] = [0, 1, 1];
let a_value: [f64; 3] = [1.0, 2.0, 1.0];
unsafe {
ffi::cobre_highs_pass_lp(
highs,
3,
2,
3,
ffi::HIGHS_MATRIX_FORMAT_COLWISE,
ffi::HIGHS_OBJ_SENSE_MINIMIZE,
0.0,
col_cost.as_ptr(),
col_lower.as_ptr(),
col_upper.as_ptr(),
row_lower.as_ptr(),
row_upper.as_ptr(),
a_start.as_ptr(),
a_index.as_ptr(),
a_value.as_ptr(),
);
ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 0);
ffi::cobre_highs_run(highs);
}
let status1 = unsafe { ffi::cobre_highs_get_model_status(highs) };
assert_eq!(status1, ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT);
unsafe {
ffi::cobre_highs_set_string_option(highs, c"solver".as_ptr(), c"simplex".as_ptr());
ffi::cobre_highs_set_int_option(highs, c"simplex_strategy".as_ptr(), 1);
ffi::cobre_highs_set_string_option(highs, c"presolve".as_ptr(), c"off".as_ptr());
ffi::cobre_highs_set_double_option(
highs,
c"primal_feasibility_tolerance".as_ptr(),
1e-7,
);
ffi::cobre_highs_set_double_option(highs, c"dual_feasibility_tolerance".as_ptr(), 1e-7);
ffi::cobre_highs_set_string_option(highs, c"parallel".as_ptr(), c"off".as_ptr());
ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0);
ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), i32::MAX);
}
unsafe { ffi::cobre_highs_clear_solver(highs) };
unsafe { ffi::cobre_highs_run(highs) };
let status2 = unsafe { ffi::cobre_highs_get_model_status(highs) };
let obj = unsafe { ffi::cobre_highs_get_objective_value(highs) };
assert_eq!(
status2,
ffi::HIGHS_MODEL_STATUS_OPTIMAL,
"after restoring defaults, second solve must be OPTIMAL, got {status2}"
);
assert!(
(obj - 100.0).abs() < 1e-8,
"objective after restore must be 100.0, got {obj}"
);
unsafe { ffi::cobre_highs_destroy(highs) };
}
#[test]
fn test_research_iteration_limit_one_triggers_iteration_limit_status() {
use crate::ffi;
let highs = unsafe { ffi::cobre_highs_create() };
assert!(!highs.is_null());
unsafe { ffi::cobre_highs_set_bool_option(highs, c"output_flag".as_ptr(), 0) };
let col_cost: [f64; 3] = [0.0, 1.0, 50.0];
let col_lower: [f64; 3] = [0.0, 0.0, 0.0];
let col_upper: [f64; 3] = [10.0, f64::INFINITY, 8.0];
let row_lower: [f64; 2] = [6.0, 14.0];
let row_upper: [f64; 2] = [6.0, 14.0];
let a_start: [i32; 4] = [0, 2, 2, 3];
let a_index: [i32; 3] = [0, 1, 1];
let a_value: [f64; 3] = [1.0, 2.0, 1.0];
unsafe {
ffi::cobre_highs_pass_lp(
highs,
3,
2,
3,
ffi::HIGHS_MATRIX_FORMAT_COLWISE,
ffi::HIGHS_OBJ_SENSE_MINIMIZE,
0.0,
col_cost.as_ptr(),
col_lower.as_ptr(),
col_upper.as_ptr(),
row_lower.as_ptr(),
row_upper.as_ptr(),
a_start.as_ptr(),
a_index.as_ptr(),
a_value.as_ptr(),
);
ffi::cobre_highs_set_int_option(highs, c"simplex_iteration_limit".as_ptr(), 1);
ffi::cobre_highs_run(highs);
}
let model_status = unsafe { ffi::cobre_highs_get_model_status(highs) };
eprintln!("iteration_limit=1 model_status: {model_status}");
assert!(
model_status == ffi::HIGHS_MODEL_STATUS_ITERATION_LIMIT
|| model_status == ffi::HIGHS_MODEL_STATUS_OPTIMAL,
"expected ITERATION_LIMIT or OPTIMAL, got {model_status}"
);
unsafe { ffi::cobre_highs_destroy(highs) };
}
#[test]
fn test_research_verify_non_optimal_highs_status_mapping() {
use super::super::HighsSolver;
use crate::SolverInterface;
use crate::types::SolverError;
use crate::types::StageTemplate;
let unbounded_template = StageTemplate {
num_cols: 2,
num_rows: 1,
num_nz: 2,
col_starts: vec![0_i32, 1, 2],
row_indices: vec![0_i32, 0],
values: vec![1.0, 1.0],
col_lower: vec![0.0, 0.0],
col_upper: vec![f64::INFINITY, f64::INFINITY],
objective: vec![-1.0, -1.0],
row_lower: vec![1.0],
row_upper: vec![f64::INFINITY],
n_state: 1,
n_transfer: 0,
n_dual_relevant: 1,
n_hydro: 0,
max_par_order: 0,
col_scale: Vec::new(),
row_scale: Vec::new(),
};
let mut solver_unb = HighsSolver::new().expect("HighsSolver::new() must succeed");
solver_unb.load_model(&unbounded_template);
let result_unb = solver_unb.solve(None).map(|_| ());
assert!(
matches!(result_unb, Err(SolverError::Unbounded)),
"unbounded LP must return Err(SolverError::Unbounded), got {result_unb:?}"
);
let infeasible_template = StageTemplate {
num_cols: 1,
num_rows: 1,
num_nz: 1,
col_starts: vec![0_i32, 1],
row_indices: vec![0_i32],
values: vec![1.0],
col_lower: vec![0.0],
col_upper: vec![10.0],
objective: vec![0.0],
row_lower: vec![99.0],
row_upper: vec![99.0],
n_state: 1,
n_transfer: 0,
n_dual_relevant: 1,
n_hydro: 0,
max_par_order: 0,
col_scale: Vec::new(),
row_scale: Vec::new(),
};
let mut solver_inf = HighsSolver::new().expect("HighsSolver::new() must succeed");
solver_inf.load_model(&infeasible_template);
let result_inf = solver_inf.solve(None).map(|_| ());
assert!(
matches!(result_inf, Err(SolverError::Infeasible)),
"infeasible LP must return Err(SolverError::Infeasible), got {result_inf:?}"
);
}
}