use super::super::pricing::DualLeavingStrategy;
const DSE_GAMMA_FLOOR: f64 = 1e-10;
const DSE_GAMMA_CEILING: f64 = 1e10;
pub(crate) struct DseWeights {
gamma: Vec<f64>,
needs_reset: bool,
}
impl DseWeights {
pub(crate) fn new(m: usize) -> Self {
Self {
gamma: vec![1.0; m],
needs_reset: false,
}
}
pub(crate) fn gamma(&self, i: usize) -> f64 {
self.gamma[i].max(DSE_GAMMA_FLOOR)
}
pub(crate) fn reset_to_identity(&mut self) {
self.gamma.fill(1.0);
self.needs_reset = false;
}
pub(crate) fn update_after_pivot(
&mut self,
leaving_row: usize,
alpha: &[f64],
sigma: &[f64],
pivot: f64,
) {
let inv_p = 1.0 / pivot;
let gamma_p = self.gamma[leaving_row];
for i in 0..self.gamma.len() {
if i == leaving_row {
continue;
}
let ratio = alpha[i] * inv_p;
let new_gamma = self.gamma[i] - 2.0 * ratio * sigma[i] + ratio * ratio * gamma_p;
self.gamma[i] = clamp_gamma(new_gamma, &mut self.needs_reset);
}
let new_gamma_p = gamma_p * inv_p * inv_p;
self.gamma[leaving_row] = clamp_gamma(new_gamma_p, &mut self.needs_reset);
}
}
fn clamp_gamma(v: f64, needs_reset: &mut bool) -> f64 {
if !v.is_finite() || v > DSE_GAMMA_CEILING {
*needs_reset = true;
DSE_GAMMA_CEILING.min(1.0_f64.max(v.abs()))
} else if v < DSE_GAMMA_FLOOR {
if v < -DSE_GAMMA_FLOOR {
*needs_reset = true;
}
DSE_GAMMA_FLOOR
} else {
v
}
}
const DSE_DISABLE_ENV: &str = "DSE_DISABLE_GAMMA_UPDATE";
fn gamma_update_disabled() -> bool {
std::env::var(DSE_DISABLE_ENV)
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false)
}
pub(crate) struct DualSteepestEdgeLeaving {
weights: DseWeights,
}
impl DualSteepestEdgeLeaving {
pub(crate) fn new(m: usize) -> Self {
Self {
weights: DseWeights::new(m),
}
}
}
impl DualLeavingStrategy for DualSteepestEdgeLeaving {
fn select_leaving(&mut self, x_b: &[f64], primal_tol: f64, _basis: &[usize]) -> Option<usize> {
let mut best_row: Option<usize> = None;
let mut best_score = 0.0_f64;
for (i, &val) in x_b.iter().enumerate() {
if val < -primal_tol {
let score = val * val / self.weights.gamma(i);
if score > best_score {
best_score = score;
best_row = Some(i);
}
}
}
best_row
}
fn needs_sigma(&self) -> bool {
true
}
fn after_pivot(&mut self, leaving_row: usize, alpha: &[f64], sigma: &[f64], pivot: f64) {
if gamma_update_disabled() {
return;
}
self.weights.update_after_pivot(leaving_row, alpha, sigma, pivot);
}
fn after_refactor(&mut self, m: usize) {
if self.weights.gamma.len() != m {
self.weights = DseWeights::new(m);
} else if self.weights.needs_reset {
self.weights.reset_to_identity();
}
}
fn set_initial_gamma(&mut self, gamma_truth: &[f64]) {
if gamma_update_disabled() {
if self.weights.gamma.len() != gamma_truth.len() {
self.weights = DseWeights::new(gamma_truth.len());
}
self.weights.reset_to_identity();
return;
}
if self.weights.gamma.len() != gamma_truth.len() {
self.weights = DseWeights::new(gamma_truth.len());
}
for (slot, &v) in self.weights.gamma.iter_mut().zip(gamma_truth.iter()) {
*slot = v.max(DSE_GAMMA_FLOOR);
}
self.weights.needs_reset = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn weights_initialised_to_identity() {
let w = DseWeights::new(4);
for i in 0..4 {
assert_eq!(w.gamma(i), 1.0);
}
}
#[test]
fn update_pivot_row_inverse_square() {
let mut w = DseWeights::new(3);
w.update_after_pivot(1, &[0.0, 2.0, 0.0], &[0.0, 1.0, 0.0], 2.0);
assert!((w.gamma(1) - 0.25).abs() < 1e-12);
}
#[test]
fn update_other_rows_apply_rank1_correction() {
let mut w = DseWeights::new(2);
w.update_after_pivot(1, &[3.0, 4.0], &[5.0, 7.0], 4.0);
assert!(w.gamma(0) >= DSE_GAMMA_FLOOR, "γ floored");
assert!((w.gamma(1) - (1.0_f64 / 16.0)).abs() < 1e-12, "pivot row 1/α²");
}
#[test]
fn dse_strategy_selects_max_score() {
let mut s = DualSteepestEdgeLeaving::new(3);
let pick = s.select_leaving(&[-2.0, -3.0, -1.0], 1e-9, &[0, 1, 2]);
assert_eq!(pick, Some(1));
}
#[test]
fn dse_strategy_respects_gamma_weighting() {
let mut s = DualSteepestEdgeLeaving::new(2);
s.weights.gamma[1] = 100.0;
let pick = s.select_leaving(&[-2.0, -3.0], 1e-9, &[0, 1]);
assert_eq!(pick, Some(0));
}
#[test]
fn dse_returns_none_when_primal_feasible() {
let mut s = DualSteepestEdgeLeaving::new(3);
let pick = s.select_leaving(&[0.5, 1.0, 0.0], 1e-9, &[0, 1, 2]);
assert_eq!(pick, None);
}
#[test]
fn ceiling_clamp_marks_for_reset() {
let mut w = DseWeights::new(2);
w.update_after_pivot(0, &[1e-8, 0.0], &[0.0, 0.0], 1e-8);
assert!(w.needs_reset, "ceiling clamp must flag reset");
}
#[test]
fn floor_clamp_negative_drift_marks_for_reset() {
let mut w = DseWeights::new(2);
w.update_after_pivot(1, &[3.0, 4.0], &[5.0, 7.0], 4.0);
assert_eq!(w.gamma(0), DSE_GAMMA_FLOOR, "γ floored to FLOOR");
assert!(w.needs_reset, "significantly-negative drift must flag reset");
}
#[test]
fn floor_clamp_tiny_positive_does_not_mark_reset() {
let mut needs_reset = false;
let clamped = clamp_gamma(DSE_GAMMA_FLOOR / 10.0, &mut needs_reset);
assert_eq!(clamped, DSE_GAMMA_FLOOR);
assert!(!needs_reset, "tiny-positive sub-FLOOR is noise, not drift");
}
}