use crate::basis::{BasisManager, LuBasis};
use crate::options::{SolverOptions, WarmStartBasis};
use crate::problem::{LpProblem, SolveStatus, SolverResult};
use crate::presolve::RuizScaler;
use crate::sparse::{CscMatrix, SparseVec};
use crate::tolerances::*;
use super::{StandardForm, SimplexOutcome, extract_solution, extract_dual_info, timeout_result_with_incumbent};
use super::dual_common::{basic_obj, compute_dual_vars, compute_reduced_costs};
use super::pricing::{DualLeavingStrategy, MostInfeasibleLeaving, SteepestEdgePricing};
use std::sync::atomic::Ordering;
pub(crate) fn two_phase_dual_simplex(
sf: &StandardForm,
problem: &LpProblem,
options: &SolverOptions,
) -> SolverResult {
let m = sf.m;
let (a, b, c, row_scale, col_scale) = RuizScaler::scale(&sf.a, &sf.b, &sf.c);
if let Some(warm) = &options.warm_start {
if warm.basis.len() == m && warm.basis.iter().all(|&idx| idx < sf.n_total) {
let mut basis = warm.basis.clone();
match LuBasis::new(&a, &basis, options.max_etas) {
Ok(mut basis_mgr) => {
let mut x_b_sv = SparseVec::from_dense(&b);
basis_mgr.ftran(&mut x_b_sv);
let mut x_b = x_b_sv.to_dense();
let mut total_iters: usize = 0;
let outcome = dual_simplex_core(
&a, &mut x_b, &c, &mut basis, m, sf.n_total, options,
&mut total_iters,
);
let mut result = warm_outcome_to_result(
outcome, sf, problem, &basis, &x_b, &col_scale, &row_scale,
);
result.iterations = total_iters;
return result;
}
Err(_) => {}
}
}
}
cold_start_dual(sf, problem, options, &a, &b, &c, &row_scale, &col_scale)
}
#[allow(clippy::too_many_arguments)]
fn cold_start_dual(
sf: &StandardForm,
problem: &LpProblem,
options: &SolverOptions,
a: &CscMatrix,
b: &[f64],
c: &[f64],
row_scale: &[f64],
col_scale: &[f64],
) -> SolverResult {
let m = sf.m;
if sf.num_artificial > 0 {
return super::two_phase_simplex(sf, problem, options);
}
let mut basis = sf.initial_basis.clone();
let mut x_b = b.to_vec();
let c_perturbed: Vec<f64> = c.iter().map(|&v| v.max(0.0)).collect();
let mut total_iters: usize = 0;
let phase1_outcome = dual_simplex_core(
a, &mut x_b, &c_perturbed, &mut basis, m, sf.n_total, options,
&mut total_iters,
);
match phase1_outcome {
SimplexOutcome::Unbounded => {
return SolverResult {
status: SolveStatus::Infeasible,
objective: 0.0,
solution: vec![],
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
..Default::default()
};
}
SimplexOutcome::Timeout(_) => {
return timeout_result_with_incumbent(sf, problem, &basis, &x_b, col_scale, total_iters);
}
SimplexOutcome::SingularBasis => {
if std::env::var("DUMP_NE_TRACE").ok().as_deref() == Some("1") {
eprintln!("[NE-TRACE] dual.rs:131 Dual Phase-I SingularBasis");
}
return SolverResult::numerical_error();
}
SimplexOutcome::Optimal(_, _) => {}
}
let mut pricing = SteepestEdgePricing::new(sf.n_total);
let phase2_outcome = super::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 mut result = primal_outcome_to_result(
phase2_outcome, sf, problem, &basis, &x_b, col_scale, row_scale,
);
result.iterations = total_iters;
result
}
fn warm_outcome_to_result(
outcome: SimplexOutcome,
sf: &StandardForm,
problem: &LpProblem,
basis: &[usize],
x_b: &[f64],
col_scale: &[f64],
row_scale: &[f64],
) -> SolverResult {
match outcome {
SimplexOutcome::Optimal(obj, y) => {
let solution = extract_solution(sf, basis, x_b, col_scale);
let (dual_solution, reduced_costs, slack) =
extract_dual_info(sf, problem, &y, &solution, row_scale);
let ws = WarmStartBasis { basis: basis.to_vec(), x_b: x_b.to_vec() };
SolverResult {
status: SolveStatus::Optimal,
objective: obj + sf.obj_offset,
solution,
dual_solution,
reduced_costs,
slack,
warm_start_basis: Some(ws),
..Default::default()
}
}
SimplexOutcome::Unbounded => SolverResult {
status: SolveStatus::Infeasible,
objective: 0.0,
solution: vec![],
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
..Default::default()
},
SimplexOutcome::Timeout(obj) => {
let solution = extract_solution(sf, basis, x_b, col_scale);
SolverResult {
status: SolveStatus::Timeout,
objective: obj + sf.obj_offset,
solution,
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
..Default::default()
}
}
SimplexOutcome::SingularBasis => {
if std::env::var("DUMP_NE_TRACE").ok().as_deref() == Some("1") {
eprintln!("[NE-TRACE] dual.rs:206 warm_outcome_to_result SingularBasis");
}
SolverResult::numerical_error()
}
}
}
fn primal_outcome_to_result(
outcome: SimplexOutcome,
sf: &StandardForm,
problem: &LpProblem,
basis: &[usize],
x_b: &[f64],
col_scale: &[f64],
row_scale: &[f64],
) -> SolverResult {
match outcome {
SimplexOutcome::Optimal(obj, y) => {
let solution = extract_solution(sf, basis, x_b, col_scale);
let (dual_solution, reduced_costs, slack) =
extract_dual_info(sf, problem, &y, &solution, row_scale);
let ws = WarmStartBasis { basis: basis.to_vec(), x_b: x_b.to_vec() };
SolverResult {
status: SolveStatus::Optimal,
objective: obj + sf.obj_offset,
solution,
dual_solution,
reduced_costs,
slack,
warm_start_basis: Some(ws),
..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,
..Default::default()
},
SimplexOutcome::Timeout(obj) => {
let solution = extract_solution(sf, basis, x_b, col_scale);
SolverResult {
status: SolveStatus::Timeout,
objective: obj + sf.obj_offset,
solution,
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
..Default::default()
}
}
SimplexOutcome::SingularBasis => {
if std::env::var("DUMP_NE_TRACE").ok().as_deref() == Some("1") {
eprintln!("[NE-TRACE] dual.rs:261 primal_outcome_to_result SingularBasis");
}
SolverResult::numerical_error()
}
}
}
pub(super) fn dual_simplex_core(
a: &CscMatrix,
x_b: &mut [f64],
c: &[f64],
basis: &mut [usize],
m: usize,
n_price: usize,
options: &SolverOptions,
iter_count_out: &mut usize,
) -> SimplexOutcome {
let max_iter = usize::MAX;
let mut basis_mgr = match LuBasis::new(a, basis, options.max_etas) {
Ok(bm) => bm,
Err(crate::error::SolverError::SingularBasis { .. }) => {
return SimplexOutcome::SingularBasis;
}
Err(_) => {
let obj: f64 = basic_obj(c, basis, x_b);
return SimplexOutcome::Timeout(obj);
}
};
let mut is_basic = vec![false; n_price];
for &b in basis.iter() {
if b < n_price {
is_basic[b] = true;
}
}
let mut reduced_costs = compute_reduced_costs(a, c, &mut basis_mgr, &is_basic, n_price, m, basis);
let mut leaving_strategy = MostInfeasibleLeaving;
let mut rho_dense = vec![0.0f64; m];
let mut trow = vec![0.0f64; n_price];
let mut alpha_dense = vec![0.0f64; m];
for _iter in 0..max_iter {
*iter_count_out = iter_count_out.saturating_add(1);
let timed_out = options.deadline.is_some_and(|d| std::time::Instant::now() >= d);
let cancelled = options.cancel_flag.as_ref().is_some_and(|f| f.load(Ordering::Relaxed));
if timed_out || cancelled {
let obj: f64 = basic_obj(c, basis, x_b);
return SimplexOutcome::Timeout(obj);
}
let leaving_row = match leaving_strategy.select_leaving(x_b, options.primal_tol, basis) {
None => {
let obj: f64 = basic_obj(c, basis, x_b);
let y = compute_dual_vars(c, &mut basis_mgr, basis, m);
return SimplexOutcome::Optimal(obj, y);
}
Some(p) => p,
};
let mut e_p = vec![0.0f64; m];
e_p[leaving_row] = 1.0;
let mut rho_sv = SparseVec::from_dense(&e_p);
basis_mgr.btran(&mut rho_sv);
rho_sv.to_dense_into(&mut rho_dense);
for j in 0..n_price {
if is_basic[j] {
trow[j] = 0.0;
continue;
}
let (rows, vals) = a.get_column(j).unwrap();
let mut dot = 0.0;
for (k, &row) in rows.iter().enumerate() {
dot += rho_dense[row] * vals[k];
}
trow[j] = dot;
}
let (entering_col, theta) = match dual_ratio_test(
&trow, &reduced_costs, &is_basic, n_price, PIVOT_TOL,
) {
None => return SimplexOutcome::Unbounded,
Some(result) => result,
};
let (col_rows, col_vals) = a.get_column(entering_col).unwrap();
let mut alpha_sv = SparseVec {
indices: col_rows.to_vec(),
values: col_vals.to_vec(),
len: m,
};
basis_mgr.ftran(&mut alpha_sv);
alpha_sv.to_dense_into(&mut alpha_dense);
let pivot_element = alpha_dense[leaving_row];
if pivot_element.abs() < PIVOT_TOL {
basis_mgr.refactor_if_needed_timed(a, basis, options.deadline);
if basis_mgr.refactor_failed {
if basis_mgr.singular_basis {
return SimplexOutcome::SingularBasis;
}
let obj: f64 = basic_obj(c, basis, x_b);
return SimplexOutcome::Timeout(obj);
}
reduced_costs =
compute_reduced_costs(a, c, &mut basis_mgr, &is_basic, n_price, m, basis);
continue;
}
let step = x_b[leaving_row] / pivot_element;
for i in 0..m {
x_b[i] -= alpha_dense[i] * step;
}
x_b[leaving_row] = step;
for val in x_b.iter_mut() {
if val.abs() < options.clamp_tol {
*val = 0.0;
}
}
let leaving_col = basis[leaving_row];
for j in 0..n_price {
if !is_basic[j] {
reduced_costs[j] -= theta * trow[j];
}
}
if leaving_col < n_price {
reduced_costs[leaving_col] = -theta;
}
if leaving_col < n_price {
is_basic[leaving_col] = false;
}
is_basic[entering_col] = true;
basis_mgr.update(entering_col, leaving_row, &alpha_sv);
basis[leaving_row] = entering_col;
if basis_mgr_needs_refactor_approx(_iter) {
basis_mgr.refactor_if_needed_timed(a, basis, options.deadline);
if basis_mgr.refactor_failed {
if basis_mgr.singular_basis {
return SimplexOutcome::SingularBasis;
}
let obj: f64 = basic_obj(c, basis, x_b);
return SimplexOutcome::Timeout(obj);
}
reduced_costs =
compute_reduced_costs(a, c, &mut basis_mgr, &is_basic, n_price, m, basis);
}
}
let obj: f64 = basic_obj(c, basis, x_b);
SimplexOutcome::Timeout(obj)
}
#[inline]
fn basis_mgr_needs_refactor_approx(iter: usize) -> bool {
iter % 50 == 49
}
fn dual_ratio_test(
trow: &[f64],
reduced_costs: &[f64],
is_basic: &[bool],
n_price: usize,
pivot_tol: f64,
) -> Option<(usize, f64)> {
let mut min_ratio = f64::INFINITY;
let mut entering = None;
for j in 0..n_price {
if is_basic[j] { continue; }
if trow[j] > pivot_tol {
let ratio = reduced_costs[j] / trow[j];
if ratio < min_ratio - pivot_tol {
min_ratio = ratio;
entering = Some(j);
} else if (ratio - min_ratio).abs() <= pivot_tol {
if let Some(prev_j) = entering {
if j < prev_j {
entering = Some(j);
}
}
}
}
}
entering.map(|j| (j, min_ratio))
}
#[cfg(test)]
mod tests {
use crate::options::{SimplexMethod, SolverOptions};
use crate::problem::{LpProblem, SolveStatus};
use crate::simplex::solve_with;
use crate::sparse::CscMatrix;
use crate::test_kkt::{assert_kkt_optimal_with, dfeas_rel_bound, pfeas_abs, EPS_KKT};
use crate::tolerances::PIVOT_TOL;
fn make_lp(
c: Vec<f64>,
rows: &[usize],
cols: &[usize],
vals: &[f64],
nrows: usize,
ncols: usize,
b: Vec<f64>,
) -> LpProblem {
let a = CscMatrix::from_triplets(rows, cols, vals, nrows, ncols).unwrap();
LpProblem::new(c, a, b).unwrap()
}
#[test]
fn test_dual_basic_nonneg_cost() {
let lp = make_lp(
vec![1.0, 2.0],
&[0, 0, 1, 2],
&[0, 1, 0, 1],
&[1.0, 1.0, 1.0, 1.0],
3,
2,
vec![4.0, 3.0, 3.0],
);
let opts = SolverOptions {
simplex_method: SimplexMethod::Dual,
..SolverOptions::default()
};
assert_kkt_optimal_with(&lp, 0.0, "test_dual_basic_nonneg_cost", &opts);
}
#[test]
fn test_dual_matches_primal() {
let lp = make_lp(
vec![-1.0, -2.0],
&[0, 0, 1, 2],
&[0, 1, 0, 1],
&[1.0, 1.0, 1.0, 1.0],
3,
2,
vec![4.0, 3.0, 3.0],
);
let primal_opts = SolverOptions {
simplex_method: SimplexMethod::Primal,
..SolverOptions::default()
};
let dual_opts = SolverOptions {
simplex_method: SimplexMethod::Dual,
..SolverOptions::default()
};
assert_kkt_optimal_with(&lp, -7.0, "test_dual_matches_primal/primal", &primal_opts);
assert_kkt_optimal_with(&lp, -7.0, "test_dual_matches_primal/dual", &dual_opts);
let result_p = solve_with(&lp, &primal_opts);
let result_d = solve_with(&lp, &dual_opts);
assert!(
(result_p.objective - result_d.objective).abs() < 1e-6,
"Primal obj={}, Dual obj={}",
result_p.objective,
result_d.objective
);
}
#[test]
fn test_dual_warm_start_rhs_change() {
let lp1 = make_lp(
vec![-1.0, -2.0],
&[0, 0, 1, 2],
&[0, 1, 0, 1],
&[1.0, 1.0, 1.0, 1.0],
3,
2,
vec![4.0, 3.0, 3.0],
);
let result1 = solve_with(&lp1, &SolverOptions::default());
assert_eq!(result1.status, SolveStatus::Optimal);
assert!(result1.warm_start_basis.is_some());
let lp2 = make_lp(
vec![-1.0, -2.0],
&[0, 0, 1, 2],
&[0, 1, 0, 1],
&[1.0, 1.0, 1.0, 1.0],
3,
2,
vec![5.0, 3.0, 3.0],
);
let opts_warm = SolverOptions {
warm_start: result1.warm_start_basis.clone(),
simplex_method: SimplexMethod::Dual,
..SolverOptions::default()
};
assert_kkt_optimal_with(&lp2, -8.0, "test_dual_warm_start_rhs_change", &opts_warm);
}
#[test]
fn test_dual_simplex_method_option() {
let lp = make_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![4.0, 3.0, 3.0],
);
let opts = SolverOptions {
simplex_method: SimplexMethod::Dual,
..SolverOptions::default()
};
assert_kkt_optimal_with(&lp, -4.0, "test_dual_simplex_method_option", &opts);
}
#[test]
fn test_dual_warm_start_preserves_dual_feasibility() {
let lp1 = make_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![6.0, 4.0, 4.0],
);
let opts1 = SolverOptions {
recover_warm_start_basis: true,
..SolverOptions::default()
};
let result1 = solve_with(&lp1, &opts1);
assert_eq!(result1.status, SolveStatus::Optimal);
assert!(result1.warm_start_basis.is_some());
let lp2 = make_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![5.0, 3.0, 3.0],
);
let opts_warm = SolverOptions {
warm_start: result1.warm_start_basis.clone(),
simplex_method: SimplexMethod::Dual,
..SolverOptions::default()
};
let result2 = solve_with(&lp2, &opts_warm);
assert_eq!(result2.status, SolveStatus::Optimal);
let pf = pfeas_abs(&lp2.a, &lp2.b, &lp2.constraint_types, &result2.solution);
assert!(pf < EPS_KKT, "pfeas={:.3e} > {:.3e}", pf, EPS_KKT);
let df = dfeas_rel_bound(&lp2.c, &lp2.bounds, &result2.solution, &result2.reduced_costs);
assert!(df < EPS_KKT, "dfeas_rel_bound={:.3e} > {:.3e}", df, EPS_KKT);
for &rc in &result2.reduced_costs {
assert!(rc >= -PIVOT_TOL, "rc={} < -PIVOT_TOL", rc);
}
}
#[test]
fn test_dual_simplex_timeout() {
let n = 200usize;
let m = 100usize;
let mut rows = Vec::new();
let mut cols = Vec::new();
let mut vals = Vec::new();
for i in 0..m {
for j in 0..n {
rows.push(i);
cols.push(j);
vals.push(1.0);
}
}
let lp = make_lp(vec![-1.0; n], &rows, &cols, &vals, m, n, vec![1.0; m]);
let opts = SolverOptions {
simplex_method: SimplexMethod::Dual,
deadline: Some(std::time::Instant::now() - std::time::Duration::from_secs(1)),
..SolverOptions::default()
};
let result = solve_with(&lp, &opts);
assert_eq!(result.status, SolveStatus::Timeout);
}
#[test]
fn test_dual_singular_basis_not_optimal() {
use crate::simplex::dual::dual_simplex_core;
use crate::simplex::SimplexOutcome;
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 2, 2).unwrap();
let c = vec![0.0, 0.0];
let mut x_b = vec![1.0, 0.0];
let mut basis = vec![0usize, 0];
let opts = SolverOptions::default();
let mut iters = 0usize;
let outcome = dual_simplex_core(
&a, &mut x_b, &c, &mut basis, 2, 2, &opts, &mut iters,
);
assert!(!matches!(outcome, SimplexOutcome::Optimal(..)));
assert!(matches!(outcome, SimplexOutcome::Timeout(..) | SimplexOutcome::SingularBasis));
}
}