use crate::basis::LuBasis;
use crate::tolerances::PIVOT_TOL;
const EPS: f64 = 1e-8;
const GAMMA_FLOOR: f64 = 1e-10;
const CAP_MULT_OF_M: f64 = 100.0;
pub(crate) trait PricingStrategy {
fn select_entering(&self, reduced_costs: &[f64], n_basic: usize) -> Option<usize>;
fn update_weights(
&mut self,
basis: &LuBasis,
entering: usize,
leaving: usize,
leaving_row: usize,
eta: &[f64],
);
fn reset_weights(&mut self, n_vars: usize);
}
#[cfg(test)]
pub(crate) struct DantzigPricing;
#[cfg(test)]
impl PricingStrategy for DantzigPricing {
fn select_entering(&self, reduced_costs: &[f64], n_basic: usize) -> Option<usize> {
let limit = n_basic.min(reduced_costs.len());
let mut min_rc = -EPS;
let mut entering = None;
for (j, &rc) in reduced_costs.iter().enumerate().take(limit) {
if rc < min_rc {
min_rc = rc;
entering = Some(j);
}
}
entering
}
fn update_weights(&mut self, _: &LuBasis, _: usize, _: usize, _: usize, _: &[f64]) {}
fn reset_weights(&mut self, _n_vars: usize) {}
}
pub(crate) struct SteepestEdgePricing {
weights: Vec<f64>,
}
impl SteepestEdgePricing {
pub fn new(n_vars: usize) -> Self {
Self {
weights: vec![1.0; n_vars],
}
}
}
impl PricingStrategy for SteepestEdgePricing {
fn select_entering(&self, reduced_costs: &[f64], n_basic: usize) -> Option<usize> {
let limit = n_basic.min(reduced_costs.len());
let mut best_score = -EPS;
let mut entering = None;
for (j, &rc) in reduced_costs.iter().enumerate().take(limit) {
if rc < -EPS {
let gamma = self.weights.get(j).copied().unwrap_or(1.0).max(GAMMA_FLOOR);
let score = -rc / gamma.sqrt();
if score > best_score {
best_score = score;
entering = Some(j);
}
}
}
entering
}
fn update_weights(
&mut self,
_basis: &LuBasis,
entering: usize,
leaving: usize,
leaving_row: usize,
eta: &[f64],
) {
if leaving < self.weights.len() {
let pivot = eta.get(leaving_row).copied().unwrap_or(0.0);
if pivot.abs() > PIVOT_TOL {
let eta_norm_sq: f64 = eta.iter().map(|&x| x * x).sum();
let cap = CAP_MULT_OF_M * (eta.len() as f64);
let raw = eta_norm_sq / (pivot * pivot);
let new_weight = raw.min(cap).max(GAMMA_FLOOR);
self.weights[leaving] = self.weights[leaving].max(new_weight);
}
}
if entering < self.weights.len() {
let eta_norm_sq: f64 = eta.iter().map(|&x| x * x).sum();
let gamma_leaving = if leaving < self.weights.len() {
self.weights[leaving]
} else {
1.0
};
let new_entering_w = if eta_norm_sq > GAMMA_FLOOR {
(gamma_leaving / eta_norm_sq).max(1.0)
} else {
1.0
};
self.weights[entering] = new_entering_w;
}
}
fn reset_weights(&mut self, n_vars: usize) {
if self.weights.len() != n_vars {
self.weights = vec![1.0; n_vars];
} else {
self.weights.fill(1.0);
}
}
}
pub(crate) trait DualLeavingStrategy {
fn select_leaving(&mut self, x_b: &[f64], primal_tol: f64, basis: &[usize]) -> Option<usize>;
fn bland_leaving(&mut self, x_b: &[f64], primal_tol: f64, basis: &[usize]) -> Option<usize> {
let mut best_row: Option<usize> = None;
let mut best_var = usize::MAX;
for (i, &v) in x_b.iter().enumerate() {
if v < -primal_tol && basis[i] < best_var {
best_var = basis[i];
best_row = Some(i);
}
}
best_row
}
fn progress_metric(&mut self, x_b: &[f64], _basis: &[usize]) -> f64 {
x_b.iter().map(|&v| (-v).max(0.0)).sum()
}
fn needs_sigma(&self) -> bool {
false
}
fn after_pivot(&mut self, _leaving_row: usize, _alpha: &[f64], _sigma: &[f64], _pivot: f64) {}
fn after_refactor(&mut self, _m: usize) {}
fn set_initial_gamma(&mut self, _gamma_truth: &[f64]) {}
fn allows_lb_repair(&self) -> bool {
true
}
}
pub(crate) struct MostInfeasibleLeaving;
impl DualLeavingStrategy for MostInfeasibleLeaving {
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
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dantzig_selects_most_negative() {
let pricing = DantzigPricing;
let rc = vec![0.0, -0.5, -1.0, 0.2, -0.3];
let entering = pricing.select_entering(&rc, rc.len());
assert_eq!(entering, Some(2)); }
#[test]
fn test_dantzig_returns_none_when_optimal() {
let pricing = DantzigPricing;
let rc = vec![0.0, 0.1, 0.5, 0.0];
assert_eq!(pricing.select_entering(&rc, rc.len()), None);
}
#[test]
fn test_dantzig_respects_n_basic_limit() {
let pricing = DantzigPricing;
let rc = vec![-0.1, -0.5, -1.0]; let entering = pricing.select_entering(&rc, 2);
assert_eq!(entering, Some(1)); }
#[test]
fn test_steepest_edge_scoring() {
let pricing = SteepestEdgePricing {
weights: vec![1.0, 4.0, 1.0], };
let rc = vec![-1.0, -2.0, -0.5];
let entering = pricing.select_entering(&rc, rc.len());
assert!(entering == Some(0) || entering == Some(1));
}
#[test]
fn test_steepest_edge_prefers_better_score() {
let pricing2 = SteepestEdgePricing {
weights: vec![1.0, 100.0, 1.0],
};
let rc = vec![-1.0, -10.0, -0.5];
let entering = pricing2.select_entering(&rc, rc.len());
assert!(entering == Some(0) || entering == Some(1));
}
fn make_identity_basis_2x2() -> crate::basis::LuBasis {
let a =
crate::sparse::CscMatrix::from_triplets(&[0, 1], &[0, 1], &[1.0, 1.0], 2, 2).unwrap();
crate::basis::LuBasis::new(&a, &[0, 1], 50).unwrap()
}
#[test]
fn devex_leaving_weight_uses_pivot_squared() {
let mut pricing = SteepestEdgePricing::new(5);
let eta = vec![0.0, 3.0, 4.0, 0.0, 0.0]; let expected = 25.0_f64 / 9.0_f64;
let basis_id = make_identity_basis_2x2();
pricing.update_weights(&basis_id, 2, 3, 1, &eta);
assert!(
(pricing.weights[3] - expected).abs() < 1e-12,
"expected γ[leaving] = {:.6} (‖η‖²/pivot²), got {:.6}",
expected,
pricing.weights[3]
);
assert_eq!(
pricing.weights[2], 1.0,
"entering weight = 1.0 when γ[leaving] < ‖η‖² (no cycle-penalty case)"
);
}
#[test]
fn devex_small_pivot_guard_skips_update() {
let mut pricing = SteepestEdgePricing::new(5);
pricing.weights[0] = 3.0;
pricing.weights[1] = 5.0;
let eta = vec![0.0, 1e-9_f64, 4.0, 0.0, 0.0];
let basis_id = make_identity_basis_2x2();
pricing.update_weights(&basis_id, 2, 3, 1, &eta);
assert_eq!(pricing.weights[3], 1.0, "small pivot: weight must remain 1.0");
assert!(
(pricing.weights[0] - 3.0).abs() < 1e-15,
"γ[0] must stay at 3.0 (no global reset), got {:.6}",
pricing.weights[0]
);
assert!(
(pricing.weights[1] - 5.0).abs() < 1e-15,
"γ[1] must stay at 5.0 (no global reset), got {:.6}",
pricing.weights[1]
);
}
#[test]
fn devex_entering_weight_penalized_when_leaving_large() {
let mut pricing = SteepestEdgePricing::new(5);
let eta = vec![0.0, 2e-8_f64, 4.0, 0.0, 0.0]; let basis_id = make_identity_basis_2x2();
pricing.update_weights(&basis_id, 2, 3, 1, &eta);
assert!(
pricing.weights[2] > 1.1,
"entering weight should be > 1 when γ[leaving] >> ‖η‖², got {:.6}",
pricing.weights[2]
);
}
#[test]
fn devex_pivot_squared_update_is_capped() {
let mut pricing = SteepestEdgePricing::new(5);
let eta = vec![0.0, 2e-8_f64, 4.0, 0.0, 0.0];
let basis_id = make_identity_basis_2x2();
pricing.update_weights(&basis_id, 2, 3, 1, &eta);
let cap = CAP_MULT_OF_M * eta.len() as f64;
assert!(
(pricing.weights[3] - cap).abs() <= 1e-9,
"expected capped γ[leaving]={cap:.3e}, got {:.3e}",
pricing.weights[3]
);
}
#[test]
fn reset_weights_resets_all_to_one() {
let mut pricing = SteepestEdgePricing::new(4);
pricing.weights[0] = 5.0;
pricing.weights[2] = 7.3;
pricing.reset_weights(4);
assert!(
pricing.weights.iter().all(|&w| (w - 1.0).abs() < 1e-15),
"reset_weights must clear all weights to 1.0, got {:?}",
pricing.weights
);
}
#[test]
fn reset_weights_resizes_if_n_changes() {
let mut pricing = SteepestEdgePricing::new(3);
pricing.weights[1] = 9.9;
pricing.reset_weights(5); assert_eq!(pricing.weights.len(), 5);
assert!(pricing.weights.iter().all(|&w| (w - 1.0).abs() < 1e-15));
}
#[test]
fn bland_leaving_uses_smallest_column_index() {
let mut strat = MostInfeasibleLeaving;
let x_b = vec![-1.0_f64, -2.0, -0.5];
let basis = vec![5_usize, 1, 3];
let pick = strat.bland_leaving(&x_b, 1e-8, &basis);
assert_eq!(
pick,
Some(1),
"Bland leaving must select row with smallest basis[i]=1 (col 1), not row 0 (col 5)"
);
}
}