#![cfg_attr(
test,
allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::float_cmp,
clippy::too_many_lines,
clippy::panic
)
)]
use cobre_solver::{RowBatch, StageTemplate};
#[cfg(any(feature = "highs", feature = "clp"))]
use cobre_solver::{Basis, SolverInterface};
#[cfg(feature = "highs")]
use cobre_solver::{HighsSolver, SolutionView, SolverError};
#[cfg(feature = "clp")]
use cobre_solver::ClpSolver;
#[cfg(all(feature = "test-support", feature = "highs"))]
use cobre_solver::test_support;
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],
}
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_load_model_and_solve() {
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 feasible LP");
let obj = solution.objective;
assert!(
(obj - 100.0).abs() < 1e-8,
"expected objective = 100.0, got {obj}"
);
let primals = &solution.primal;
assert!(
(primals[0] - 6.0).abs() < 1e-8,
"expected x0 = 6.0, got {}",
primals[0]
);
assert!(
(primals[1] - 0.0).abs() < 1e-8,
"expected x1 = 0.0, got {}",
primals[1]
);
assert!(
(primals[2] - 2.0).abs() < 1e-8,
"expected x2 = 2.0, got {}",
primals[2]
);
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_load_model_replaces_previous() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let template = make_fixture_stage_template();
solver.load_model(&template);
let obj1 = solver
.solve(None)
.expect("first solve() must succeed")
.objective;
assert!(
(obj1 - 100.0).abs() < 1e-8,
"expected first objective = 100.0, got {obj1}"
);
let mut modified = make_fixture_stage_template();
modified.objective = vec![0.0, 1.0, 25.0];
solver.load_model(&modified);
let obj2 = solver
.solve(None)
.expect("second solve() must succeed")
.objective;
assert!(
(obj2 - 50.0).abs() < 1e-8,
"expected second objective = 50.0, got {obj2}"
);
}
#[test]
fn test_fixture_stage_template_data() {
let t = make_fixture_stage_template();
assert_eq!(t.num_cols, 3);
assert_eq!(t.num_rows, 2);
assert_eq!(t.num_nz, 3);
assert_eq!(t.col_starts, vec![0_i32, 2, 2, 3]);
assert_eq!(t.row_indices, vec![0_i32, 1, 1]);
assert_eq!(t.values, vec![1.0, 2.0, 1.0]);
assert_eq!(t.col_lower, vec![0.0, 0.0, 0.0]);
assert_eq!(t.col_upper[0], 10.0);
assert!(t.col_upper[1].is_infinite() && t.col_upper[1].is_sign_positive());
assert_eq!(t.col_upper[2], 8.0);
assert_eq!(t.objective, vec![0.0, 1.0, 50.0]);
assert_eq!(t.row_lower, vec![6.0, 14.0]);
assert_eq!(t.row_upper, vec![6.0, 14.0]);
assert_eq!(t.n_state, 1);
assert_eq!(t.n_transfer, 0);
assert_eq!(t.n_dual_relevant, 1);
assert_eq!(t.n_hydro, 1);
assert_eq!(t.max_par_order, 0);
}
#[test]
fn test_fixture_row_batch_data() {
let b = make_fixture_row_batch();
assert_eq!(b.num_rows, 2);
assert_eq!(b.row_starts, vec![0_i32, 2, 4]);
assert_eq!(b.col_indices, vec![0_i32, 1, 0, 1]);
assert_eq!(b.values, vec![-5.0, 1.0, 3.0, 1.0]);
assert_eq!(b.row_lower, vec![20.0, 80.0]);
assert!(b.row_upper[0].is_infinite() && b.row_upper[0].is_sign_positive());
assert!(b.row_upper[1].is_infinite() && b.row_upper[1].is_sign_positive());
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_add_rows_tightens() {
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 after adding both cuts");
assert!(
(solution.objective - 162.0).abs() < 1e-8,
"expected objective = 162.0, got {}",
solution.objective
);
let primals = &solution.primal;
assert_eq!(primals.len(), 3);
assert!(
(primals[0] - 6.0).abs() < 1e-8
&& (primals[1] - 62.0).abs() < 1e-8
&& (primals[2] - 2.0).abs() < 1e-8,
"expected [6.0, 62.0, 2.0], got [{}, {}, {}]",
primals[0],
primals[1],
primals[2]
);
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_add_rows_single_cut() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let template = make_fixture_stage_template();
let single_cut = RowBatch {
num_rows: 1,
row_starts: vec![0_i32, 2],
col_indices: vec![0_i32, 1],
values: vec![-5.0, 1.0],
row_lower: vec![20.0],
row_upper: vec![f64::INFINITY],
};
solver.load_model(&template);
solver.add_rows(&single_cut);
let solution = solver
.solve(None)
.expect("solve() must succeed after adding single cut");
let obj = solution.objective;
assert!(
(obj - 150.0).abs() < 1e-8,
"expected objective = 150.0, got {obj}"
);
let primals = &solution.primal;
assert!(
(primals[1] - 50.0).abs() < 1e-8,
"expected x1 = 50.0, got {}",
primals[1]
);
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_set_row_bounds_state_change() {
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 patching row bounds");
let obj = solution.objective;
assert!(
(obj - 368.0).abs() < 1e-8,
"expected objective = 368.0, got {obj}"
);
let primals = &solution.primal;
assert!(
(primals[0] - 4.0).abs() < 1e-8,
"expected x0 = 4.0, got {}",
primals[0]
);
assert!(
(primals[1] - 68.0).abs() < 1e-8,
"expected x1 = 68.0, got {}",
primals[1]
);
assert!(
(primals[2] - 6.0).abs() < 1e-8,
"expected x2 = 6.0, got {}",
primals[2]
);
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_set_col_bounds_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.add_rows(&cuts);
solver.set_col_bounds(&[2], &[0.0], &[3.0]);
let solution = solver
.solve(None)
.expect("solve() must succeed after tightening col 2 bounds");
let obj = solution.objective;
assert!(
(obj - 162.0).abs() < 1e-8,
"expected objective = 162.0 (tighter bound does not bind), got {obj}"
);
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_set_col_bounds_tightens() {
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]);
let solution = solver
.solve(None)
.expect("solve() must succeed after patching col 1 lower bound");
let obj = solution.objective;
assert!(
(obj - 110.0).abs() < 1e-8,
"expected objective = 110.0, got {obj}"
);
let primals = &solution.primal;
assert!(
(primals[1] - 10.0).abs() < 1e-8,
"expected x1 = 10.0, got {}",
primals[1]
);
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_set_col_bounds_repatch() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let template = make_fixture_stage_template();
solver.load_model(&template);
let obj1 = solver
.solve(None)
.expect("first solve() must succeed with original bounds")
.objective;
assert!(
(obj1 - 100.0).abs() < 1e-8,
"expected first objective = 100.0, got {obj1}"
);
solver.set_col_bounds(&[1], &[10.0], &[f64::INFINITY]);
let obj2 = solver
.solve(None)
.expect("second solve() must succeed after tightening col 1")
.objective;
assert!(
(obj2 - 110.0).abs() < 1e-8,
"expected second objective = 110.0, got {obj2}"
);
solver.set_col_bounds(&[1], &[0.0], &[f64::INFINITY]);
let obj3 = solver
.solve(None)
.expect("third solve() must succeed after restoring col 1 bounds")
.objective;
assert!(
(obj3 - 100.0).abs() < 1e-8,
"expected third objective = 100.0 (bounds restored), got {obj3}"
);
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_solve_dual_values() {
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 feasible LP");
assert_eq!(
solution.dual.len(),
2,
"expected dual.len() = 2, got {}",
solution.dual.len()
);
let pi_0 = solution.dual[0];
assert!(
(pi_0 - (-100.0)).abs() < 1e-6,
"expected dual[0] = -100.0, got {pi_0}"
);
let pi_1 = solution.dual[1];
assert!(
(pi_1 - 50.0).abs() < 1e-6,
"expected dual[1] = 50.0, got {pi_1}"
);
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_solve_dual_values_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 after adding both cuts");
assert_eq!(
solution.dual.len(),
4,
"expected dual.len() = 4, got {}",
solution.dual.len()
);
let expected = [-103.0_f64, 50.0, 0.0, 1.0];
for (i, &expected_pi) in expected.iter().enumerate() {
let actual_pi = solution.dual[i];
assert!(
(actual_pi - expected_pi).abs() < 1e-6,
"expected dual[{i}] = {expected_pi}, got {actual_pi}"
);
}
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_solve_reduced_costs() {
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 feasible LP");
assert_eq!(
solution.reduced_costs.len(),
3,
"expected reduced_costs.len() = 3, got {}",
solution.reduced_costs.len()
);
let rc_x1 = solution.reduced_costs[1];
assert!(
(rc_x1 - 1.0).abs() < 1e-6,
"expected reduced_costs[1] = 1.0, got {rc_x1}"
);
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_solve_iterations_reported() {
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 feasible LP");
assert!(
solution.solve_time_seconds >= 0.0,
"expected solve_time_seconds >= 0.0, got {}",
solution.solve_time_seconds
);
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_dual_normalization_cut_relevant_row() {
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 feasible LP");
let pi_0 = solution.dual[0];
assert!(
(pi_0 - (-100.0)).abs() < 1e-6,
"expected cut-relevant dual[0] = -100.0, got {pi_0}"
);
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_dual_normalization_sensitivity_check() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let template = make_fixture_stage_template();
solver.load_model(&template);
let z_original = solver
.solve(None)
.expect("first solve() must succeed on original fixture")
.objective;
assert!(
(z_original - 100.0).abs() < 1e-8,
"expected original objective = 100.0, got {z_original}"
);
solver.set_row_bounds(&[0], &[6.01], &[6.01]);
let z_perturbed = solver
.solve(None)
.expect("second solve() must succeed after patching Row 0 RHS")
.objective;
let finite_diff = (z_perturbed - z_original) / 0.01;
assert!(
(finite_diff - (-100.0)).abs() < 1e-2,
"expected finite_diff = -100.0, got {finite_diff}"
);
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_dual_normalization_with_binding_cut() {
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 after adding both cuts");
let pi_3 = solution.dual[3];
assert!(
(pi_3 - 1.0).abs() < 1e-6,
"expected binding cut dual[3] = 1.0, got {pi_3}"
);
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_statistics_are_cumulative() {
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, "expected 2 solves");
assert_eq!(stats.success_count, 2, "expected 2 successes");
solver.load_model(&template);
solver.solve(None).expect("third solve() must succeed");
let stats_after = solver.statistics();
assert_eq!(
stats_after.solve_count, 3,
"solve_count must accumulate across load_model calls"
);
assert_eq!(
stats_after.success_count, 3,
"success_count must accumulate across load_model calls"
);
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_statistics_initial() {
let solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let stats = solver.statistics();
assert_eq!(
stats.solve_count, 0,
"expected solve_count = 0 on fresh solver"
);
assert_eq!(
stats.success_count, 0,
"expected success_count = 0 on fresh solver"
);
assert_eq!(
stats.failure_count, 0,
"expected failure_count = 0 on fresh solver"
);
assert_eq!(
stats.total_iterations, 0,
"expected total_iterations = 0 on fresh solver"
);
assert_eq!(
stats.retry_count, 0,
"expected retry_count = 0 on fresh solver"
);
assert_eq!(
stats.total_solve_time_seconds, 0.0,
"expected total_solve_time_seconds = 0.0 on fresh solver"
);
assert_eq!(
stats.basis_consistency_failures, 0,
"expected basis_consistency_failures = 0 on fresh solver"
);
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_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");
solver.solve(None).expect("third solve() must succeed");
let stats = solver.statistics();
assert_eq!(
stats.solve_count, 3,
"expected solve_count = 3 after three solves, got {}",
stats.solve_count
);
assert_eq!(
stats.success_count, 3,
"expected success_count = 3 after three successful solves, got {}",
stats.success_count
);
assert_eq!(
stats.failure_count, 0,
"expected failure_count = 0, got {}",
stats.failure_count
);
assert!(
stats.total_solve_time_seconds > 0.0,
"expected total_solve_time_seconds > 0.0, got {}",
stats.total_solve_time_seconds
);
assert_eq!(
stats.basis_consistency_failures, 0,
"expected basis_consistency_failures = 0 after cold solves, got {}",
stats.basis_consistency_failures
);
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_name_returns_identifier() {
let solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
let name = solver.name();
assert_eq!(name, "HiGHS", "expected name = \"HiGHS\", got \"{name}\"");
assert!(!name.is_empty(), "name must be non-empty");
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_lifecycle_repeated_patch_solve() {
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 obj1 = solver
.solve(None)
.expect("step 2 solve() must succeed with base fixture + cuts")
.objective;
assert!(
(obj1 - 162.0).abs() < 1e-8,
"step 2: expected objective = 162.0, got {obj1}"
);
solver.set_row_bounds(&[0], &[4.0], &[4.0]);
let obj2 = solver
.solve(None)
.expect("step 3 solve() must succeed with x0=4.0")
.objective;
assert!(
(obj2 - 368.0).abs() < 1e-8,
"step 3: expected objective = 368.0, got {obj2}"
);
solver.set_row_bounds(&[0], &[8.0], &[8.0]);
let result = solver.solve(None);
assert!(
matches!(result, Err(cobre_solver::SolverError::Infeasible)),
"step 4: expected Err(SolverError::Infeasible), got {:?}",
result.map(|s| s.objective)
);
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_solve_infeasible() {
let infeasible_template = StageTemplate {
num_cols: 1,
num_rows: 0,
num_nz: 0,
col_starts: vec![0_i32, 0],
row_indices: vec![],
values: vec![],
col_lower: vec![5.0],
col_upper: vec![3.0],
objective: vec![1.0],
row_lower: vec![],
row_upper: vec![],
n_state: 1,
n_transfer: 0,
n_dual_relevant: 0,
n_hydro: 0,
max_par_order: 0,
col_scale: Vec::new(),
row_scale: Vec::new(),
};
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
solver.load_model(&infeasible_template);
let result = solver.solve(None).map(|s| s.objective);
assert!(
matches!(result, Err(cobre_solver::SolverError::Infeasible)),
"expected Err(SolverError::Infeasible), got {result:?}"
);
let stats = solver.statistics();
assert_eq!(
stats.solve_count, 1,
"expected solve_count = 1 after infeasible solve, got {}",
stats.solve_count
);
assert_eq!(
stats.failure_count, 1,
"expected failure_count = 1 after infeasible solve, got {}",
stats.failure_count
);
assert_eq!(
stats.success_count, 0,
"expected success_count = 0 after infeasible solve, got {}",
stats.success_count
);
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_solve_unbounded() {
let unbounded_template = StageTemplate {
num_cols: 1,
num_rows: 0,
num_nz: 0,
col_starts: vec![0_i32, 0],
row_indices: vec![],
values: vec![],
col_lower: vec![f64::NEG_INFINITY],
col_upper: vec![f64::INFINITY],
objective: vec![-1.0],
row_lower: vec![],
row_upper: vec![],
n_state: 1,
n_transfer: 0,
n_dual_relevant: 0,
n_hydro: 0,
max_par_order: 0,
col_scale: Vec::new(),
row_scale: Vec::new(),
};
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
solver.load_model(&unbounded_template);
let result = solver.solve(None).map(|s| s.objective);
assert!(
matches!(result, Err(cobre_solver::SolverError::Unbounded)),
"expected Err(SolverError::Unbounded), got {result:?}"
);
let stats = solver.statistics();
assert_eq!(
stats.solve_count, 1,
"expected solve_count = 1 after unbounded solve, got {}",
stats.solve_count
);
assert_eq!(
stats.failure_count, 1,
"expected failure_count = 1 after unbounded solve, got {}",
stats.failure_count
);
assert_eq!(
stats.success_count, 0,
"expected success_count = 0 after unbounded solve, got {}",
stats.success_count
);
}
#[cfg(feature = "highs")]
#[allow(dead_code)]
fn make_larger_lp_template() -> StageTemplate {
StageTemplate {
num_cols: 5,
num_rows: 4,
num_nz: 8,
col_starts: vec![0_i32, 1, 3, 5, 7, 8],
row_indices: vec![0_i32, 0, 1, 1, 2, 2, 3, 3],
values: vec![1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
col_lower: vec![0.0, 0.0, 0.0, 0.0, 0.0],
col_upper: vec![100.0, 100.0, 100.0, 100.0, 100.0],
objective: vec![1.0, 1.0, 1.0, 1.0, 1.0],
row_lower: vec![10.0, 8.0, 6.0, 4.0],
row_upper: vec![f64::INFINITY, f64::INFINITY, f64::INFINITY, 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(),
}
}
#[cfg(all(feature = "highs", feature = "test-support"))]
#[test]
fn test_solver_highs_solve_time_limit() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
solver.load_model(&make_larger_lp_template());
unsafe {
test_support::cobre_highs_set_double_option(
solver.raw_handle(),
c"time_limit".as_ptr(),
0.0,
);
}
let result = solver.solve(None);
assert!(result.is_err(), "time_limit=0 must exhaust all retries");
let stats = solver.statistics();
assert_eq!(stats.solve_count, 1);
assert_eq!(stats.failure_count, 1);
assert!(
stats.retry_count > 0,
"retry escalation must have been attempted"
);
}
#[cfg(all(feature = "highs", feature = "test-support"))]
#[test]
fn test_solver_highs_solve_iteration_limit() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
solver.load_model(&make_larger_lp_template());
unsafe {
test_support::cobre_highs_set_string_option(
solver.raw_handle(),
c"presolve".as_ptr(),
c"off".as_ptr(),
);
test_support::cobre_highs_set_int_option(
solver.raw_handle(),
c"simplex_iteration_limit".as_ptr(),
0,
);
}
let result = solver.solve(None);
assert!(
result.is_ok(),
"internal safeguard limits must override external simplex_iteration_limit=0"
);
let stats = solver.statistics();
assert_eq!(stats.solve_count, 1);
assert_eq!(stats.success_count, 1);
}
#[cfg(all(feature = "highs", feature = "test-support"))]
#[test]
fn test_solver_highs_restore_defaults_after_limit() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
solver.load_model(&make_larger_lp_template());
unsafe {
test_support::cobre_highs_set_int_option(
solver.raw_handle(),
c"simplex_iteration_limit".as_ptr(),
0,
);
}
assert!(
solver.solve(None).is_ok(),
"internal safeguards must override external simplex_iteration_limit=0"
);
solver.load_model(&make_fixture_stage_template());
let objective = solver
.solve(None)
.expect("solve() must succeed after reset")
.objective;
assert!((objective - 100.0).abs() < 1e-8);
let stats = solver.statistics();
assert_eq!(stats.solve_count, 2);
assert_eq!(stats.success_count, 2);
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_infeasible_with_rows() {
let infeasible_with_rows = StageTemplate {
num_cols: 2,
num_rows: 2,
num_nz: 4,
col_starts: vec![0_i32, 2, 4],
row_indices: vec![0_i32, 1, 0, 1],
values: vec![1.0, 1.0, 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![10.0, f64::NEG_INFINITY],
row_upper: vec![f64::INFINITY, 5.0],
n_state: 1,
n_transfer: 0,
n_dual_relevant: 2,
n_hydro: 0,
max_par_order: 0,
col_scale: Vec::new(),
row_scale: Vec::new(),
};
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
solver.load_model(&infeasible_with_rows);
let result = solver.solve(None);
assert!(
matches!(result, Err(SolverError::Infeasible)),
"expected Err(SolverError::Infeasible), got {:?}",
result.map(|_| ())
);
}
#[cfg(all(feature = "highs", feature = "test-support"))]
#[test]
fn test_solver_highs_infeasible_with_presolve() {
let infeasible_with_rows = StageTemplate {
num_cols: 2,
num_rows: 2,
num_nz: 4,
col_starts: vec![0_i32, 2, 4],
row_indices: vec![0_i32, 1, 0, 1],
values: vec![1.0, 1.0, 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![10.0, f64::NEG_INFINITY],
row_upper: vec![f64::INFINITY, 5.0],
n_state: 1,
n_transfer: 0,
n_dual_relevant: 2,
n_hydro: 0,
max_par_order: 0,
col_scale: Vec::new(),
row_scale: Vec::new(),
};
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
unsafe {
test_support::cobre_highs_set_string_option(
solver.raw_handle(),
c"presolve".as_ptr(),
c"on".as_ptr(),
);
}
solver.load_model(&infeasible_with_rows);
let result = solver.solve(None).map(|s| s.objective);
assert!(
matches!(result, Err(SolverError::Infeasible)),
"expected Err(SolverError::Infeasible), got {result:?}"
);
}
#[cfg(feature = "highs")]
#[test]
fn test_solver_highs_unbounded_with_primal_ray() {
let unbounded_with_rows = StageTemplate {
num_cols: 2,
num_rows: 1,
num_nz: 1,
col_starts: vec![0_i32, 1, 1],
row_indices: vec![0_i32],
values: vec![1.0],
col_lower: vec![0.0, f64::NEG_INFINITY],
col_upper: vec![f64::INFINITY, f64::INFINITY],
objective: vec![0.0, -1.0],
row_lower: vec![f64::NEG_INFINITY],
row_upper: vec![10.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 = HighsSolver::new().expect("HighsSolver::new() must succeed");
solver.load_model(&unbounded_with_rows);
let result = solver.solve(None);
assert!(
matches!(result, Err(SolverError::Unbounded)),
"expected Err(SolverError::Unbounded), got {:?}",
result.map(|_| ())
);
}
#[cfg(all(feature = "highs", feature = "test-support"))]
#[test]
fn test_solver_highs_unbounded_or_infeasible() {
let ambiguous_template = StageTemplate {
num_cols: 2,
num_rows: 2,
num_nz: 2,
col_starts: vec![0_i32, 2, 2],
row_indices: vec![0_i32, 1],
values: vec![1.0, 1.0],
col_lower: vec![0.0, f64::NEG_INFINITY],
col_upper: vec![f64::INFINITY, f64::INFINITY],
objective: vec![0.0, -1.0],
row_lower: vec![10.0, f64::NEG_INFINITY],
row_upper: vec![f64::INFINITY, 5.0],
n_state: 1,
n_transfer: 0,
n_dual_relevant: 2,
n_hydro: 0,
max_par_order: 0,
col_scale: Vec::new(),
row_scale: Vec::new(),
};
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
unsafe {
test_support::cobre_highs_set_string_option(
solver.raw_handle(),
c"presolve".as_ptr(),
c"on".as_ptr(),
);
}
solver.load_model(&ambiguous_template);
let result = solver.solve(None).map(|s| s.objective);
match &result {
Err(SolverError::Infeasible | SolverError::Unbounded) => {
}
other => panic!("expected Infeasible or Unbounded error, got {other:?}"),
}
}
#[cfg(feature = "highs")]
#[test]
fn solve_equals_solve_owned() {
let mut solver_a = HighsSolver::new().expect("HighsSolver::new() must succeed");
solver_a.load_model(&make_fixture_stage_template());
let owned = solver_a
.solve(None)
.expect("solve() must succeed")
.to_owned();
let mut solver_b = HighsSolver::new().expect("HighsSolver::new() must succeed");
solver_b.load_model(&make_fixture_stage_template());
let view = solver_b.solve(None).expect("solve() must succeed");
let from_view = view.to_owned();
assert_eq!(
owned.objective, from_view.objective,
"objectives must be bitwise equal"
);
assert_eq!(
owned.primal, from_view.primal,
"primals must be bitwise equal"
);
assert_eq!(owned.dual, from_view.dual, "duals must be bitwise equal");
assert_eq!(
owned.reduced_costs, from_view.reduced_costs,
"reduced_costs must be bitwise equal"
);
assert_eq!(
owned.iterations, from_view.iterations,
"iterations must match"
);
}
#[cfg(feature = "highs")]
#[test]
fn solve_borrows_internal_buffers() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
solver.load_model(&make_fixture_stage_template());
let (obj1, primal0_first) = {
let view = solver.solve(None).expect("first solve() must succeed");
(view.objective, view.primal[0])
};
let view2 = solver.solve(None).expect("second solve() must succeed");
assert_eq!(
obj1, view2.objective,
"objective must be identical on both calls"
);
assert_eq!(
primal0_first, view2.primal[0],
"primal[0] must be identical on both calls"
);
}
#[cfg(feature = "highs")]
#[test]
fn solve_after_add_rows() {
let template = make_fixture_stage_template();
let cuts = make_fixture_row_batch();
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
solver.load_model(&template);
solver.add_rows(&cuts);
let view = solver
.solve(None)
.expect("solve() after add_rows must succeed");
assert!(
(view.objective - 162.0).abs() < 1e-8,
"objective must be 162.0 after adding Benders cuts, got {}",
view.objective
);
assert_eq!(
view.dual.len(),
template.num_rows + cuts.num_rows,
"dual length must equal template.num_rows ({}) + cuts.num_rows ({}) = {}",
template.num_rows,
cuts.num_rows,
template.num_rows + cuts.num_rows,
);
}
#[cfg(feature = "highs")]
#[test]
fn solve_statistics_updated() {
let mut solver = HighsSolver::new().expect("HighsSolver::new() must succeed");
solver.load_model(&make_fixture_stage_template());
let _view: SolutionView<'_> = 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 call"
);
assert_eq!(
stats.success_count, 1,
"success_count must be 1 after a successful solve"
);
}
#[cfg(feature = "highs")]
#[test]
fn basis_dimensions_after_solve() {
let mut solver = HighsSolver::new().expect("solver");
let template = make_fixture_stage_template();
solver.load_model(&template);
solver.solve(None).expect("solve");
let mut basis = Basis::new(template.num_cols, template.num_rows);
solver.get_basis(&mut basis);
assert_eq!(basis.col_status.len(), 3, "expected 3 col statuses");
assert_eq!(basis.row_status.len(), 2, "expected 2 row statuses");
for (i, &code) in basis.col_status.iter().enumerate() {
assert!(
(0..=4).contains(&code),
"col_status[{i}] = {code} is not a valid HiGHS basis status (0..=4)"
);
}
for (i, &code) in basis.row_status.iter().enumerate() {
assert!(
(0..=4).contains(&code),
"row_status[{i}] = {code} is not a valid HiGHS basis status (0..=4)"
);
}
}
#[cfg(all(feature = "highs", not(debug_assertions)))]
#[test]
fn basis_cut_extension() {
let mut solver = HighsSolver::new().expect("solver");
let template = make_fixture_stage_template();
solver.load_model(&template);
solver.solve(None).expect("cold solve");
let mut basis = Basis::new(template.num_cols, template.num_rows);
solver.get_basis(&mut basis);
solver.load_model(&template);
let cuts = make_fixture_row_batch();
solver.add_rows(&cuts);
let view = solver.solve(Some(&basis)).expect("warm-start with cuts");
assert!(
(view.objective - 162.0).abs() < 1e-8,
"expected objective 162.0, got {}",
view.objective
);
}
#[cfg(feature = "highs")]
#[test]
fn basis_warm_start_iterations() {
let mut solver = HighsSolver::new().expect("solver");
let template = make_fixture_stage_template();
solver.load_model(&template);
let cold_view = solver.solve(None).expect("cold solve");
let cold_iterations = cold_view.iterations;
let mut basis = Basis::new(template.num_cols, template.num_rows);
solver.get_basis(&mut basis);
solver.load_model(&template);
let warm_view = solver.solve(Some(&basis)).expect("warm-start");
assert!(
warm_view.iterations <= cold_iterations,
"warm-start iterations ({}) must not exceed cold-start iterations ({})",
warm_view.iterations,
cold_iterations
);
let stats = solver.statistics();
assert_eq!(
stats.basis_consistency_failures, 0,
"basis_consistency_failures must be 0 after accepted basis, got {}",
stats.basis_consistency_failures
);
}
#[cfg(feature = "highs")]
#[test]
fn test_basis_roundtrip() {
let mut solver = HighsSolver::new().expect("solver");
let template = make_fixture_stage_template();
solver.load_model(&template);
solver.solve(None).expect("cold solve must succeed");
let mut basis = Basis::new(template.num_cols, template.num_rows);
solver.get_basis(&mut basis);
solver.load_model(&template);
let warm_view = solver
.solve(Some(&basis))
.expect("warm-start solve must succeed");
assert!(
(warm_view.objective - 100.0).abs() < 1e-8,
"warm-start objective must equal 100.0, got {}",
warm_view.objective
);
assert!(
warm_view.iterations <= 1,
"warm-start from exact basis must complete in at most 1 iteration, got {}",
warm_view.iterations
);
}
#[cfg(feature = "clp")]
#[test]
fn test_solver_clp_load_model_and_solve() {
let mut solver = ClpSolver::new().expect("ClpSolver::new() must succeed");
let template = make_fixture_stage_template();
solver.load_model(&template);
let solution = solver
.solve(None)
.expect("solve() must succeed on feasible LP");
let obj = solution.objective;
assert!(
(obj - 100.0).abs() < 1e-8,
"expected objective = 100.0, got {obj}"
);
let primals = &solution.primal;
assert!(
(primals[0] - 6.0).abs() < 1e-8,
"expected x0 = 6.0, got {}",
primals[0]
);
assert!(
(primals[1] - 0.0).abs() < 1e-8,
"expected x1 = 0.0, got {}",
primals[1]
);
assert!(
(primals[2] - 2.0).abs() < 1e-8,
"expected x2 = 2.0, got {}",
primals[2]
);
}
#[cfg(feature = "clp")]
#[test]
fn test_solver_clp_solve_dual_values() {
let mut solver = ClpSolver::new().expect("ClpSolver::new() must succeed");
let template = make_fixture_stage_template();
solver.load_model(&template);
let solution = solver
.solve(None)
.expect("solve() must succeed on feasible LP");
assert_eq!(
solution.dual.len(),
2,
"expected dual.len() = 2, got {}",
solution.dual.len()
);
let pi_0 = solution.dual[0];
assert!(
(pi_0 - (-100.0)).abs() < 1e-6,
"expected dual[0] = -100.0, got {pi_0}"
);
let pi_1 = solution.dual[1];
assert!(
(pi_1 - 50.0).abs() < 1e-6,
"expected dual[1] = 50.0, got {pi_1}"
);
}
#[cfg(feature = "clp")]
#[test]
fn test_solver_clp_add_rows_then_solve() {
let mut solver = ClpSolver::new().expect("ClpSolver::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 after adding both cuts");
assert!(
(solution.objective - 162.0).abs() < 1e-8,
"expected objective = 162.0, got {}",
solution.objective
);
let primals = &solution.primal;
assert_eq!(primals.len(), 3);
assert!(
(primals[0] - 6.0).abs() < 1e-8
&& (primals[1] - 62.0).abs() < 1e-8
&& (primals[2] - 2.0).abs() < 1e-8,
"expected [6.0, 62.0, 2.0], got [{}, {}, {}]",
primals[0],
primals[1],
primals[2]
);
}
#[cfg(feature = "clp")]
#[test]
fn test_solver_clp_warm_start_roundtrip() {
let mut solver = ClpSolver::new().expect("ClpSolver::new() must succeed");
let template = make_fixture_stage_template();
solver.load_model(&template);
solver.solve(None).expect("cold solve must succeed");
let mut basis = Basis::new(0, 0);
solver.get_basis(&mut basis);
solver.load_model(&template);
let warm_view = solver
.solve(Some(&basis))
.expect("warm-start solve must succeed");
assert!(
(warm_view.objective - 100.0).abs() < 1e-8,
"warm-start objective must equal 100.0, got {}",
warm_view.objective
);
}