use std::os::raw::c_void;
use std::time::Instant;
use super::config::ClpProfile;
use crate::{
DEFAULT_PROFILE_HEURISTIC_SENTINEL, clp_ffi,
types::{SolutionView, SolverError, SolverStatistics},
};
const CLP_BASIS_AT_LOWER: i32 = 3;
const CLP_BASIS_BASIC: i32 = 1;
pub struct ClpSolver {
pub(super) handle: *mut c_void,
pub(super) col_value: Vec<f64>,
pub(super) col_dual: Vec<f64>,
pub(super) row_dual: Vec<f64>,
pub(super) col_starts: Vec<i32>,
pub(super) row_indices: Vec<i32>,
pub(super) values: Vec<f64>,
pub(super) col_lower: Vec<f64>,
pub(super) col_upper: Vec<f64>,
pub(super) row_lower: Vec<f64>,
pub(super) row_upper: Vec<f64>,
pub(super) num_nz: usize,
pub(super) num_cols: usize,
pub(super) num_rows: usize,
pub(super) has_model: bool,
pub(super) stats: SolverStatistics,
pub(super) current_profile: ClpProfile,
pub(super) hot_start_token: *mut c_void,
}
unsafe impl Send for ClpSolver {}
impl ClpSolver {
pub fn new() -> Result<Self, SolverError> {
let handle = unsafe { clp_ffi::cobre_clp_create() };
if handle.is_null() {
return Err(SolverError::InternalError {
message: "CLP instance creation failed: Clp_newModel() returned null".to_string(),
error_code: None,
});
}
unsafe { clp_ffi::cobre_clp_set_log_level(handle, 0) };
Ok(Self {
handle,
col_value: Vec::new(),
col_dual: Vec::new(),
row_dual: Vec::new(),
col_starts: Vec::new(),
row_indices: Vec::new(),
values: Vec::new(),
col_lower: Vec::new(),
col_upper: Vec::new(),
row_lower: Vec::new(),
row_upper: Vec::new(),
num_nz: 0,
num_cols: 0,
num_rows: 0,
has_model: false,
stats: SolverStatistics::default(),
current_profile: ClpProfile::default(),
hot_start_token: std::ptr::null_mut(),
})
}
pub(super) fn copy_solution(&mut self) {
if self.num_cols > 0 {
let primal = unsafe {
let ptr = clp_ffi::cobre_clp_get_col_solution(self.handle);
std::slice::from_raw_parts(ptr, self.num_cols)
};
self.col_value.copy_from_slice(primal);
let reduced = unsafe {
let ptr = clp_ffi::cobre_clp_get_reduced_cost(self.handle);
std::slice::from_raw_parts(ptr, self.num_cols)
};
self.col_dual.copy_from_slice(reduced);
}
if self.num_rows > 0 {
let row_price = unsafe {
let ptr = clp_ffi::cobre_clp_get_row_price(self.handle);
std::slice::from_raw_parts(ptr, self.num_rows)
};
for (dst, &raw) in self.row_dual.iter_mut().zip(row_price) {
*dst = normalize_row_dual(raw);
}
}
}
pub(super) fn install_basis(&mut self, b: &crate::Basis) -> Result<(), SolverError> {
assert!(
b.col_status.len() == self.num_cols,
"basis column count {} does not match LP column count {}",
b.col_status.len(),
self.num_cols
);
if b.row_status.len() < self.num_rows {
self.stats.basis_consistency_failures += 1;
return Err(SolverError::BasisRowCountMismatch {
lp_rows: self.num_rows,
basis_rows: b.row_status.len(),
});
}
self.stats.basis_offered += 1;
let row_copy_len = b.row_status.len().min(self.num_rows);
let basis_set_start = Instant::now();
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
for c in 0..self.num_cols {
unsafe {
clp_ffi::cobre_clp_set_column_status(self.handle, c as i32, b.col_status[c]);
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
for r in 0..row_copy_len {
unsafe {
clp_ffi::cobre_clp_set_row_status(self.handle, r as i32, b.row_status[r]);
}
}
self.stats.total_basis_set_time_seconds += basis_set_start.elapsed().as_secs_f64();
Ok(())
}
pub(super) fn reset_cold_basis(&mut self) {
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
for c in 0..self.num_cols {
unsafe {
clp_ffi::cobre_clp_set_column_status(self.handle, c as i32, CLP_BASIS_AT_LOWER);
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
for r in 0..self.num_rows {
unsafe {
clp_ffi::cobre_clp_set_row_status(self.handle, r as i32, CLP_BASIS_BASIC);
}
}
}
pub(super) fn resolve_simplex_cap(&self) -> i32 {
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
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
}
}
pub(super) fn set_dual_row_steepest(&mut self, mode: i32) {
unsafe {
clp_ffi::cobre_clp_set_dual_row_steepest(self.handle, mode);
}
}
pub fn mark_hot_start(&mut self) {
assert!(
self.has_model,
"mark_hot_start called without a loaded model — call load_model first"
);
if !self.hot_start_token.is_null() {
self.unmark_hot_start();
}
let token = unsafe { clp_ffi::cobre_clp_mark_hot_start(self.handle) };
debug_assert!(
!token.is_null(),
"markHotStart returned a null saveStuff token"
);
self.hot_start_token = token;
}
pub fn solve_from_hot_start(&mut self) -> Result<SolutionView<'_>, SolverError> {
assert!(
!self.hot_start_token.is_null(),
"solve_from_hot_start called without an active snapshot — call mark_hot_start first"
);
let t0 = Instant::now();
let status =
unsafe { clp_ffi::cobre_clp_solve_from_hot_start(self.handle, self.hot_start_token) };
let solve_time = t0.elapsed().as_secs_f64();
self.stats.solve_count += 1;
if status == clp_ffi::CLP_STATUS_OPTIMAL {
#[allow(clippy::cast_sign_loss)]
let iterations = unsafe { clp_ffi::cobre_clp_number_iterations(self.handle) } as u64;
let objective = unsafe { clp_ffi::cobre_clp_objective_value(self.handle) };
self.copy_solution();
self.stats.success_count += 1;
self.stats.first_try_successes += 1;
self.stats.total_iterations += iterations;
self.stats.total_solve_time_seconds += solve_time;
return Ok(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: solve_time,
});
}
self.stats.failure_count += 1;
match status {
clp_ffi::CLP_STATUS_PRIMAL_INFEASIBLE => Err(SolverError::Infeasible),
clp_ffi::CLP_STATUS_DUAL_INFEASIBLE => Err(SolverError::Unbounded),
clp_ffi::CLP_STATUS_STOPPED => {
#[allow(clippy::cast_sign_loss)]
let iterations =
unsafe { clp_ffi::cobre_clp_number_iterations(self.handle) } as u64;
Err(SolverError::IterationLimit { iterations })
}
clp_ffi::CLP_STATUS_ERRORS => Err(SolverError::InternalError {
message: "CLP hot-start solve failed (simplex returned ERRORS status)".to_string(),
error_code: Some(4),
}),
other => Err(SolverError::InternalError {
message: format!("CLP hot-start returned unexpected status {other}"),
error_code: Some(other),
}),
}
}
pub fn unmark_hot_start(&mut self) {
if self.hot_start_token.is_null() {
return;
}
unsafe {
clp_ffi::cobre_clp_unmark_hot_start(self.handle, self.hot_start_token);
}
self.hot_start_token = std::ptr::null_mut();
}
}
const fn normalize_row_dual(raw: f64) -> f64 {
raw
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
pub(super) fn i32_from_usize(v: usize) -> i32 {
debug_assert!(
i32::try_from(v).is_ok(),
"value {v} overflows i32: LP exceeds CLP API limit"
);
v as i32
}
impl Drop for ClpSolver {
fn drop(&mut self) {
self.unmark_hot_start();
unsafe { clp_ffi::cobre_clp_destroy(self.handle) };
}
}
#[must_use]
pub fn clp_version() -> String {
let major = unsafe { clp_ffi::cobre_clp_version_major() };
let minor = unsafe { clp_ffi::cobre_clp_version_minor() };
let patch = unsafe { clp_ffi::cobre_clp_version_release() };
format!("{major}.{minor}.{patch}")
}