use std::cell::Cell;
pub(crate) const BFRT_TIE_TOL: f64 = 1e-8;
thread_local! {
static BFRT_FLIP_INVOCATIONS: Cell<u64> = const { Cell::new(0) };
}
pub fn reset_bfrt_flip_invocations() {
BFRT_FLIP_INVOCATIONS.with(|c| c.set(0));
}
pub fn bfrt_flip_invocations() -> u64 {
BFRT_FLIP_INVOCATIONS.with(|c| c.get())
}
pub(super) fn bump_bfrt_flip_invocations() {
BFRT_FLIP_INVOCATIONS.with(|c| c.set(c.get().saturating_add(1)));
}
#[derive(Debug, Clone, Copy)]
pub struct ColBound {
pub upper: f64,
pub at_upper: bool,
}
#[derive(Debug, Clone)]
pub struct BfrtResult {
pub entering_col: usize,
pub theta: f64,
pub flips: Vec<usize>,
}
pub fn bfrt_select_entering(
trow: &[f64],
reduced_costs: &[f64],
is_basic: &[bool],
bounds: &[ColBound],
n_price: usize,
pivot_tol: f64,
leaving_residual: f64,
) -> Option<BfrtResult> {
debug_assert!(trow.len() >= n_price);
debug_assert!(reduced_costs.len() >= n_price);
debug_assert!(is_basic.len() >= n_price);
debug_assert!(bounds.len() >= n_price);
let mut breaks: Vec<(f64, usize, f64, f64)> = Vec::new();
for j in 0..n_price {
if is_basic[j] {
continue;
}
let a = trow[j];
let r = reduced_costs[j];
let b = &bounds[j];
let (theta, abs_pivot) = if !b.at_upper && a > pivot_tol {
(r / a, a.abs())
} else if b.at_upper && a < -pivot_tol {
((-r) / (-a), a.abs())
} else {
continue;
};
if theta < -pivot_tol {
continue;
}
let theta = theta.max(0.0);
let weight = if b.upper.is_finite() {
b.upper * abs_pivot
} else {
f64::INFINITY
};
breaks.push((theta, j, abs_pivot, weight));
}
if breaks.is_empty() {
return None;
}
breaks.sort_by(|x, y| x.0.partial_cmp(&y.0).unwrap_or(std::cmp::Ordering::Equal));
let residual_target = leaving_residual.abs();
let mut residual = residual_target;
let mut entering_idx: usize = 0;
let mut found = false;
for (k, &(_theta, _j, _abs_pivot, weight)) in breaks.iter().enumerate() {
if residual <= weight {
entering_idx = k;
found = true;
break;
}
residual -= weight;
}
if !found {
entering_idx = breaks.len() - 1;
}
let chosen_theta = breaks[entering_idx].0;
let mut best_idx = entering_idx;
let mut best_pivot = breaks[entering_idx].2;
for (k, &(theta, _j, abs_pivot, _w)) in breaks.iter().enumerate().skip(entering_idx + 1) {
if (theta - chosen_theta).abs() > BFRT_TIE_TOL {
break;
}
if abs_pivot > best_pivot {
best_pivot = abs_pivot;
best_idx = k;
}
}
let flips: Vec<usize> = (0..entering_idx).map(|k| breaks[k].1).collect();
if !flips.is_empty() {
bump_bfrt_flip_invocations();
}
Some(BfrtResult {
entering_col: breaks[best_idx].1,
theta: breaks[best_idx].0,
flips,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tolerances::PIVOT_TOL;
fn lb_bounds(uppers: &[f64]) -> Vec<ColBound> {
uppers
.iter()
.map(|&u| ColBound {
upper: u,
at_upper: false,
})
.collect()
}
fn no_basic(n: usize) -> Vec<bool> {
vec![false; n]
}
#[test]
fn bfrt_no_finite_upper_matches_harris() {
let trow = vec![1.0, 2.0, 3.0];
let r = vec![0.3, 0.4, 0.9];
let bounds = lb_bounds(&[f64::INFINITY; 3]);
let result =
bfrt_select_entering(&trow, &r, &no_basic(3), &bounds, 3, PIVOT_TOL, 100.0).unwrap();
assert_eq!(result.entering_col, 1);
assert!((result.theta - 0.2).abs() < 1e-9);
assert!(result.flips.is_empty(), "no finite uppers → no flips");
}
#[test]
fn bfrt_flips_past_small_breakpoint() {
let trow = vec![1.0, 2.0, 0.5];
let r = vec![0.1, 1.0, 0.5];
let bounds = vec![
ColBound {
upper: 1.0,
at_upper: false,
},
ColBound {
upper: 1.0,
at_upper: false,
},
ColBound {
upper: f64::INFINITY,
at_upper: false,
},
];
let res =
bfrt_select_entering(&trow, &r, &no_basic(3), &bounds, 3, PIVOT_TOL, 1.5).unwrap();
assert_eq!(res.entering_col, 1, "BFRT should skip j=0 and pick j=1");
assert!((res.theta - 0.5).abs() < 1e-9);
assert_eq!(res.flips, vec![0], "j=0 must be marked as a flip");
}
#[test]
fn bfrt_flips_three_then_enters_at_infinite() {
let trow = vec![1.0, 1.0, 1.0, 1.0];
let r = vec![0.1, 0.2, 0.3, 0.4];
let bounds = vec![
ColBound {
upper: 1.0,
at_upper: false,
},
ColBound {
upper: 2.0,
at_upper: false,
},
ColBound {
upper: 3.0,
at_upper: false,
},
ColBound {
upper: f64::INFINITY,
at_upper: false,
},
];
let res =
bfrt_select_entering(&trow, &r, &no_basic(4), &bounds, 4, PIVOT_TOL, 10.0).unwrap();
assert_eq!(res.entering_col, 3);
assert_eq!(res.flips, vec![0, 1, 2]);
assert!((res.theta - 0.4).abs() < 1e-9);
}
#[test]
fn bfrt_handles_at_upper_columns() {
let trow = vec![-1.0, 2.0];
let r = vec![-0.2, 0.6];
let bounds = vec![
ColBound {
upper: 1.0,
at_upper: true,
},
ColBound {
upper: f64::INFINITY,
at_upper: false,
},
];
let res =
bfrt_select_entering(&trow, &r, &no_basic(2), &bounds, 2, PIVOT_TOL, 0.5).unwrap();
assert_eq!(res.entering_col, 0);
assert!((res.theta - 0.2).abs() < 1e-9);
assert!(res.flips.is_empty());
}
#[test]
fn bfrt_returns_none_when_no_compatible_column() {
let trow = vec![-1.0, -2.0];
let r = vec![0.1, 0.2];
let bounds = lb_bounds(&[1.0, 1.0]);
let res = bfrt_select_entering(&trow, &r, &no_basic(2), &bounds, 2, PIVOT_TOL, 1.0);
assert!(res.is_none());
}
#[test]
fn bfrt_tie_prefers_larger_pivot() {
let trow = vec![1.0, 5.0];
let r = vec![0.1, 0.5];
let bounds = lb_bounds(&[f64::INFINITY, f64::INFINITY]);
let res =
bfrt_select_entering(&trow, &r, &no_basic(2), &bounds, 2, PIVOT_TOL, 10.0).unwrap();
assert_eq!(res.entering_col, 1, "larger |pivot| should win the tie");
assert!(
res.flips.iter().all(|&f| bounds[f].upper.is_finite()),
"flips must not contain infinite-upper columns: {:?}",
res.flips,
);
}
#[test]
fn bfrt_tie_loser_at_selected_breakpoint_is_not_flip() {
let trow = vec![1.0, 5.0];
let r = vec![0.1, 0.5];
let bounds = lb_bounds(&[1.0, 1.0]);
let res =
bfrt_select_entering(&trow, &r, &no_basic(2), &bounds, 2, PIVOT_TOL, 0.5).unwrap();
assert_eq!(res.entering_col, 1);
assert!(
res.flips.is_empty(),
"same-theta losers are not crossed and must not flip"
);
}
#[test]
fn bfrt_skips_negative_breakpoints() {
let trow = vec![1.0, 2.0];
let r = vec![-1.0, 1.0];
let bounds = lb_bounds(&[f64::INFINITY, f64::INFINITY]);
let res =
bfrt_select_entering(&trow, &r, &no_basic(2), &bounds, 2, PIVOT_TOL, 1.0).unwrap();
assert_eq!(res.entering_col, 1);
assert!((res.theta - 0.5).abs() < 1e-12);
}
#[test]
fn bfrt_tie_excludes_infinite_upper() {
let trow = vec![1.0, 5.0];
let r = vec![0.1, 0.5];
let bounds = lb_bounds(&[f64::INFINITY, f64::INFINITY]);
let res =
bfrt_select_entering(&trow, &r, &no_basic(2), &bounds, 2, PIVOT_TOL, 10.0).unwrap();
assert!(
res.flips.iter().all(|&f| bounds[f].upper.is_finite()),
"no infinite-upper flips even on tie, got: {:?}",
res.flips,
);
}
#[test]
fn bfrt_tie_filters_only_infinite_upper() {
let trow = vec![1.0, 5.0];
let r = vec![0.1, 0.5];
let bounds = vec![
ColBound {
upper: 1.0,
at_upper: false,
},
ColBound {
upper: f64::INFINITY,
at_upper: false,
},
];
let res =
bfrt_select_entering(&trow, &r, &no_basic(2), &bounds, 2, PIVOT_TOL, 10.0).unwrap();
assert_eq!(res.entering_col, 1);
assert_eq!(res.flips, vec![0], "finite-upper walk-flip must survive");
assert!(
res.flips.iter().all(|&f| bounds[f].upper.is_finite()),
"no infinite-upper in flips: {:?}",
res.flips,
);
}
#[test]
fn bfrt_skips_basic_columns() {
let trow = vec![5.0, 1.0];
let r = vec![0.1, 0.5];
let bounds = lb_bounds(&[f64::INFINITY, f64::INFINITY]);
let is_basic = vec![true, false];
let res = bfrt_select_entering(&trow, &r, &is_basic, &bounds, 2, PIVOT_TOL, 10.0).unwrap();
assert_eq!(res.entering_col, 1);
}
#[test]
fn bfrt_flip_counter_increments_only_when_flipping() {
reset_bfrt_flip_invocations();
let bounds = lb_bounds(&[f64::INFINITY; 2]);
let _ = bfrt_select_entering(
&[1.0, 2.0],
&[0.3, 0.4],
&no_basic(2),
&bounds,
2,
PIVOT_TOL,
10.0,
);
assert_eq!(bfrt_flip_invocations(), 0);
let bounds = vec![
ColBound {
upper: 1.0,
at_upper: false,
},
ColBound {
upper: f64::INFINITY,
at_upper: false,
},
];
let _ = bfrt_select_entering(
&[1.0, 1.0],
&[0.1, 0.5],
&no_basic(2),
&bounds,
2,
PIVOT_TOL,
5.0,
);
assert_eq!(bfrt_flip_invocations(), 1);
let _ = bfrt_select_entering(
&[1.0, 1.0],
&[0.1, 0.5],
&no_basic(2),
&bounds,
2,
PIVOT_TOL,
5.0,
);
assert_eq!(bfrt_flip_invocations(), 2);
}
#[test]
fn bfrt_beats_harris_on_bounded_chain() {
let mut trow = Vec::new();
let mut r = Vec::new();
let mut bounds = Vec::new();
for k in 1..=10 {
trow.push(1.0);
r.push(0.01 * k as f64);
bounds.push(ColBound {
upper: 1.0,
at_upper: false,
});
}
trow.push(1.0);
r.push(1.0);
bounds.push(ColBound {
upper: f64::INFINITY,
at_upper: false,
});
let res =
bfrt_select_entering(&trow, &r, &no_basic(11), &bounds, 11, PIVOT_TOL, 5.0).unwrap();
assert_eq!(res.entering_col, 4);
assert!((res.theta - 0.05).abs() < 1e-9);
assert_eq!(res.flips.len(), 4);
assert!(res.theta > 0.01 * 4.0, "BFRT must beat Harris by ≥ 4x here");
}
}