use crate::basis::{BasisManager, LuBasis};
use crate::options::SolverOptions;
use crate::sparse::{CscMatrix, SparseVec};
use crate::tolerances::PIVOT_TOL;
use super::ratio_test::{RatioTestStrategy, HarrisRatioTest, bland_ratio_test};
use super::super::SimplexOutcome;
use super::super::dual_common::{basic_obj, compute_dual_vars, compute_reduced_costs};
use super::super::pricing::DualLeavingStrategy;
use std::sync::atomic::Ordering;
const NO_PROGRESS_TRIGGER_FACTOR: usize = 3;
const NO_PROGRESS_MIN: usize = 100;
const BLAND_ITER_CAP_FACTOR: usize = 10;
const NO_PROGRESS_REL_EPS: f64 = 1e-12;
const LEX_PERTURB_REL: f64 = 1e-4;
fn recompute_gamma_truth(basis_mgr: &mut LuBasis, m: usize) -> 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 {
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();
}
gamma_truth
}
fn apply_lex_perturbation(
reduced_costs: &mut [f64],
is_basic: &[bool],
x_b: &mut [f64],
m: usize,
perturb_x: bool,
) {
let scale_r = reduced_costs
.iter()
.map(|v| v.abs())
.fold(0.0_f64, f64::max)
.max(1.0);
let base_r = LEX_PERTURB_REL * scale_r;
let n_price = reduced_costs.len();
for (j, slot) in reduced_costs.iter_mut().enumerate() {
if !is_basic[j] {
*slot += base_r * (1.0 + (j as f64) / (n_price as f64));
}
}
if perturb_x {
let scale_x = x_b.iter().map(|v| v.abs()).fold(0.0_f64, f64::max).max(1.0);
let base_x = LEX_PERTURB_REL * scale_x;
for (i, slot) in x_b.iter_mut().enumerate() {
*slot += base_x * (1.0 + (i as f64) / (m as f64));
}
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn dual_simplex_core_advanced(
a: &CscMatrix,
x_b: &mut [f64],
c: &[f64],
basis: &mut [usize],
m: usize,
n_price: usize,
options: &SolverOptions,
leaving: &mut dyn DualLeavingStrategy,
iter_count_out: &mut usize,
) -> SimplexOutcome {
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 ratio_tester = HarrisRatioTest::new(options.dual_tol, PIVOT_TOL);
let mut rho_dense = vec![0.0f64; m];
let mut trow = vec![0.0f64; n_price];
let mut alpha_dense = vec![0.0f64; m];
let needs_sigma = leaving.needs_sigma();
let mut sigma_dense = if needs_sigma {
vec![0.0f64; m]
} else {
Vec::new()
};
if needs_sigma {
let gamma_truth = recompute_gamma_truth(&mut basis_mgr, m);
leaving.set_initial_gamma(&gamma_truth);
}
let k_trigger = (NO_PROGRESS_TRIGGER_FACTOR * m).max(NO_PROGRESS_MIN);
let mut best_infeas = leaving.progress_metric(x_b, basis);
let mut iters_since_progress: usize = 0;
let mut bland_mode = false;
let mut bland_start_iter: usize = 0;
loop {
*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);
}
if bland_mode
&& *iter_count_out - bland_start_iter > BLAND_ITER_CAP_FACTOR * n_price
{
let obj: f64 = basic_obj(c, basis, x_b);
return SimplexOutcome::Timeout(obj);
}
let leaving_pick = if bland_mode {
leaving.bland_leaving(x_b, options.primal_tol, basis)
} else {
leaving.select_leaving(x_b, options.primal_tol, basis)
};
let leaving_row = match leaving_pick {
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 ratio_pick = if bland_mode {
bland_ratio_test(&trow, &reduced_costs, &is_basic, n_price, PIVOT_TOL)
} else {
ratio_tester.select_entering(&trow, &reduced_costs, &is_basic, n_price)
};
let (entering_col, theta) = match ratio_pick {
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 {
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);
if bland_mode {
apply_lex_perturbation(&mut reduced_costs, &is_basic, x_b, m, false);
}
leaving.after_refactor(m);
continue;
}
if needs_sigma {
let mut sigma_sv = SparseVec::from_dense(&rho_dense);
basis_mgr.ftran(&mut sigma_sv);
sigma_sv.to_dense_into(&mut sigma_dense);
}
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;
}
leaving.after_pivot(leaving_row, &alpha_dense, &sigma_dense, pivot_element);
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() {
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);
if bland_mode {
apply_lex_perturbation(&mut reduced_costs, &is_basic, x_b, m, false);
}
leaving.after_refactor(m);
if needs_sigma {
let gamma_truth = recompute_gamma_truth(&mut basis_mgr, m);
leaving.set_initial_gamma(&gamma_truth);
}
}
if !bland_mode {
let current = leaving.progress_metric(x_b, basis);
let threshold = best_infeas * (1.0 - NO_PROGRESS_REL_EPS);
if current < threshold {
best_infeas = current;
iters_since_progress = 0;
} else {
iters_since_progress = iters_since_progress.saturating_add(1);
if iters_since_progress >= k_trigger {
bland_mode = true;
bland_start_iter = *iter_count_out;
apply_lex_perturbation(&mut reduced_costs, &is_basic, x_b, m, true);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn b4_perturb_x_false_does_not_modify_xb() {
let is_basic = vec![false, false, false, false];
{
let mut rc = vec![1.0, 2.0, 3.0, 0.5];
let mut x_b = vec![0.5_f64, 1.0, 0.3];
let m = x_b.len();
apply_lex_perturbation(&mut rc, &is_basic, &mut x_b, m, true);
let x_b_after_entry = x_b.clone();
assert_ne!(x_b_after_entry, vec![0.5, 1.0, 0.3], "initial entry must perturb x_b");
let mut rc2 = vec![1.0, 2.0, 3.0, 0.5];
apply_lex_perturbation(&mut rc2, &is_basic, &mut x_b, m, false);
assert_eq!(x_b, x_b_after_entry,
"Pattern A: x_b must not change with perturb_x=false (B4 バグ復帰で FAIL)");
let mut rc3 = vec![1.0, 2.0, 3.0, 0.5];
apply_lex_perturbation(&mut rc3, &is_basic, &mut x_b, m, false);
assert_eq!(x_b, x_b_after_entry,
"Pattern A: x_b must remain stable across repeated perturb_x=false calls");
}
{
let mut rc = vec![100.0, 200.0, 50.0, 1.0];
let mut x_b = vec![1000.0_f64, 500.0, 750.0];
let m = x_b.len();
apply_lex_perturbation(&mut rc, &is_basic, &mut x_b, m, true);
let x_b_after_entry = x_b.clone();
assert_ne!(x_b_after_entry, vec![1000.0, 500.0, 750.0], "initial entry must perturb x_b");
for call_n in 0..3 {
let mut rc_n = vec![100.0, 200.0, 50.0, 1.0];
apply_lex_perturbation(&mut rc_n, &is_basic, &mut x_b, m, false);
assert_eq!(x_b, x_b_after_entry,
"Pattern B: x_b must not grow after refactor call #{} (B4 バグ復帰で FAIL)", call_n);
}
}
}
}