use super::super::dual_common::{
basic_obj, compute_dual_vars, made_progress_with_floor, recompute_gamma_truth, NO_PROGRESS_MIN,
NO_PROGRESS_TRIGGER_FACTOR,
};
use super::super::pricing::DualLeavingStrategy;
use super::super::trace::IterTrace;
use super::super::SimplexOutcome;
use super::ratio_test::{bland_ratio_test, HarrisRatioTest, RatioTestStrategy};
use crate::basis::{BasisManager, LuBasis};
use crate::options::SolverOptions;
use crate::sparse::{CscMatrix, SparseVec};
use crate::tolerances::PIVOT_TOL;
use std::collections::HashMap;
use std::sync::atomic::Ordering;
use super::deadline_expired;
const LEX_PERTURB_REL: f64 = 1e-4;
#[derive(Clone, Copy, Debug, Default)]
struct LexPerturbStats {
delta: f64,
effect: f64,
}
#[derive(Default)]
struct BasisCycleDetector {
seen: HashMap<Vec<usize>, usize>,
}
impl BasisCycleDetector {
fn repeated(&mut self, iter: usize, basis: &[usize]) -> bool {
self.seen.insert(basis.to_vec(), iter).is_some()
}
fn clear(&mut self) {
self.seen.clear();
}
}
fn collect_bland_ratio_candidates(
trow: &[f64],
reduced_costs: &[f64],
is_basic: &[bool],
n_price: usize,
pivot_tol: f64,
) -> (Vec<usize>, Vec<f64>) {
let mut candidates = Vec::new();
let mut ratios = Vec::new();
for j in 0..n_price {
if is_basic[j] || trow[j] <= pivot_tol {
continue;
}
let ratio = reduced_costs[j] / trow[j];
if ratio >= pivot_tol {
candidates.push(j);
ratios.push(ratio);
}
}
(candidates, ratios)
}
fn collect_harris_ratio_candidates(
trow: &[f64],
reduced_costs: &[f64],
is_basic: &[bool],
n_price: usize,
harris_tol: f64,
pivot_tol: f64,
) -> (Vec<usize>, Vec<f64>) {
let mut theta_max = f64::INFINITY;
for j in 0..n_price {
if is_basic[j] || trow[j] <= pivot_tol {
continue;
}
let relaxed_ratio = (reduced_costs[j] + harris_tol) / trow[j];
if relaxed_ratio < theta_max {
theta_max = relaxed_ratio;
}
}
if !theta_max.is_finite() {
return (Vec::new(), Vec::new());
}
let mut candidates = Vec::new();
let mut ratios = Vec::new();
for j in 0..n_price {
if is_basic[j] || trow[j] <= pivot_tol {
continue;
}
let ratio = reduced_costs[j] / trow[j];
if ratio <= theta_max {
candidates.push(j);
ratios.push(ratio);
}
}
(candidates, ratios)
}
fn apply_lex_perturbation(
reduced_costs: &mut [f64],
is_basic: &[bool],
x_b: &mut [f64],
m: usize,
perturb_x: bool,
) -> LexPerturbStats {
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();
let mut max_rc_delta = 0.0_f64;
for (j, slot) in reduced_costs.iter_mut().enumerate() {
if !is_basic[j] {
let delta = base_r * (1.0 + (j as f64) / (n_price as f64));
*slot += delta;
max_rc_delta = max_rc_delta.max(delta.abs());
}
}
let mut max_x_delta = 0.0_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() {
let delta = base_x * (1.0 + (i as f64) / (m as f64));
*slot += delta;
max_x_delta = max_x_delta.max(delta.abs());
}
return LexPerturbStats {
delta: base_r,
effect: max_rc_delta.max(max_x_delta),
};
}
LexPerturbStats {
delta: base_r,
effect: max_rc_delta,
}
}
const CORE_RC_DEADLINE_CHECK_INTERVAL: usize = 512;
#[cfg(test)]
thread_local! {
pub(crate) static CORE_RC_DEADLINE_CHECK_COUNT: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
}
fn compute_reduced_costs_timed(
a: &CscMatrix,
c: &[f64],
basis_mgr: &mut LuBasis,
is_basic: &[bool],
n_price: usize,
m: usize,
basis: &[usize],
deadline: Option<std::time::Instant>,
) -> Option<Vec<f64>> {
if deadline_expired(deadline) {
return None;
}
let y = compute_dual_vars(c, basis_mgr, basis, m);
let mut reduced_costs = vec![0.0f64; n_price];
let mut j = 0;
while j < n_price {
if deadline_expired(deadline) {
return None;
}
#[cfg(test)]
CORE_RC_DEADLINE_CHECK_COUNT.with(|c| c.set(c.get() + 1));
let end = (j + CORE_RC_DEADLINE_CHECK_INTERVAL).min(n_price);
for k in j..end {
if is_basic[k] {
continue;
}
let (rows, vals) = a.get_column(k).unwrap();
let mut ya = 0.0;
for (idx, &row) in rows.iter().enumerate() {
ya += y[row] * vals[idx];
}
reduced_costs[k] = c[k] - ya;
}
j = end;
}
Some(reduced_costs)
}
#[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,
n_enter: usize,
yield_on_stall: bool,
options: &SolverOptions,
leaving: &mut dyn DualLeavingStrategy,
iter_count_out: &mut usize,
) -> SimplexOutcome {
debug_assert!(n_enter <= n_price);
let mut basis_mgr = match LuBasis::new_timed(a, basis, options.max_etas, options.deadline) {
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 = match compute_reduced_costs_timed(
a,
c,
&mut basis_mgr,
&is_basic,
n_price,
m,
basis,
options.deadline,
) {
Some(rc) => rc,
None => {
let obj: f64 = basic_obj(c, basis, x_b);
return SimplexOutcome::Timeout(obj);
}
};
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 {
match recompute_gamma_truth(
&mut basis_mgr,
m,
options.deadline,
options.cancel_flag.as_deref(),
) {
None => {
let obj = basic_obj(c, basis, x_b);
return SimplexOutcome::Timeout(obj);
}
Some(gamma_truth) => 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 cycle_detector = BasisCycleDetector::default();
let mut no_candidate_refreshed_basis: Option<Vec<usize>> = None;
let mut rejected_pivot_basis: Option<Vec<usize>> = None;
let mut rejected_pivot_row: Option<usize> = None;
let mut rejected_entering = vec![false; n_price];
let mut trace = IterTrace::new("dual-advanced");
loop {
*iter_count_out = iter_count_out.saturating_add(1);
let timed_out = deadline_expired(options.deadline);
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 let Some(t) = trace.as_mut() {
let obj = basic_obj(c, basis, x_b);
t.log(*iter_count_out, obj, basis, bland_mode);
}
if !bland_mode && cycle_detector.repeated(*iter_count_out, basis) {
bland_mode = true;
}
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,
};
if rejected_pivot_basis.as_deref() != Some(&*basis)
|| rejected_pivot_row != Some(leaving_row)
{
rejected_pivot_basis = None;
rejected_pivot_row = None;
rejected_entering.fill(false);
}
let mut masked_is_basic: Vec<bool>;
let price_excluded: &[bool] = if rejected_entering.iter().any(|&v| v) {
masked_is_basic = is_basic.clone();
for (j, blocked) in rejected_entering.iter().enumerate().take(n_enter) {
if *blocked {
masked_is_basic[j] = true;
}
}
&masked_is_basic
} else {
&is_basic
};
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 deadline_expired(options.deadline) {
let obj: f64 = basic_obj(c, basis, x_b);
return SimplexOutcome::Timeout(obj);
}
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 mut lb_violation =
x_b[leaving_row] < 0.0 && leaving.allows_lb_repair() && basis[leaving_row] < n_enter;
let artificial_lb_violation =
x_b[leaving_row] < 0.0 && leaving.allows_lb_repair() && basis[leaving_row] >= n_enter;
if lb_violation {
for t in trow[..n_price].iter_mut() {
*t = -*t;
}
}
let (mut candidate_indices, mut candidate_ratios, mut ratio_pick) = if bland_mode {
let (indices, ratios) =
collect_bland_ratio_candidates(&trow, &reduced_costs, price_excluded, n_enter, PIVOT_TOL);
let pick = bland_ratio_test(&trow, &reduced_costs, price_excluded, n_enter, PIVOT_TOL);
(indices, ratios, pick)
} else {
let (indices, ratios) = collect_harris_ratio_candidates(
&trow,
&reduced_costs,
price_excluded,
n_enter,
ratio_tester.harris_tol,
ratio_tester.pivot_tol,
);
let pick = ratio_tester.select_entering(&trow, &reduced_costs, price_excluded, n_enter);
(indices, ratios, pick)
};
if ratio_pick.is_none() && artificial_lb_violation {
for t in trow[..n_price].iter_mut() {
*t = -*t;
}
lb_violation = true;
(candidate_indices, candidate_ratios, ratio_pick) = if bland_mode {
let (indices, ratios) = collect_bland_ratio_candidates(
&trow,
&reduced_costs,
price_excluded,
n_enter,
PIVOT_TOL,
);
let pick =
bland_ratio_test(&trow, &reduced_costs, price_excluded, n_enter, PIVOT_TOL);
(indices, ratios, pick)
} else {
let (indices, ratios) = collect_harris_ratio_candidates(
&trow,
&reduced_costs,
price_excluded,
n_enter,
ratio_tester.harris_tol,
ratio_tester.pivot_tol,
);
let pick =
ratio_tester.select_entering(&trow, &reduced_costs, price_excluded, n_enter);
(indices, ratios, pick)
};
}
if let Some(t) = trace.as_mut() {
t.log_ratio_test(
&candidate_indices,
&candidate_ratios,
ratio_pick.map(|(j, _)| j),
bland_mode,
);
}
let (entering_col, theta) = match ratio_pick {
None => {
if rejected_entering.iter().any(|&v| v) {
let obj: f64 = basic_obj(c, basis, x_b);
return SimplexOutcome::Timeout(obj);
}
if no_candidate_refreshed_basis.as_deref() != Some(&*basis) {
no_candidate_refreshed_basis = Some(basis.to_vec());
basis_mgr.force_refactor_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 = match compute_reduced_costs_timed(
a,
c,
&mut basis_mgr,
&is_basic,
n_price,
m,
basis,
options.deadline,
) {
Some(rc) => rc,
None => {
let obj: f64 = basic_obj(c, basis, x_b);
return SimplexOutcome::Timeout(obj);
}
};
if bland_mode {
let stats =
apply_lex_perturbation(&mut reduced_costs, &is_basic, x_b, m, false);
if let Some(t) = trace.as_mut() {
t.log_lex_perturbation(stats.delta, stats.effect);
}
}
leaving.after_refactor(m);
continue;
}
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 {
rejected_pivot_basis = Some(basis.to_vec());
rejected_pivot_row = Some(leaving_row);
if entering_col < rejected_entering.len() {
rejected_entering[entering_col] = true;
}
basis_mgr.force_refactor_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 = match compute_reduced_costs_timed(
a,
c,
&mut basis_mgr,
&is_basic,
n_price,
m,
basis,
options.deadline,
) {
Some(rc) => rc,
None => {
let obj: f64 = basic_obj(c, basis, x_b);
return SimplexOutcome::Timeout(obj);
}
};
if bland_mode {
let stats = apply_lex_perturbation(&mut reduced_costs, &is_basic, x_b, m, false);
if let Some(t) = trace.as_mut() {
t.log_lex_perturbation(stats.delta, stats.effect);
}
}
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 deadline_expired(options.deadline) {
let obj: f64 = basic_obj(c, basis, x_b);
return SimplexOutcome::Timeout(obj);
}
if !is_basic[j] {
reduced_costs[j] -= theta * trow[j];
}
}
if leaving_col < n_price {
reduced_costs[leaving_col] = if lb_violation { theta } else { -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;
no_candidate_refreshed_basis = None;
rejected_pivot_basis = None;
rejected_pivot_row = None;
rejected_entering.fill(false);
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 = match compute_reduced_costs_timed(
a,
c,
&mut basis_mgr,
&is_basic,
n_price,
m,
basis,
options.deadline,
) {
Some(rc) => rc,
None => {
let obj: f64 = basic_obj(c, basis, x_b);
return SimplexOutcome::Timeout(obj);
}
};
if bland_mode {
let stats = apply_lex_perturbation(&mut reduced_costs, &is_basic, x_b, m, false);
if let Some(t) = trace.as_mut() {
t.log_lex_perturbation(stats.delta, stats.effect);
}
}
leaving.after_refactor(m);
}
let current = leaving.progress_metric(x_b, basis);
if made_progress_with_floor(best_infeas, current, 0.0) {
best_infeas = current;
iters_since_progress = 0;
if !bland_mode {
cycle_detector.clear();
}
} else {
iters_since_progress = iters_since_progress.saturating_add(1);
if iters_since_progress >= k_trigger {
if bland_mode {
if yield_on_stall {
let obj: f64 = basic_obj(c, basis, x_b);
return SimplexOutcome::Timeout(obj);
}
iters_since_progress = 0;
} else {
bland_mode = true;
iters_since_progress = 0;
let stats =
apply_lex_perturbation(&mut reduced_costs, &is_basic, x_b, m, true);
if let Some(t) = trace.as_mut() {
t.log_lex_perturbation(stats.delta, stats.effect);
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::super::super::pricing::MostInfeasibleLeaving;
use super::*;
use crate::options::SolverOptions;
use crate::sparse::CscMatrix;
#[test]
fn basis_cycle_detector_flags_repeated_basis_and_clear_resets() {
let mut detector = BasisCycleDetector::default();
assert!(!detector.repeated(1, &[1, 2, 3]));
assert!(!detector.repeated(2, &[1, 3, 2]));
assert!(
detector.repeated(3, &[1, 2, 3]),
"revisiting an earlier basis must trigger Bland/lex anti-cycling"
);
detector.clear();
assert!(
!detector.repeated(4, &[1, 2, 3]),
"real progress clears the cycle window"
);
}
#[test]
fn warm_start_infeasible_basis_returns_unbounded_not_cycle() {
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
let c = vec![0.0_f64, 0.0];
let mut basis = vec![1usize]; let mut x_b = vec![-1.0_f64]; let opts = SolverOptions::default();
let mut leaving = MostInfeasibleLeaving;
let mut iters = 0usize;
let outcome = dual_simplex_core_advanced(
&a,
&mut x_b,
&c,
&mut basis,
1,
2,
2,
false,
&opts,
&mut leaving,
&mut iters,
);
assert!(
matches!(outcome, SimplexOutcome::Unbounded),
"warm-start lb-violation with no lb-repair candidate must yield Unbounded \
(dual infeasibility proof); got {outcome:?}. \
If Timeout: fallback retry was restored — the no-op proof triggered."
);
}
#[test]
fn n_enter_excludes_high_index_columns_from_entering() {
let a =
CscMatrix::from_triplets(&[0, 0, 0, 0], &[0, 1, 2, 3], &[1.0, 1.0, 1.0, -1.0], 1, 4)
.unwrap();
let c = vec![0.0_f64; 4];
let opts = SolverOptions::default();
let run = |n_enter: usize| {
let mut basis = vec![0usize];
let mut x_b = vec![-1.0_f64];
let mut leaving = MostInfeasibleLeaving;
let mut iters = 0usize;
dual_simplex_core_advanced(
&a, &mut x_b, &c, &mut basis, 1, 4, n_enter, false, &opts, &mut leaving, &mut iters,
)
};
assert!(
matches!(run(4), SimplexOutcome::Optimal(_, _)),
"n_enter=4 (col 3 enterable) must reach Optimal"
);
assert!(
matches!(run(2), SimplexOutcome::Unbounded),
"n_enter=2 must EXCLUDE col 3 (idx >= n_enter) from entering → Unbounded; \
got non-Unbounded ⇒ core passes n_price not n_enter (artificial re-entry ban reverted)"
);
}
#[test]
fn yield_on_stall_gated_by_caller_flag() {
use super::super::super::pricing::DualLeavingStrategy;
struct AlwaysStallLeaving;
impl DualLeavingStrategy for AlwaysStallLeaving {
fn select_leaving(&mut self, _x_b: &[f64], _t: f64, _b: &[usize]) -> Option<usize> {
Some(0)
}
fn bland_leaving(&mut self, _x_b: &[f64], _t: f64, _b: &[usize]) -> Option<usize> {
Some(0)
}
fn progress_metric(&mut self, _x_b: &[f64], _b: &[usize]) -> f64 {
1.0
}
fn allows_lb_repair(&self) -> bool {
false
}
}
let a = CscMatrix::from_triplets(&[0, 0, 0], &[0, 1, 2], &[1.0, 1.0, 1.0], 1, 3).unwrap();
let c = vec![0.0_f64, 0.0, 0.0];
let run = |yield_on_stall: bool| {
let opts = SolverOptions {
max_etas: 1,
deadline: Some(std::time::Instant::now() + std::time::Duration::from_millis(300)),
..SolverOptions::default()
};
let mut basis = vec![2usize];
let mut x_b = vec![-1.0_f64];
let mut leaving = AlwaysStallLeaving;
let mut iters = 0usize;
let out = dual_simplex_core_advanced(
&a, &mut x_b, &c, &mut basis, 1, 3, 3, yield_on_stall, &opts, &mut leaving,
&mut iters,
);
(out, iters)
};
let (out_yield, iters_yield) = run(true);
let (out_noyield, iters_noyield) = run(false);
assert!(
matches!(out_yield, SimplexOutcome::Timeout(_)),
"yield path must Timeout (stall-yield)"
);
assert!(
matches!(out_noyield, SimplexOutcome::Timeout(_)),
"no-yield path must Timeout (deadline), not stall-yield"
);
assert!(
iters_noyield > iters_yield.saturating_mul(4),
"yield_on_stall=false must NOT stall-yield: it ran only {iters_noyield} iters \
vs the yield path's {iters_yield}; near-equal ⇒ the flag is ignored and a \
standard caller would be wrongly Timeout-ed"
);
}
#[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
);
}
}
}
#[test]
fn recompute_gamma_after_refactor_not_called() {
use super::super::super::pricing::DualLeavingStrategy;
use std::cell::Cell;
struct CountingDseLeaving<'a> {
init_calls: &'a Cell<usize>,
refactor_calls: &'a Cell<usize>,
}
impl<'a> DualLeavingStrategy for CountingDseLeaving<'a> {
fn select_leaving(
&mut self,
x_b: &[f64],
primal_tol: f64,
_basis: &[usize],
) -> Option<usize> {
let mut best_row = None;
let mut max_violation = primal_tol;
for (i, &val) in x_b.iter().enumerate() {
if val < -max_violation {
max_violation = -val;
best_row = Some(i);
}
}
best_row
}
fn needs_sigma(&self) -> bool {
true
}
fn after_refactor(&mut self, _m: usize) {
self.refactor_calls.set(self.refactor_calls.get() + 1);
}
fn set_initial_gamma(&mut self, _gamma_truth: &[f64]) {
self.init_calls.set(self.init_calls.get() + 1);
}
}
let rows = [0, 2, 0, 1, 1, 2, 0, 1, 2];
let cols = [0, 0, 1, 1, 2, 2, 3, 4, 5];
let vals = [-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0];
let a = CscMatrix::from_triplets(&rows, &cols, &vals, 3, 6).unwrap();
let c = vec![4.0, 5.0, 6.0, 0.0, 0.0, 0.0];
let mut basis = vec![3usize, 4, 5];
let mut x_b = vec![-1.0_f64, -2.0, -1.0];
let opts = SolverOptions {
max_etas: 1,
..SolverOptions::default()
};
let init_calls = Cell::new(0);
let refactor_calls = Cell::new(0);
let mut leaving = CountingDseLeaving {
init_calls: &init_calls,
refactor_calls: &refactor_calls,
};
let mut iters = 0usize;
let _ = dual_simplex_core_advanced(
&a,
&mut x_b,
&c,
&mut basis,
3,
6,
6,
false,
&opts,
&mut leaving,
&mut iters,
);
assert!(
refactor_calls.get() >= 1,
"test data must trigger at least one refactor; got {} (raise max_etas trigger?)",
refactor_calls.get(),
);
assert_eq!(
init_calls.get(),
1,
"set_initial_gamma must fire exactly once (cold-start only); got {} \
across {} refactor(s) — recompute_gamma_truth re-introduced after refactor?",
init_calls.get(),
refactor_calls.get(),
);
}
#[test]
fn core_rc_timed_deadline_checks_are_chunked_not_per_column() {
use crate::basis::LuBasis;
let n_synthetic = CORE_RC_DEADLINE_CHECK_INTERVAL * 4 + 100;
let m = 3usize;
let mut rows_t: Vec<usize> = Vec::new();
let mut cols_t: Vec<usize> = Vec::new();
let mut vals_t: Vec<f64> = Vec::new();
for j in 0..n_synthetic {
rows_t.push(j % m);
cols_t.push(j);
vals_t.push(1.0);
}
let a = CscMatrix::from_triplets(&rows_t, &cols_t, &vals_t, m, n_synthetic).unwrap();
let basis: Vec<usize> = (0..m).collect();
let c: Vec<f64> = (0..n_synthetic).map(|j| j as f64).collect();
let is_basic: Vec<bool> = (0..n_synthetic).map(|j| j < m).collect();
let opts = SolverOptions {
max_etas: 50,
..SolverOptions::default()
};
let mut basis_mgr = LuBasis::new_timed(&a, &basis, opts.max_etas, None).unwrap();
let before = CORE_RC_DEADLINE_CHECK_COUNT.with(|c| c.get());
let deadline_far = Some(std::time::Instant::now() + std::time::Duration::from_secs(60));
let rc_opt = compute_reduced_costs_timed(
&a, &c, &mut basis_mgr, &is_basic, n_synthetic, m, &basis, deadline_far,
);
assert!(rc_opt.is_some(), "RC compute must succeed (deadline is far)");
let rc = rc_opt.unwrap();
let after = CORE_RC_DEADLINE_CHECK_COUNT.with(|c| c.get());
let checks = after - before;
let max_expected = n_synthetic.div_ceil(CORE_RC_DEADLINE_CHECK_INTERVAL);
assert!(
checks <= max_expected,
"core compute_reduced_costs_timed issued {checks} deadline checks for \
n={n_synthetic}, expected ≤ {max_expected}. Per-column regression."
);
assert!(
checks < n_synthetic,
"core RC must issue far fewer deadline checks than columns: \
{checks} for {n_synthetic} columns."
);
for j in m..n_synthetic {
let expected = c[j] - c[j % m];
assert!(
(rc[j] - expected).abs() < 1e-10,
"rc[{j}] = {} expected {expected} (correctness check)",
rc[j]
);
}
}
}