use crate::tolerances::PIVOT_TOL;
pub(super) fn bound_tolerance_step(x_b: &[f64], d: &[f64], m: usize, floor: f64, tol: f64) -> f64 {
let mut theta = f64::INFINITY;
for i in 0..m {
if d[i] > floor {
let t = (x_b[i] + tol) / d[i];
if t < theta {
theta = t;
}
}
}
theta
}
pub(super) fn max_pivot_within(
x_b: &[f64],
d: &[f64],
basis: &[usize],
m: usize,
floor: f64,
theta: f64,
) -> Option<usize> {
let mut leaving: Option<usize> = None;
let mut best_pivot_abs = 0.0f64;
for i in 0..m {
if d[i] > floor {
let ratio = x_b[i] / d[i];
if ratio <= theta {
let d_abs = d[i].abs();
if d_abs > best_pivot_abs + PIVOT_TOL {
best_pivot_abs = d_abs;
leaving = Some(i);
} else if (d_abs - best_pivot_abs).abs() <= PIVOT_TOL {
match leaving {
None => leaving = Some(i),
Some(prev) if basis[i] < basis[prev] => leaving = Some(i),
_ => {}
}
}
}
}
}
leaving
}
pub(super) fn select_leaving_feasibility_preserving(
x_b: &[f64],
d: &[f64],
basis: &[usize],
m: usize,
floor: f64,
feas_tol: f64,
) -> Option<usize> {
let theta = bound_tolerance_step(x_b, d, m, floor, feas_tol);
if !theta.is_finite() {
return None;
}
max_pivot_within(x_b, d, basis, m, floor, theta)
}
#[cfg(test)]
mod ratio_test_feasibility_tests {
use super::select_leaving_feasibility_preserving;
use crate::tolerances::PIVOT_TOL;
fn min_basic_after_pivot(x_b: &[f64], d: &[f64], leaving: usize) -> f64 {
let step = x_b[leaving] / d[leaving];
let mut min_v = f64::INFINITY;
for i in 0..x_b.len() {
let v = if i == leaving { step } else { x_b[i] - d[i] * step };
if v < min_v {
min_v = v;
}
}
min_v
}
fn old_absolute_window_leaving(x_b: &[f64], d: &[f64], basis: &[usize], floor: f64) -> usize {
let m = x_b.len();
let mut min_ratio = f64::INFINITY;
for i in 0..m {
if d[i] > floor {
min_ratio = min_ratio.min(x_b[i] / d[i]);
}
}
let window = min_ratio + PIVOT_TOL;
let mut leaving = None;
let mut best = 0.0f64;
for i in 0..m {
if d[i] > floor && x_b[i] / d[i] <= window {
let da = d[i].abs();
if da > best + PIVOT_TOL {
best = da;
leaving = Some(i);
} else if (da - best).abs() <= PIVOT_TOL {
match leaving {
None => leaving = Some(i),
Some(p) if basis[i] < basis[p] => leaving = Some(i),
_ => {}
}
}
}
}
leaving.unwrap()
}
#[test]
fn bound_tolerance_blocks_ill_scaled_overshoot() {
let x_b = [1e-9, 1e-3];
let d = [1e6, 1e6];
let basis = [5usize, 3usize];
let feas_tol = PIVOT_TOL;
let floor = PIVOT_TOL;
let leaving =
select_leaving_feasibility_preserving(&x_b, &d, &basis, x_b.len(), floor, feas_tol)
.expect("eligible leaving row exists");
assert_eq!(
leaving, 0,
"helper must pick the true-min-ratio row 0, not the far row 1"
);
let min_basic = min_basic_after_pivot(&x_b, &d, leaving);
assert!(
min_basic >= -feas_tol,
"helper pivot must keep basics ≥ −feas_tol; got {min_basic}"
);
let old_leaving = old_absolute_window_leaving(&x_b, &d, &basis, floor);
assert_eq!(old_leaving, 1, "old window picks the far row (Bland tie)");
let old_min_basic = min_basic_after_pivot(&x_b, &d, old_leaving);
assert!(
old_min_basic < -feas_tol,
"old window must breach feasibility (proves the sentinel bites); got {old_min_basic}"
);
assert!(
old_min_basic < -1e-4,
"breach magnitude ∝ d[i]; expected ≈ −1e-3, got {old_min_basic}"
);
}
#[test]
fn picks_largest_pivot_within_window() {
let x_b = [0.0, 0.0];
let d = [0.5, 2.0];
let basis = [7usize, 4usize];
let leaving =
select_leaving_feasibility_preserving(&x_b, &d, &basis, x_b.len(), PIVOT_TOL, PIVOT_TOL)
.expect("eligible leaving row exists");
assert_eq!(leaving, 1, "must pick the larger pivot |d|=2.0 (row 1)");
}
#[test]
fn no_eligible_row_is_unbounded() {
let x_b = [3.0, 4.0];
let d = [-1.0, 0.0];
let basis = [0usize, 1usize];
let leaving = select_leaving_feasibility_preserving(
&x_b,
&d,
&basis,
x_b.len(),
PIVOT_TOL,
PIVOT_TOL,
);
assert!(leaving.is_none(), "no positive direction ⇒ unbounded ⇒ None");
}
}