mod core;
mod crossover;
mod ratio_test;
mod reconcile;
pub(crate) use core::revised_simplex_core;
pub(crate) use crossover::crossover_dual_from_primal;
pub(crate) use reconcile::{extract_solution, reconcile_final_basis_state};
#[cfg(test)]
pub(crate) use reconcile::pivot_out_degenerate_artificials as test_pivot_out_degenerate_artificials;
use crate::basis::{BasisManager, LuBasis};
use crate::options::{SolverOptions, WarmStartBasis};
use crate::presolve::LpEquilibration;
use crate::problem::{LpProblem, SolveStatus, SolverResult};
use crate::sparse::CscMatrix;
use crate::tolerances::*;
use super::dual_common::{basic_obj, lp_unbounded_ray_verified};
use super::pricing::SteepestEdgePricing;
use super::{extract_dual_info, SimplexOutcome, StandardForm};
use self::reconcile::{check_eq_feasibility, pivot_out_degenerate_artificials, try_apply_crash};
#[allow(clippy::print_stderr)]
fn trace_stage(message: impl std::fmt::Display) {
if std::env::var("OTSPOT_SIMPLEX_STAGE_TRACE")
.ok()
.is_some_and(|v| v == "1" || v.eq_ignore_ascii_case("true"))
{
eprintln!("[simplex-stage] {message}");
}
}
#[cfg(test)]
pub(crate) static PIVOT_CLEAN_EARLY_EXIT_COUNT: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
#[cfg(test)]
pub(crate) static PIVOT_CLEAN_CLEANUP_RAN_COUNT: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
#[cfg(test)]
pub(crate) static OBJ_PROGRESS_RESET_COUNT: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
#[cfg(test)]
pub(crate) static CYCLE_DETECT_NONDEGEN_PRESERVED: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
thread_local! {
#[cfg(test)]
pub(crate) static PIVOT_OUT_BTRAN_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
#[cfg(test)]
pub(crate) static PIVOT_OUT_BATCH_LU_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
#[cfg(test)]
pub(crate) static PIVOT_OUT_SEQUENTIAL_FALLBACK_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
#[cfg(test)]
pub(crate) static PIVOT_OUT_UNCOMMITTED_SEQUENTIAL_COUNT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
#[allow(clippy::too_many_arguments)]
fn gate_phase2_unbounded(
outcome: SimplexOutcome,
a: &CscMatrix,
basis: &[usize],
c: &[f64],
x_b: &[f64],
m: usize,
n_cols: usize,
n_enter: usize,
options: &SolverOptions,
) -> SimplexOutcome {
if matches!(outcome, SimplexOutcome::Unbounded)
&& !lp_unbounded_ray_verified(a, basis, c, m, n_cols, n_enter, options)
{
SimplexOutcome::Timeout(basic_obj(c, basis, x_b))
} else {
outcome
}
}
const SLACK_DIAG_TOL: f64 = 1e-14;
fn phase1_infeasibility_verdict(farkas: Vec<f64>, total_iters: usize) -> SolverResult {
if farkas.is_empty() {
return SolverResult {
status: SolveStatus::Timeout,
objective: f64::INFINITY,
iterations: total_iters,
..Default::default()
};
}
SolverResult {
status: SolveStatus::Infeasible,
objective: f64::INFINITY,
dual_solution: farkas,
iterations: total_iters,
..Default::default()
}
}
fn extract_farkas_certificate(
a_ext: &CscMatrix,
b: &[f64],
basis: &[usize],
m: usize,
n_original: usize,
options: &SolverOptions,
) -> Vec<f64> {
let art_rows: Vec<usize> = (0..m)
.filter(|&i| basis[i] >= n_original)
.collect();
if art_rows.is_empty() {
return vec![];
}
let mut basis_mgr = match LuBasis::new_timed(a_ext, basis, options.max_etas, options.deadline) {
Ok(bm) => bm,
Err(_) => return vec![],
};
let b_norm = b.iter().fold(0.0_f64, |acc, &v| acc.max(v.abs()));
let tol = options.dual_tol * (1.0_f64).max(b_norm);
let aty_ok = |y: &[f64]| -> bool {
for j in 0..n_original {
let Ok((rows, vals)) = a_ext.get_column(j) else {
return false;
};
let aty: f64 = rows.iter().zip(vals.iter()).map(|(&r, &v)| v * y[r]).sum();
if aty > tol {
return false;
}
}
true
};
let mut x_b_fresh = b.to_vec();
basis_mgr.ftran_dense(&mut x_b_fresh);
{
let mut y: Vec<f64> = (0..m)
.map(|i| if basis[i] >= n_original { 1.0 } else { 0.0 })
.collect();
basis_mgr.btran_dense(&mut y);
let by: f64 = b.iter().zip(y.iter()).map(|(&bi, &yi)| bi * yi).sum();
if by > tol && aty_ok(&y) {
return y;
}
}
{
let pos_art_indicator: Vec<f64> = (0..m)
.map(|i| {
if basis[i] >= n_original && x_b_fresh[i] > PIVOT_TOL {
x_b_fresh[i]
} else {
0.0
}
})
.collect();
if pos_art_indicator.iter().any(|&v| v > 0.0) {
let mut y = pos_art_indicator;
basis_mgr.btran_dense(&mut y);
let by: f64 = b.iter().zip(y.iter()).map(|(&bi, &yi)| bi * yi).sum();
if by > PIVOT_TOL && aty_ok(&y) {
return y;
}
}
}
for &row in &art_rows {
if x_b_fresh[row] <= PIVOT_TOL {
continue;
}
let mut e_i = vec![0.0_f64; m];
e_i[row] = 1.0;
basis_mgr.btran_dense(&mut e_i);
let by: f64 = b.iter().zip(e_i.iter()).map(|(&bi, &yi)| bi * yi).sum();
if by > tol && aty_ok(&e_i) {
return e_i;
}
}
vec![]
}
fn extract_timeout_solution_reconciled(
sf: &StandardForm,
a: &CscMatrix,
b: &[f64],
c: &[f64],
basis: &[usize],
x_b: &[f64],
col_scale: &[f64],
max_etas: usize,
deadline: Option<std::time::Instant>,
) -> Vec<f64> {
let mut x_b_reconciled = x_b.to_vec();
let mut y = vec![0.0_f64; basis.len()];
if reconcile_final_basis_state(
a,
b,
c,
basis,
&mut x_b_reconciled,
&mut y,
max_etas,
deadline,
)
.is_ok()
{
extract_solution(sf, basis, &x_b_reconciled, col_scale)
} else {
extract_solution(sf, basis, x_b, col_scale)
}
}
fn objective_from_solution(sf: &StandardForm, problem: &LpProblem, solution: &[f64]) -> f64 {
problem
.c
.iter()
.zip(solution.iter())
.map(|(&ci, &xi)| ci * xi)
.sum::<f64>()
+ sf.obj_offset
}
pub(crate) fn two_phase_simplex(
sf: &StandardForm,
problem: &LpProblem,
options: &SolverOptions,
) -> SolverResult {
let m = sf.m;
let mut total_iters: usize = 0;
trace_stage(format_args!(
"start m={} n_total={} n_artificial={}",
sf.m, sf.n_total, sf.num_artificial
));
let Some((a, b, c, row_scale, col_scale)) =
LpEquilibration::scale_with_deadline(&sf.a, &sf.b, &sf.c, options.deadline)
else {
trace_stage("equilibration timeout");
return SolverResult {
status: SolveStatus::Timeout,
objective: f64::INFINITY,
..Default::default()
};
};
if sf.num_artificial == 0 {
trace_stage("direct phase2 start");
let mut basis = sf.initial_basis.clone();
let mut x_b = b.clone();
for i in 0..m {
let col = basis[i];
if let Ok((rows, vals)) = a.get_column(col) {
for (k, &row) in rows.iter().enumerate() {
if row == i && vals[k].abs() > SLACK_DIAG_TOL {
x_b[i] /= vals[k];
break;
}
}
}
}
let mut pricing = SteepestEdgePricing::new(sf.n_total);
let phase2_outcome = revised_simplex_core(
&a,
&mut x_b,
&c,
&b,
&mut basis,
m,
sf.n_total,
sf.n_total,
&mut pricing,
options,
&mut total_iters,
false,
);
let phase2_outcome = gate_phase2_unbounded(
phase2_outcome,
&a,
&basis,
&c,
&x_b,
m,
sf.n_total,
sf.n_total,
options,
);
match phase2_outcome {
SimplexOutcome::Optimal(obj, mut y) => {
match reconcile_final_basis_state(
&a,
&b,
&c,
&basis,
&mut x_b,
&mut y,
options.max_etas,
options.deadline,
) {
Ok(()) => {}
Err(crate::error::SolverError::DeadlineExceeded) => {
trace_stage("direct phase2 final reconcile deadline");
let solution = extract_timeout_solution_reconciled(
sf,
&a,
&b,
&c,
&basis,
&x_b,
&col_scale,
options.max_etas,
options.deadline,
);
return SolverResult {
status: SolveStatus::Timeout,
objective: obj + sf.obj_offset,
solution,
iterations: total_iters,
..Default::default()
};
}
Err(_) => {
trace_stage("direct phase2 final reconcile numerical");
return SolverResult::numerical_error();
}
}
let solution = extract_solution(sf, &basis, &x_b, &col_scale);
if !check_eq_feasibility(problem, &solution) {
trace_stage("direct phase2 final feasibility failed");
return SolverResult {
status: SolveStatus::NumericalError,
objective: obj + sf.obj_offset,
solution: vec![],
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
..Default::default()
};
}
let (dual_solution, reduced_costs, slack) =
extract_dual_info(sf, problem, &y, &solution, &row_scale);
let ws = WarmStartBasis {
basis: basis.clone(),
x_b: x_b.clone(),
};
SolverResult {
status: SolveStatus::Optimal,
objective: obj + sf.obj_offset,
solution,
dual_solution,
reduced_costs,
slack,
warm_start_basis: Some(ws),
iterations: total_iters,
..Default::default()
}
}
SimplexOutcome::Unbounded => SolverResult {
status: SolveStatus::Unbounded,
objective: f64::NEG_INFINITY,
solution: vec![],
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
iterations: total_iters,
..Default::default()
},
SimplexOutcome::Timeout(obj) => {
let solution = extract_timeout_solution_reconciled(
sf,
&a,
&b,
&c,
&basis,
&x_b,
&col_scale,
options.max_etas,
options.deadline,
);
SolverResult {
status: SolveStatus::Timeout,
objective: obj + sf.obj_offset,
solution,
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
iterations: total_iters,
..Default::default()
}
}
SimplexOutcome::SingularBasis => {
trace_stage("direct phase2 singular basis");
SolverResult::numerical_error()
}
}
} else {
trace_stage("phase1 setup start");
let mut trip_rows: Vec<usize> = Vec::new();
let mut trip_cols: Vec<usize> = Vec::new();
let mut trip_vals: Vec<f64> = Vec::new();
for j in 0..a.ncols {
if let Ok((r, v)) = a.get_column(j) {
for (k, &row) in r.iter().enumerate() {
trip_rows.push(row);
trip_cols.push(j);
trip_vals.push(v[k]);
}
}
}
let mut basis = sf.initial_basis.clone();
let mut x_b = b.clone();
let mut art_col = sf.n_total;
for i in 0..m {
if !sf.needs_artificial[i] {
continue;
}
trip_rows.push(i);
trip_cols.push(art_col);
trip_vals.push(1.0);
basis[i] = art_col;
art_col += 1;
}
let n_ext = art_col;
let a_ext = CscMatrix::from_triplets(&trip_rows, &trip_cols, &trip_vals, m, n_ext).unwrap();
trace_stage(format_args!("phase1 setup done n_ext={n_ext}"));
let mut c_phase1 = vec![0.0; n_ext];
c_phase1[sf.n_total..].fill(1.0);
let crashed = if options.warm_start.is_none()
&& options.use_lp_crash_basis
&& sf.num_artificial > 0
{
try_apply_crash(
&a_ext,
m,
sf.n_shifted,
sf.n_total,
&b,
options.max_etas,
options.deadline,
&basis,
)
} else {
None
};
if let Some((crash_basis, crash_x_b)) = crashed {
trace_stage("crash basis accepted");
basis = crash_basis;
x_b = crash_x_b;
} else {
trace_stage("cold artificial basis");
for i in 0..m {
if let Ok((rows, vals)) = a_ext.get_column(basis[i]) {
for (k, &row) in rows.iter().enumerate() {
if row == i && vals[k].abs() > SLACK_DIAG_TOL {
x_b[i] /= vals[k];
break;
}
}
}
}
}
for i in 0..m {
if basis[i] >= sf.n_total && x_b[i].abs() <= PIVOT_TOL {
x_b[i] = PIVOT_TOL * (i as f64 + 1.0);
}
}
let mut pricing1 = SteepestEdgePricing::new(n_ext);
trace_stage("phase1 core start");
let phase1_outcome = revised_simplex_core(
&a_ext,
&mut x_b,
&c_phase1,
&b,
&mut basis,
m,
n_ext,
n_ext,
&mut pricing1,
options,
&mut total_iters,
true,
);
match phase1_outcome {
SimplexOutcome::Optimal(_obj, _) => {
trace_stage(format_args!("phase1 optimal iters={total_iters}"));
use crate::options::MAX_PHASE1_RETRIES;
let mut phase1_feasible = false;
'retry: for attempt in 0..=MAX_PHASE1_RETRIES {
if options
.deadline
.is_some_and(|d| std::time::Instant::now() >= d)
{
break 'retry;
}
let mut y_dummy = vec![0.0f64; m];
let rec_obj = match reconcile_final_basis_state(
&a_ext,
&b,
&c_phase1,
&basis,
&mut x_b,
&mut y_dummy,
options.max_etas,
options.deadline,
) {
Ok(()) => (0..m)
.map(|i| c_phase1[basis[i]] * x_b[i].max(0.0))
.sum::<f64>(),
Err(_) => {
trace_stage("phase1 reconcile failed");
break 'retry;
}
};
if rec_obj <= PIVOT_TOL {
phase1_feasible = true;
break 'retry;
}
if attempt == MAX_PHASE1_RETRIES {
break 'retry;
}
for v in x_b.iter_mut() {
if *v < 0.0 {
*v = 0.0;
}
}
let mut pricing_retry = SteepestEdgePricing::new(n_ext);
match revised_simplex_core(
&a_ext,
&mut x_b,
&c_phase1,
&b,
&mut basis,
m,
n_ext,
n_ext,
&mut pricing_retry,
options,
&mut total_iters,
true,
) {
SimplexOutcome::Optimal(_, _) => {}
SimplexOutcome::Unbounded => break 'retry,
SimplexOutcome::Timeout(_) => {
let mut y_check = vec![0.0f64; m];
let reconciled = reconcile_final_basis_state(
&a_ext,
&b,
&c_phase1,
&basis,
&mut x_b,
&mut y_check,
options.max_etas,
options.deadline,
)
.is_ok();
if reconciled {
let rec_obj_retry: f64 = (0..m)
.map(|i| c_phase1[basis[i]] * x_b[i].max(0.0))
.sum();
if rec_obj_retry <= PIVOT_TOL {
phase1_feasible = true;
break 'retry;
}
}
return SolverResult {
status: SolveStatus::Timeout,
objective: f64::INFINITY,
solution: vec![],
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
iterations: total_iters,
..Default::default()
};
}
SimplexOutcome::SingularBasis => {
trace_stage("phase1 retry singular basis");
return SolverResult::numerical_error();
}
}
}
if !phase1_feasible {
trace_stage("phase1 not feasible; extracting farkas");
let farkas =
extract_farkas_certificate(&a_ext, &b, &basis, m, sf.n_total, options);
return phase1_infeasibility_verdict(farkas, total_iters);
}
trace_stage("pivot out degenerate artificials start");
pivot_out_degenerate_artificials(&a_ext, &mut basis, &x_b, sf, options);
let remaining_art = basis.iter().filter(|&&col| col >= sf.n_total).count();
trace_stage(format_args!(
"pivot out degenerate artificials done remaining_art={remaining_art}"
));
let mut c_phase2 = vec![0.0; n_ext];
c_phase2[..sf.n_total].copy_from_slice(&c[..sf.n_total]);
{
let mut y_transition = vec![0.0f64; m];
trace_stage("phase2 transition reconcile start");
match reconcile_final_basis_state(
&a_ext,
&b,
&c_phase2,
&basis,
&mut x_b,
&mut y_transition,
options.max_etas,
options.deadline,
) {
Ok(()) => {}
Err(crate::error::SolverError::DeadlineExceeded) => {
trace_stage("phase2 transition reconcile deadline");
let solution = extract_timeout_solution_reconciled(
sf,
&a_ext,
&b,
&c_phase2,
&basis,
&x_b,
&col_scale,
options.max_etas,
options.deadline,
);
return SolverResult {
status: SolveStatus::Timeout,
objective: objective_from_solution(sf, problem, &solution),
solution,
iterations: total_iters,
..Default::default()
};
}
Err(_) => {
trace_stage("phase2 transition reconcile numerical");
return SolverResult::numerical_error();
}
}
}
for i in 0..m {
if x_b[i].abs() < PIVOT_TOL {
x_b[i] = PIVOT_TOL * (i as f64 + 1.0);
}
}
for v in x_b.iter_mut() {
if *v < 0.0 {
*v = 0.0;
}
}
let mut pricing2 = SteepestEdgePricing::new(n_ext);
trace_stage("phase2 core start");
let phase2_outcome = revised_simplex_core(
&a_ext,
&mut x_b,
&c_phase2,
&b,
&mut basis,
m,
n_ext,
sf.n_total,
&mut pricing2,
options,
&mut total_iters,
false,
);
let phase2_outcome = gate_phase2_unbounded(
phase2_outcome,
&a_ext,
&basis,
&c_phase2,
&x_b,
m,
n_ext,
sf.n_total,
options,
);
match phase2_outcome {
SimplexOutcome::Optimal(obj2, mut y) => {
trace_stage(format_args!("phase2 optimal iters={total_iters}"));
match reconcile_final_basis_state(
&a_ext,
&b,
&c_phase2,
&basis,
&mut x_b,
&mut y,
options.max_etas,
options.deadline,
) {
Ok(()) => {}
Err(crate::error::SolverError::DeadlineExceeded) => {
trace_stage("phase2 final reconcile deadline");
let solution = extract_timeout_solution_reconciled(
sf,
&a_ext,
&b,
&c_phase2,
&basis,
&x_b,
&col_scale,
options.max_etas,
options.deadline,
);
return SolverResult {
status: SolveStatus::Timeout,
objective: obj2 + sf.obj_offset,
solution,
iterations: total_iters,
..Default::default()
};
}
Err(_) => {
trace_stage("phase2 final reconcile numerical");
return SolverResult::numerical_error();
}
}
let solution = extract_solution(sf, &basis, &x_b, &col_scale);
if !check_eq_feasibility(problem, &solution) {
trace_stage("phase2 final feasibility failed");
return SolverResult {
status: SolveStatus::NumericalError,
objective: obj2 + sf.obj_offset,
solution: vec![],
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
..Default::default()
};
}
let (dual_solution, reduced_costs, slack) =
extract_dual_info(sf, problem, &y, &solution, &row_scale);
let ws = WarmStartBasis {
basis: basis.clone(),
x_b: x_b.clone(),
};
SolverResult {
status: SolveStatus::Optimal,
objective: obj2 + sf.obj_offset,
solution,
dual_solution,
reduced_costs,
slack,
warm_start_basis: Some(ws),
iterations: total_iters,
..Default::default()
}
}
SimplexOutcome::Unbounded => SolverResult {
status: SolveStatus::Unbounded,
objective: f64::NEG_INFINITY,
solution: vec![],
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
iterations: total_iters,
..Default::default()
},
SimplexOutcome::Timeout(obj2) => {
let solution = extract_timeout_solution_reconciled(
sf,
&a_ext,
&b,
&c_phase2,
&basis,
&x_b,
&col_scale,
options.max_etas,
options.deadline,
);
SolverResult {
status: SolveStatus::Timeout,
objective: obj2 + sf.obj_offset,
solution,
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
iterations: total_iters,
..Default::default()
}
}
SimplexOutcome::SingularBasis => {
trace_stage("phase2 singular basis");
SolverResult::numerical_error()
}
}
}
SimplexOutcome::Unbounded => {
trace_stage(format_args!("phase1 unbounded iters={total_iters}"));
let farkas = extract_farkas_certificate(&a_ext, &b, &basis, m, sf.n_total, options);
phase1_infeasibility_verdict(farkas, total_iters)
}
SimplexOutcome::Timeout(obj1) => {
trace_stage(format_args!("phase1 timeout iters={total_iters} obj={obj1:.9e}"));
if obj1 <= PIVOT_TOL {
{
let mut y_dummy = vec![0.0_f64; m];
if reconcile_final_basis_state(
&a_ext,
&b,
&c_phase1,
&basis,
&mut x_b,
&mut y_dummy,
options.max_etas,
options.deadline,
)
.is_err()
{
return SolverResult {
status: SolveStatus::Timeout,
objective: f64::INFINITY,
solution: vec![],
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
iterations: total_iters,
..Default::default()
};
}
}
let rec_obj: f64 = (0..m).map(|i| c_phase1[basis[i]] * x_b[i].max(0.0)).sum();
if rec_obj > PIVOT_TOL {
return SolverResult {
status: SolveStatus::Timeout,
objective: f64::INFINITY,
solution: vec![],
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
iterations: total_iters,
..Default::default()
};
}
pivot_out_degenerate_artificials(&a_ext, &mut basis, &x_b, sf, options);
let mut c_phase2 = vec![0.0; n_ext];
c_phase2[..sf.n_total].copy_from_slice(&c[..sf.n_total]);
{
let mut y_transition = vec![0.0f64; m];
match reconcile_final_basis_state(
&a_ext,
&b,
&c_phase2,
&basis,
&mut x_b,
&mut y_transition,
options.max_etas,
options.deadline,
) {
Ok(()) => {}
Err(crate::error::SolverError::DeadlineExceeded) => {
let solution = extract_timeout_solution_reconciled(
sf,
&a_ext,
&b,
&c_phase2,
&basis,
&x_b,
&col_scale,
options.max_etas,
options.deadline,
);
return SolverResult {
status: SolveStatus::Timeout,
objective: objective_from_solution(sf, problem, &solution),
solution,
iterations: total_iters,
..Default::default()
};
}
Err(_) => return SolverResult::numerical_error(),
}
}
for i in 0..m {
if x_b[i].abs() < PIVOT_TOL {
x_b[i] = PIVOT_TOL * (i as f64 + 1.0);
}
}
for v in x_b.iter_mut() {
if *v < 0.0 {
*v = 0.0;
}
}
let mut pricing2 = SteepestEdgePricing::new(n_ext);
let phase2_outcome = revised_simplex_core(
&a_ext,
&mut x_b,
&c_phase2,
&b,
&mut basis,
m,
n_ext,
sf.n_total,
&mut pricing2,
options,
&mut total_iters,
false,
);
let phase2_outcome = gate_phase2_unbounded(
phase2_outcome,
&a_ext,
&basis,
&c_phase2,
&x_b,
m,
n_ext,
sf.n_total,
options,
);
match phase2_outcome {
SimplexOutcome::Optimal(obj2, mut y) => {
match reconcile_final_basis_state(
&a_ext,
&b,
&c_phase2,
&basis,
&mut x_b,
&mut y,
options.max_etas,
options.deadline,
) {
Ok(()) => {}
Err(crate::error::SolverError::DeadlineExceeded) => {
let solution = extract_timeout_solution_reconciled(
sf,
&a_ext,
&b,
&c_phase2,
&basis,
&x_b,
&col_scale,
options.max_etas,
options.deadline,
);
return SolverResult {
status: SolveStatus::Timeout,
objective: obj2 + sf.obj_offset,
solution,
iterations: total_iters,
..Default::default()
};
}
Err(_) => return SolverResult::numerical_error(),
}
let solution = extract_solution(sf, &basis, &x_b, &col_scale);
if !check_eq_feasibility(problem, &solution) {
return SolverResult::numerical_error();
}
let (dual_solution, reduced_costs, slack) =
extract_dual_info(sf, problem, &y, &solution, &row_scale);
let ws = WarmStartBasis {
basis: basis.clone(),
x_b: x_b.clone(),
};
return SolverResult {
status: SolveStatus::Optimal,
objective: obj2 + sf.obj_offset,
solution,
dual_solution,
reduced_costs,
slack,
warm_start_basis: Some(ws),
..Default::default()
};
}
SimplexOutcome::Timeout(obj2) => {
let solution = extract_timeout_solution_reconciled(
sf,
&a_ext,
&b,
&c_phase2,
&basis,
&x_b,
&col_scale,
options.max_etas,
options.deadline,
);
return SolverResult {
status: SolveStatus::Timeout,
objective: obj2 + sf.obj_offset,
solution,
iterations: total_iters,
..Default::default()
};
}
SimplexOutcome::Unbounded => {
return SolverResult {
status: SolveStatus::Unbounded,
objective: f64::NEG_INFINITY,
solution: vec![],
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
..Default::default()
};
}
SimplexOutcome::SingularBasis => {
return SolverResult::numerical_error();
}
}
}
SolverResult {
status: SolveStatus::Timeout,
objective: f64::INFINITY,
solution: vec![],
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
iterations: total_iters,
..Default::default()
}
}
SimplexOutcome::SingularBasis => SolverResult::numerical_error(),
}
}
}
#[cfg(test)]
pub(crate) fn test_apply_selective_charnes_perturb(x_b: &mut [f64], m: usize) {
let eps = PIVOT_TOL * (m as f64).max(1.0);
let step_zero_threshold = PIVOT_TOL * (m as f64).max(1.0);
for (i, v) in x_b.iter_mut().enumerate() {
if v.abs() < step_zero_threshold {
*v = eps * (i as f64 + 1.0);
}
}
}
#[cfg(test)]
mod farkas_gate_tests {
use super::{extract_farkas_certificate, phase1_infeasibility_verdict};
use crate::options::SolverOptions;
use crate::problem::SolveStatus;
use crate::sparse::CscMatrix;
#[test]
fn empty_cert_yields_timeout_not_infeasible() {
let r = phase1_infeasibility_verdict(vec![], 7);
assert_eq!(
r.status,
SolveStatus::Timeout,
"empty (unverifiable) Farkas cert must NOT be declared Infeasible"
);
assert_eq!(r.iterations, 7);
}
#[test]
fn verified_cert_yields_infeasible() {
let r = phase1_infeasibility_verdict(vec![-1.0, 1.0], 3);
assert_eq!(r.status, SolveStatus::Infeasible);
assert_eq!(r.dual_solution, vec![-1.0, 1.0]);
assert_eq!(r.iterations, 3);
}
#[test]
fn extract_farkas_discriminates_witness_from_degenerate() {
let a_ext =
CscMatrix::from_triplets(&[0, 1, 0, 1], &[0, 0, 1, 2], &[1.0, 1.0, 1.0, 1.0], 2, 3)
.unwrap();
let basis = [0usize, 2usize]; let n_original = 1;
let opts = SolverOptions::default();
let cert_infeasible =
extract_farkas_certificate(&a_ext, &[1.0, 2.0], &basis, 2, n_original, &opts);
assert!(
!cert_infeasible.is_empty(),
"true infeasible RHS must yield a verified Farkas certificate"
);
let cert_feasible =
extract_farkas_certificate(&a_ext, &[1.0, 1.0], &basis, 2, n_original, &opts);
assert!(
cert_feasible.is_empty(),
"feasible RHS (degenerate artificial, bᵀy≈0) must NOT be certified infeasible"
);
}
}
#[cfg(test)]
mod cycle_perturbation_tests {
use super::test_apply_selective_charnes_perturb;
use crate::options::{SimplexMethod, SolverOptions};
use crate::problem::{ConstraintType, LpProblem, SolveStatus};
use crate::simplex::entry::solve_with;
use crate::sparse::CscMatrix;
use crate::tolerances::PIVOT_TOL;
#[test]
fn selective_charnes_perturb_spares_large_values() {
let m = 4usize;
let eps = PIVOT_TOL * (m as f64); let thresh = eps;
let mut x_b = vec![100.0, 0.0, thresh * 0.5, 50.0];
let saved = [x_b[0], x_b[3]];
test_apply_selective_charnes_perturb(&mut x_b, m);
assert_eq!(x_b[0], saved[0], "x_b[0]=100 must not be modified (non-degenerate)");
assert_eq!(x_b[3], saved[1], "x_b[3]=50 must not be modified (non-degenerate)");
assert_eq!(
x_b[1], eps * 2.0,
"x_b[1]=0 at i=1 must become eps*(1+1)=eps*2"
);
assert_eq!(
x_b[2], eps * 3.0,
"x_b[2]=thresh*0.5 at i=2 must become eps*(2+1)=eps*3"
);
assert!(x_b[1] > 0.0 && x_b[2] > 0.0, "perturbed values must be positive");
assert!(
(x_b[1] - x_b[2]).abs() > 1e-20,
"perturbed values must be distinct"
);
}
fn primal_opts() -> SolverOptions {
SolverOptions {
simplex_method: SimplexMethod::Primal,
..Default::default()
}
}
fn make_le_lp(
c: Vec<f64>,
rows: &[usize],
cols: &[usize],
vals: &[f64],
m: usize,
n: usize,
b: Vec<f64>,
) -> LpProblem {
let a = CscMatrix::from_triplets(rows, cols, vals, m, n).unwrap();
LpProblem::new_general(
c,
a,
b,
vec![ConstraintType::Le; m],
vec![(0.0, f64::INFINITY); n],
None,
)
.unwrap()
}
#[test]
fn selective_perturbation_degenerate_lp_converges() {
let lp = make_le_lp(
vec![-1.0, -1.0],
&[0, 0, 1, 2],
&[0, 1, 0, 1],
&[1.0, 1.0, 1.0, 1.0],
3,
2,
vec![2.0, 1.0, 1.0],
);
let result = solve_with(&lp, &primal_opts());
assert_eq!(
result.status,
SolveStatus::Optimal,
"degenerate LP must reach Optimal; got {:?}",
result.status
);
assert!(
(result.objective - (-2.0)).abs() < 1e-6,
"expected obj=-2, got {}",
result.objective
);
}
#[test]
fn cycle_perturbation_threshold_scales_with_m() {
for m in [1_usize, 10, 100, 1441] {
let eps = PIVOT_TOL * (m as f64).max(1.0);
let step_zero_threshold = PIVOT_TOL * (m as f64).max(1.0);
assert!(
(eps - step_zero_threshold).abs() < f64::EPSILON,
"eps and step_zero_threshold must match for m={m}: eps={eps}, thr={step_zero_threshold}"
);
if m > 1 {
assert!(
eps > PIVOT_TOL,
"threshold must exceed PIVOT_TOL for m={m}>1 (must scale with m)"
);
}
}
}
}