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
}
fn relax_best(
leaving: &mut Option<usize>,
best_pivot_abs: &mut f64,
i: usize,
d_abs: f64,
basis: &[usize],
) {
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),
_ => {}
}
}
}
pub(super) fn max_pivot_within(
x_b: &[f64],
d: &[f64],
basis: &[usize],
m: usize,
floor: f64,
theta: f64,
art_threshold: Option<usize>,
) -> Option<usize> {
let mut leaving_any: Option<usize> = None;
let mut best_any = 0.0f64;
let mut leaving_art: Option<usize> = None;
let mut best_art = 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();
relax_best(&mut leaving_any, &mut best_any, i, d_abs, basis);
if art_threshold.is_some_and(|t| basis[i] >= t) {
relax_best(&mut leaving_art, &mut best_art, i, d_abs, basis);
}
}
}
}
if leaving_art.is_some() {
leaving_art
} else {
leaving_any
}
}
pub(super) fn select_leaving_feasibility_preserving(
x_b: &[f64],
d: &[f64],
basis: &[usize],
m: usize,
floor: f64,
feas_tol: f64,
art_threshold: Option<usize>,
) -> 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, art_threshold)
}
#[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, None)
.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,
None,
)
.expect("eligible leaving row exists");
assert_eq!(leaving, 1, "must pick the larger pivot |d|=2.0 (row 1)");
}
#[test]
fn phase1_prefers_artificial_within_tie_band() {
let x_b = [0.0, 0.0];
let d = [2.0, 0.5];
let basis = [3usize, 10usize];
let threshold = 10usize;
let off = select_leaving_feasibility_preserving(
&x_b,
&d,
&basis,
x_b.len(),
PIVOT_TOL,
PIVOT_TOL,
None,
)
.expect("eligible leaving row exists");
assert_eq!(off, 0, "OFF: largest pivot wins (structural row 0)");
let on = select_leaving_feasibility_preserving(
&x_b,
&d,
&basis,
x_b.len(),
PIVOT_TOL,
PIVOT_TOL,
Some(threshold),
)
.expect("eligible leaving row exists");
assert_eq!(on, 1, "ON: artificial row 1 leaves first");
assert!(min_basic_after_pivot(&x_b, &d, off) >= -PIVOT_TOL);
assert!(min_basic_after_pivot(&x_b, &d, on) >= -PIVOT_TOL);
}
#[test]
fn phase1_artificial_outside_tie_band_is_noop() {
let x_b = [0.0, 1.0];
let d = [1.0, 1.0];
let basis = [3usize, 10usize];
let threshold = 10usize;
let off = select_leaving_feasibility_preserving(
&x_b,
&d,
&basis,
x_b.len(),
PIVOT_TOL,
PIVOT_TOL,
None,
);
let on = select_leaving_feasibility_preserving(
&x_b,
&d,
&basis,
x_b.len(),
PIVOT_TOL,
PIVOT_TOL,
Some(threshold),
);
assert_eq!(on, Some(0), "out-of-band artificial must NOT be selected");
assert_eq!(on, off, "preference is a no-op outside the tie-band");
}
#[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,
None,
);
assert!(leaving.is_none(), "no positive direction ⇒ unbounded ⇒ None");
}
}