use super::{extract_dual_info, extract_solution, SimplexOutcome, StandardForm};
use crate::basis::{BasisManager, LuBasis};
use crate::options::{SolverOptions, WarmStartBasis};
use crate::problem::{LpProblem, SolveStatus, SolverResult};
use crate::sparse::{CscMatrix, SparseVec};
use std::sync::atomic::{AtomicBool, Ordering};
pub(super) fn compute_dual_vars_into(
c: &[f64],
basis_mgr: &mut LuBasis,
basis: &[usize],
y_out: &mut [f64],
) {
debug_assert_eq!(y_out.len(), basis.len());
for (i, slot) in y_out.iter_mut().enumerate() {
*slot = c[basis[i]];
}
basis_mgr.btran_dense(y_out);
}
pub(super) fn compute_dual_vars(
c: &[f64],
basis_mgr: &mut LuBasis,
basis: &[usize],
m: usize,
) -> Vec<f64> {
let mut y = vec![0.0f64; m];
debug_assert_eq!(basis.len(), m);
compute_dual_vars_into(c, basis_mgr, basis, &mut y);
y
}
pub(super) fn compute_reduced_costs_into(
a: &CscMatrix,
c: &[f64],
basis_mgr: &mut LuBasis,
is_basic: &[bool],
n_price: usize,
basis: &[usize],
y_buf: &mut [f64],
rc_out: &mut [f64],
) {
debug_assert_eq!(rc_out.len(), n_price);
compute_dual_vars_into(c, basis_mgr, basis, y_buf);
for j in 0..n_price {
if is_basic[j] {
rc_out[j] = 0.0;
continue;
}
let (rows, vals) = a.get_column(j).unwrap();
let mut ya = 0.0;
for (k, &row) in rows.iter().enumerate() {
ya += y_buf[row] * vals[k];
}
rc_out[j] = c[j] - ya;
}
}
pub(super) fn compute_reduced_costs(
a: &CscMatrix,
c: &[f64],
basis_mgr: &mut LuBasis,
is_basic: &[bool],
n_price: usize,
m: usize,
basis: &[usize],
) -> Vec<f64> {
let mut y = vec![0.0f64; m];
let mut reduced_costs = vec![0.0f64; n_price];
compute_reduced_costs_into(
a,
c,
basis_mgr,
is_basic,
n_price,
basis,
&mut y,
&mut reduced_costs,
);
reduced_costs
}
#[allow(clippy::too_many_arguments)]
pub(super) fn outcome_to_result(
outcome: SimplexOutcome,
sf: &StandardForm,
problem: &LpProblem,
basis: &[usize],
x_b: &[f64],
col_scale: &[f64],
row_scale: &[f64],
dual_unbounded_is_infeasible: bool,
) -> 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 => {
if dual_unbounded_is_infeasible {
SolverResult {
status: SolveStatus::Infeasible,
objective: f64::INFINITY,
solution: vec![],
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
..Default::default()
}
} else {
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 => SolverResult::numerical_error(),
}
}
pub(super) fn lp_unbounded_ray_verified(
a: &CscMatrix,
basis: &[usize],
c: &[f64],
m: usize,
n_price: usize,
n_enter: usize,
options: &SolverOptions,
) -> bool {
let mut basis_mgr = match LuBasis::new_timed(a, basis, options.max_etas, options.deadline) {
Ok(bm) => bm,
Err(_) => return false,
};
let mut in_basis = vec![false; n_price];
for &col in basis {
if col < n_price {
in_basis[col] = true;
}
}
let mut y: Vec<f64> = basis.iter().map(|&col| c[col]).collect();
basis_mgr.btran_dense(&mut y);
for q in 0..n_enter {
if in_basis[q] {
continue;
}
let Ok((rows, vals)) = a.get_column(q) else {
continue;
};
let rc = c[q]
- rows
.iter()
.zip(vals.iter())
.map(|(&r, &v)| v * y[r])
.sum::<f64>();
if rc >= -options.dual_tol {
continue;
}
let mut d_sv = SparseVec {
indices: rows.to_vec(),
values: vals.to_vec(),
len: m,
};
basis_mgr.ftran(&mut d_sv);
let d = d_sv.to_dense();
let scale = d.iter().map(|v| v.abs()).fold(1.0_f64, f64::max);
let ray_floor = f64::EPSILON * scale;
let ray_ok = d.iter().enumerate().all(|(i, &di)| {
if basis[i] >= n_enter {
di.abs() <= ray_floor
} else {
di <= ray_floor
}
});
if ray_ok {
return true;
}
}
false
}
pub(super) fn basic_obj(c: &[f64], basis: &[usize], x_b: &[f64]) -> f64 {
debug_assert_eq!(basis.len(), x_b.len());
basis.iter().zip(x_b.iter()).map(|(&j, &v)| c[j] * v).sum()
}
pub(super) const NO_PROGRESS_TRIGGER_FACTOR: usize = 3;
pub(super) const NO_PROGRESS_MIN: usize = 100;
pub(super) const NO_PROGRESS_REL_EPS: f64 = 1e-12;
pub(super) fn made_progress_with_floor(best: f64, current: f64, floor: f64) -> bool {
best - current > best.abs().max(floor) * NO_PROGRESS_REL_EPS
}
const GAMMA_DEADLINE_CHECK_INTERVAL: usize = 10;
pub(super) fn recompute_gamma_truth(
basis_mgr: &mut LuBasis,
m: usize,
deadline: Option<std::time::Instant>,
cancel_flag: Option<&AtomicBool>,
) -> Option<Vec<f64>> {
let mut gamma_truth = vec![0.0f64; m];
let mut e_i = vec![0.0f64; m];
let mut rho_i = vec![0.0f64; m];
for i in 0..m {
if i % GAMMA_DEADLINE_CHECK_INTERVAL == 0 {
if deadline.is_some_and(|d| std::time::Instant::now() >= d) {
return None;
}
if cancel_flag.is_some_and(|f| f.load(Ordering::Relaxed)) {
return None;
}
}
e_i.iter_mut().for_each(|v| *v = 0.0);
e_i[i] = 1.0;
let mut sv = SparseVec::from_dense(&e_i);
basis_mgr.btran(&mut sv);
sv.to_dense_into(&mut rho_i);
gamma_truth[i] = rho_i.iter().map(|&v| v * v).sum();
}
Some(gamma_truth)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::basis::LuBasis;
use crate::sparse::CscMatrix;
fn make_identity_plus(n: usize, m: usize) -> (CscMatrix, Vec<f64>, Vec<usize>) {
let mut rows: Vec<usize> = Vec::new();
let mut cols: Vec<usize> = Vec::new();
let mut vals: Vec<f64> = Vec::new();
for j in 0..m {
rows.push(j);
cols.push(j);
vals.push(1.0);
}
for j in m..n {
rows.push((j - m) % m);
cols.push(j);
vals.push(2.0);
}
let a = CscMatrix::from_triplets(&rows, &cols, &vals, m, n).unwrap();
let basis: Vec<usize> = (0..m).collect();
let c: Vec<f64> = (0..n).map(|j| (j as f64) + 1.0).collect();
(a, c, basis)
}
#[test]
fn dual_vars_identity_basis_returns_c_b() {
let m = 4;
let (a, c, basis) = make_identity_plus(m + 3, m);
let mut bm = LuBasis::new(&a, &basis, 32).unwrap();
let y = compute_dual_vars(&c, &mut bm, &basis, m);
for i in 0..m {
assert!(
(y[i] - c[i]).abs() < 1e-12,
"y[{}] = {} expected {}",
i,
y[i],
c[i]
);
}
}
#[test]
fn dual_vars_into_matches_allocating_and_is_reuse_safe() {
let m = 4;
let (a, c, basis) = make_identity_plus(m + 3, m);
let mut bm = LuBasis::new(&a, &basis, 32).unwrap();
let y_alloc = compute_dual_vars(&c, &mut bm, &basis, m);
let mut y_into = vec![999.0f64; m];
compute_dual_vars_into(&c, &mut bm, &basis, &mut y_into);
for i in 0..m {
assert!((y_into[i] - y_alloc[i]).abs() < 1e-14);
}
for slot in y_into.iter_mut() {
*slot = -42.0;
}
compute_dual_vars_into(&c, &mut bm, &basis, &mut y_into);
for i in 0..m {
assert!((y_into[i] - y_alloc[i]).abs() < 1e-14);
}
}
#[test]
fn reduced_costs_identity_basis_match_closed_form() {
let m = 3;
let n = m + 3;
let (a, c, basis) = make_identity_plus(n, m);
let is_basic: Vec<bool> = (0..n).map(|j| j < m).collect();
let mut bm = LuBasis::new(&a, &basis, 32).unwrap();
let r = compute_reduced_costs(&a, &c, &mut bm, &is_basic, n, m, &basis);
for j in 0..m {
assert_eq!(r[j], 0.0);
}
for j in m..n {
let expected = c[j] - 2.0 * c[(j - m) % m];
assert!(
(r[j] - expected).abs() < 1e-12,
"r[{}] = {} expected {}",
j,
r[j],
expected
);
}
}
#[test]
fn reduced_costs_into_matches_allocating_and_clears_basic_slots() {
let m = 3;
let n = m + 3;
let (a, c, basis) = make_identity_plus(n, m);
let is_basic: Vec<bool> = (0..n).map(|j| j < m).collect();
let mut bm = LuBasis::new(&a, &basis, 32).unwrap();
let r_alloc = compute_reduced_costs(&a, &c, &mut bm, &is_basic, n, m, &basis);
let mut y_buf = vec![0.0f64; m];
let mut rc_out = vec![123.456f64; n];
compute_reduced_costs_into(
&a,
&c,
&mut bm,
&is_basic,
n,
&basis,
&mut y_buf,
&mut rc_out,
);
for j in 0..n {
assert!((rc_out[j] - r_alloc[j]).abs() < 1e-14, "j={}", j);
}
for j in 0..m {
assert_eq!(rc_out[j], 0.0, "basic slot {} not zeroed", j);
}
}
#[test]
fn reduced_costs_zero_cost_yields_zero_vector() {
let m = 3;
let n = m + 2;
let (a, _c, basis) = make_identity_plus(n, m);
let c = vec![0.0f64; n];
let is_basic: Vec<bool> = (0..n).map(|j| j < m).collect();
let mut bm = LuBasis::new(&a, &basis, 32).unwrap();
let r = compute_reduced_costs(&a, &c, &mut bm, &is_basic, n, m, &basis);
for &rj in &r {
assert!(rj.abs() < 1e-14, "r = {:?} should be all zero", r);
}
}
#[test]
fn dual_vars_permuted_basis_uses_basis_indexing() {
let m = 3;
let n = m;
let rows: Vec<usize> = (0..m).collect();
let cols: Vec<usize> = (0..m).collect();
let vals: Vec<f64> = vec![1.0; m];
let a = CscMatrix::from_triplets(&rows, &cols, &vals, m, n).unwrap();
let basis = vec![2usize, 0, 1];
let c = vec![10.0, 20.0, 30.0];
let mut bm = LuBasis::new(&a, &basis, 32).unwrap();
let y = compute_dual_vars(&c, &mut bm, &basis, m);
for i in 0..m {
let (rs, vs) = a.get_column(basis[i]).unwrap();
let mut dot = 0.0;
for (k, &row) in rs.iter().enumerate() {
dot += y[row] * vs[k];
}
assert!(
(dot - c[basis[i]]).abs() < 1e-12,
"y^T a_{{basis[{}]}} = {} expected {}",
i,
dot,
c[basis[i]]
);
}
}
#[test]
fn basic_obj_identity_basis() {
let m = 4;
let (_a, c, basis) = make_identity_plus(m + 2, m);
let x_b = vec![1.0, 2.0, 3.0, 4.0];
let obj = basic_obj(&c, &basis, &x_b);
let expected: f64 = (0..m).map(|i| c[basis[i]] * x_b[i]).sum();
assert!((obj - expected).abs() < 1e-14);
assert!((obj - 30.0).abs() < 1e-14);
}
#[test]
fn basic_obj_permuted_basis() {
let basis = vec![2usize, 0, 1];
let c = vec![10.0, 20.0, 30.0];
let x_b = vec![1.0, 2.0, 3.0];
let obj = basic_obj(&c, &basis, &x_b);
assert!((obj - 110.0).abs() < 1e-14);
}
#[test]
fn basic_obj_empty_basis() {
let c = vec![1.0, 2.0, 3.0];
let basis: Vec<usize> = vec![];
let x_b: Vec<f64> = vec![];
assert_eq!(basic_obj(&c, &basis, &x_b), 0.0);
}
#[test]
fn basic_obj_negative_x_b_signs_preserved() {
let c = vec![1.0, 2.0, 3.0];
let basis = vec![0usize, 1, 2];
let x_b = vec![-1.0, 2.0, -3.0];
assert!((basic_obj(&c, &basis, &x_b) - (-6.0)).abs() < 1e-14);
}
#[test]
fn dse_expired_deadline_during_gamma_loop_returns_timeout() {
let m = 4;
let (a, _c, basis) = make_identity_plus(m + 2, m);
let mut bm = LuBasis::new(&a, &basis, 32).unwrap();
let past = std::time::Instant::now() - std::time::Duration::from_secs(1);
let result = recompute_gamma_truth(&mut bm, m, Some(past), None);
assert!(
result.is_none(),
"expired deadline must short-circuit the BTRAN loop and return None; \
got Some (deadline check is missing or not firing at i=0)"
);
let result_no_dl = recompute_gamma_truth(&mut bm, m, None, None);
assert!(
result_no_dl.is_some(),
"None deadline must always return Some"
);
}
#[test]
fn recompute_gamma_truth_cancellation_during_sweep() {
use std::sync::Arc;
let m = 4;
let (a, _c, basis) = make_identity_plus(m + 2, m);
let mut bm = LuBasis::new(&a, &basis, 32).unwrap();
let flag = Arc::new(AtomicBool::new(true));
let result = recompute_gamma_truth(&mut bm, m, None, Some(&flag));
assert!(
result.is_none(),
"pre-set cancel_flag must abort the BTRAN sweep and return None; \
got Some (cancel_flag check is missing or not firing)"
);
flag.store(false, Ordering::Relaxed);
let result_ok = recompute_gamma_truth(&mut bm, m, None, Some(&flag));
assert!(
result_ok.is_some(),
"cleared cancel_flag must allow sweep to complete"
);
let result_no_flag = recompute_gamma_truth(&mut bm, m, None, None);
assert!(
result_no_flag.is_some(),
"None cancel_flag must always return Some"
);
}
#[test]
fn made_progress_threshold_boundary() {
assert!(
made_progress_with_floor(1.0, 0.0, 0.0),
"1.0 → 0.0 is clear improvement; must return true"
);
let eps = NO_PROGRESS_REL_EPS;
let best = 1.0_f64;
let at_boundary = best - best.abs() * eps;
assert!(
!made_progress_with_floor(best, at_boundary, 0.0),
"improvement == eps * |best| is not strictly above threshold; must return false"
);
let above_boundary = best - best.abs() * eps - 1e-15;
assert!(
made_progress_with_floor(best, above_boundary, 0.0),
"improvement slightly above eps * |best| must return true"
);
assert!(
!made_progress_with_floor(1.0, 1.0, 0.0),
"no improvement must return false"
);
assert!(
!made_progress_with_floor(0.0, 0.0, 0.0),
"best == 0, current == 0: no improvement must return false"
);
}
#[test]
fn made_progress_with_floor_protects_near_zero_best() {
assert!(
!made_progress_with_floor(0.0, -0.5e-12, 1.0),
"near-zero best, floor=1.0: improvement below floor*eps must be rejected"
);
assert!(
made_progress_with_floor(0.0, -0.5e-12, 0.0),
"best=0, current=-0.5e-12, floor=0: any positive improvement must return true"
);
assert!(
made_progress_with_floor(0.0, -1.5e-12, 1.0),
"floor=1.0, improvement > floor*eps must pass"
);
assert!(
made_progress_with_floor(1.0, 0.0, 0.0),
"floor=0.0, best=1.0, current=0.0: clear improvement must return true"
);
}
#[test]
fn lp_unbounded_ray_verified_distinguishes_genuine_bounded_and_artificial() {
let opts = crate::options::SolverOptions::default();
let a_unb = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[-1.0, 1.0], 1, 2).unwrap();
assert!(
lp_unbounded_ray_verified(&a_unb, &[1], &[-1.0, 0.0], 1, 2, 2, &opts),
"genuine unbounded ray must verify (else true-Unbounded demoted to Timeout)"
);
let a_bnd = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
assert!(
!lp_unbounded_ray_verified(&a_bnd, &[1], &[-1.0, 0.0], 1, 2, 2, &opts),
"bounded LP must NOT verify a ray (else eta-drift false-Unbounded survives)"
);
let a_art =
CscMatrix::from_triplets(&[0, 0, 0], &[0, 1, 2], &[-1.0, 1.0, -1.0], 1, 3).unwrap();
assert!(
!lp_unbounded_ray_verified(&a_art, &[1], &[0.0, 0.0, -1.0], 1, 3, 2, &opts),
"a column ≥ n_enter (artificial) must be excluded from the recession ray"
);
let a_smallpiv =
CscMatrix::from_triplets(&[0, 0], &[0, 1], &[5e-9, 1.0], 1, 2).unwrap();
assert!(
!lp_unbounded_ray_verified(&a_smallpiv, &[1], &[-1.0, 0.0], 1, 2, 2, &opts),
"CONCERN B: a small positive pivot (0<d<PIVOT_TOL) is a leaving row ⇒ bounded, not a ray"
);
}
#[test]
fn lp_unbounded_ray_verified_rejects_ray_that_increases_basic_artificial() {
let opts = crate::options::SolverOptions::default();
let a = CscMatrix::from_triplets(&[0, 1, 1], &[0, 1, 2], &[1.0, -1.0, 1.0], 2, 3).unwrap();
assert!(
!lp_unbounded_ray_verified(&a, &[0, 2], &[0.0, -1.0, 0.0], 2, 3, 2, &opts),
"a direction that increases a basic artificial off 0 must NOT verify as a recession ray"
);
}
}