pub(crate) trait RatioTestStrategy {
fn select_entering(
&self,
trow: &[f64],
reduced_costs: &[f64],
is_basic: &[bool],
n_price: usize,
) -> Option<(usize, f64)>;
}
pub(crate) struct HarrisRatioTest {
pub harris_tol: f64,
pub pivot_tol: f64,
}
impl HarrisRatioTest {
pub fn new(harris_tol: f64, pivot_tol: f64) -> Self {
Self {
harris_tol,
pivot_tol,
}
}
}
impl RatioTestStrategy for HarrisRatioTest {
fn select_entering(
&self,
trow: &[f64],
reduced_costs: &[f64],
is_basic: &[bool],
n_price: usize,
) -> Option<(usize, f64)> {
let alpha_h = self.harris_tol;
let pivot_tol = self.pivot_tol;
let mut theta_max = f64::INFINITY;
for j in 0..n_price {
if is_basic[j] {
continue;
}
if trow[j] > pivot_tol {
let relaxed_ratio = (reduced_costs[j] + alpha_h) / trow[j];
if relaxed_ratio < theta_max {
theta_max = relaxed_ratio;
}
}
}
if theta_max == f64::INFINITY {
return None;
}
let mut best_j: Option<usize> = None;
let mut best_pivot = 0.0_f64;
let mut best_theta = f64::INFINITY;
for j in 0..n_price {
if is_basic[j] {
continue;
}
if trow[j] > pivot_tol {
let ratio = reduced_costs[j] / trow[j];
if ratio <= theta_max {
let pivot_abs = trow[j].abs();
if pivot_abs > best_pivot {
best_pivot = pivot_abs;
best_j = Some(j);
best_theta = ratio;
}
}
}
}
if let Some(j) = best_j {
return Some((j, best_theta));
}
let mut fallback_j: Option<usize> = None;
let mut fallback_theta = f64::INFINITY;
for j in 0..n_price {
if is_basic[j] {
continue;
}
if trow[j] > pivot_tol {
let ratio = reduced_costs[j] / trow[j];
if fallback_j.is_none()
|| ratio < fallback_theta
|| (ratio == fallback_theta && j < fallback_j.unwrap())
{
fallback_j = Some(j);
fallback_theta = ratio;
}
}
}
fallback_j.map(|j| (j, fallback_theta))
}
}
pub(crate) fn bland_ratio_test(
trow: &[f64],
reduced_costs: &[f64],
is_basic: &[bool],
n_price: usize,
pivot_tol: f64,
) -> Option<(usize, f64)> {
let mut best_ratio = f64::INFINITY;
let mut best_j: Option<usize> = None;
for j in 0..n_price {
if is_basic[j] {
continue;
}
if trow[j] > pivot_tol {
let ratio = reduced_costs[j] / trow[j];
if ratio >= 0.0 && ratio < best_ratio {
best_ratio = ratio;
best_j = Some(j);
}
}
}
best_j.map(|j| (j, best_ratio))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tolerances::PIVOT_TOL;
const HARRIS_TOL: f64 = 1e-7;
fn no_basic(n: usize) -> Vec<bool> {
vec![false; n]
}
#[test]
fn harris_candidate_set_construction() {
let trow = vec![1.0, 2.0, 3.0];
let r = vec![0.3, 0.4, 0.9];
let is_basic = no_basic(3);
let harris = HarrisRatioTest::new(HARRIS_TOL, PIVOT_TOL);
let result = harris.select_entering(&trow, &r, &is_basic, 3);
assert!(result.is_some());
let (col, theta) = result.unwrap();
assert_eq!(col, 1, "j=1 should be selected (only candidate)");
assert!(
(theta - 0.2).abs() < 1e-6,
"theta should be ~0.2, got {}",
theta
);
}
#[test]
fn harris_pass2_max_pivot_selected() {
let trow = vec![1.0, 5.0, 2.0];
let r = vec![0.1, 0.5, 0.2];
let is_basic = no_basic(3);
let harris = HarrisRatioTest::new(1e-3, PIVOT_TOL); let result = harris.select_entering(&trow, &r, &is_basic, 3);
assert!(result.is_some());
let (col, _theta) = result.unwrap();
assert_eq!(
col, 1,
"j=1 has largest |trow[j]|=5.0, should be selected for stability"
);
}
#[test]
fn harris_pass2_largest_pivot_among_candidates() {
let trow = vec![3.0, 1.0, 7.0, 2.0];
let r = vec![0.3, 0.1, 0.7, 0.2];
let is_basic = no_basic(4);
let harris = HarrisRatioTest::new(1e-2, PIVOT_TOL);
let result = harris.select_entering(&trow, &r, &is_basic, 4);
assert!(result.is_some());
let (col, _) = result.unwrap();
assert_eq!(col, 2, "j=2 has largest |trow[j]|=7.0, should be selected");
}
#[test]
fn harris_bland_fallback_when_no_candidate() {
let trow = vec![1.0, 2.0];
let r = vec![0.1, 0.1];
let is_basic = no_basic(2);
let harris = HarrisRatioTest::new(0.0, PIVOT_TOL);
let result = harris.select_entering(&trow, &r, &is_basic, 2);
assert!(
result.is_some(),
"Should return Some even with harris_tol=0"
);
let (col, _) = result.unwrap();
assert_eq!(col, 1, "j=1 (ratio=0.05) should be the candidate");
}
#[test]
fn harris_no_eligible_column_returns_none() {
let trow = vec![-1.0, -2.0];
let r = vec![0.1, 0.2];
let is_basic = no_basic(2);
let harris = HarrisRatioTest::new(HARRIS_TOL, PIVOT_TOL);
let result = harris.select_entering(&trow, &r, &is_basic, 2);
assert!(result.is_none(), "No eligible columns should return None");
}
#[test]
fn harris_skips_basic_variables() {
let trow = vec![5.0, 1.0];
let r = vec![0.1, 0.2];
let is_basic = vec![true, false];
let harris = HarrisRatioTest::new(HARRIS_TOL, PIVOT_TOL);
let result = harris.select_entering(&trow, &r, &is_basic, 2);
assert!(result.is_some());
let (col, _) = result.unwrap();
assert_eq!(col, 1, "j=0 is basic, only j=1 should be selected");
}
#[test]
fn bland_selects_min_ratio() {
let trow = vec![1.0, 2.0, 3.0];
let r = vec![0.3, 0.2, 0.9];
let is_basic = no_basic(3);
let result = bland_ratio_test(&trow, &r, &is_basic, 3, PIVOT_TOL);
assert!(result.is_some());
let (col, theta) = result.unwrap();
assert_eq!(col, 1);
assert!((theta - 0.1).abs() < 1e-9);
}
#[test]
fn bland_smallest_index_on_tie() {
let trow = vec![1.0, 1.0, 1.0];
let r = vec![0.5, 0.5, 0.5];
let is_basic = no_basic(3);
let result = bland_ratio_test(&trow, &r, &is_basic, 3, PIVOT_TOL);
assert!(result.is_some());
let (col, _) = result.unwrap();
assert_eq!(col, 0);
}
#[test]
fn bland_no_eligible_returns_none() {
let trow = vec![-1.0, 0.0, -2.0];
let r = vec![0.1, 0.2, 0.3];
let is_basic = no_basic(3);
let result = bland_ratio_test(&trow, &r, &is_basic, 3, PIVOT_TOL);
assert!(result.is_none());
}
#[test]
fn bland_skips_basic_variables() {
let trow = vec![10.0, 1.0];
let r = vec![0.1, 0.5];
let is_basic = vec![true, false];
let result = bland_ratio_test(&trow, &r, &is_basic, 2, PIVOT_TOL);
assert!(result.is_some());
let (col, theta) = result.unwrap();
assert_eq!(col, 1);
assert!((theta - 0.5).abs() < 1e-9);
}
}