use crate::basis::{BasisManager, LuBasis};
use crate::options::SolverOptions;
use crate::problem::LpProblem;
use crate::sparse::{CscMatrix, SparseVec};
use crate::tolerances::PIVOT_TOL;
use std::sync::atomic::Ordering;
use super::super::dual_common::{basic_obj, compute_dual_vars_into, compute_reduced_costs_into};
use super::super::standard_form::{BoundedStandardForm, SimplexOutcome};
use super::bound_flip::{bfrt_select_entering, bump_bfrt_flip_invocations, ColBound};
#[derive(Debug)]
pub(crate) enum BoundedOutcome {
#[allow(dead_code)]
Optimal(f64, Vec<f64>),
Unbounded,
Timeout(f64),
SingularBasis,
#[allow(dead_code)]
UbViolationOutOfScope { row: usize },
}
#[cfg(test)]
thread_local! {
static FLIP_APPLY_DISABLE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
#[cfg(test)]
pub(crate) fn set_flip_apply_disabled(v: bool) {
FLIP_APPLY_DISABLE.with(|c| c.set(v));
}
#[cfg(test)]
fn flip_apply_disabled() -> bool {
FLIP_APPLY_DISABLE.with(|c| c.get())
}
#[cfg(not(test))]
#[inline(always)]
fn flip_apply_disabled() -> bool {
false
}
const SIMPLEX_ITER_HARD_CAP: usize = 1_000_000;
const BOUNDED_DUAL_ITER_HARD_CAP: usize = SIMPLEX_ITER_HARD_CAP;
pub(crate) struct BoundedDualState {
pub(crate) basis: Vec<usize>,
pub(crate) at_upper: Vec<bool>,
pub(crate) x_b: Vec<f64>,
pub(crate) reduced_costs: Vec<f64>,
pub(crate) is_basic: Vec<bool>,
pub(crate) iterations: usize,
}
impl BoundedDualState {
pub(crate) fn cold(bsf: &BoundedStandardForm, b_scaled: &[f64]) -> Self {
let m = bsf.m;
let n_total = bsf.n_total;
assert_eq!(bsf.initial_basis.len(), m);
let mut is_basic = vec![false; n_total];
for &j in bsf.initial_basis.iter() {
if j < n_total {
is_basic[j] = true;
}
}
Self {
basis: bsf.initial_basis.clone(),
at_upper: vec![false; n_total],
x_b: b_scaled.to_vec(),
reduced_costs: vec![0.0; n_total],
is_basic,
iterations: 0,
}
}
}
struct IterBuffers {
rho: Vec<f64>,
trow: Vec<f64>,
alpha: Vec<f64>,
alpha_flip: Vec<f64>,
col_bounds: Vec<ColBound>,
y: Vec<f64>,
}
impl IterBuffers {
fn new(m: usize, n_total: usize, upper_bounds: &[f64]) -> Self {
let col_bounds = (0..n_total)
.map(|j| ColBound {
upper: upper_bounds[j],
at_upper: false,
})
.collect();
Self {
rho: vec![0.0; m],
trow: vec![0.0; n_total],
alpha: vec![0.0; m],
alpha_flip: vec![0.0; m],
col_bounds,
y: vec![0.0; m],
}
}
}
pub(crate) fn solve_bounded_dual(
bsf: &BoundedStandardForm,
a: &CscMatrix,
b: &[f64],
c: &[f64],
options: &SolverOptions,
ubs: &[f64],
) -> (BoundedOutcome, BoundedDualState) {
let state = BoundedDualState::cold(bsf, b);
iterate(state, bsf, a, c, options, ubs)
}
pub(crate) fn iterate(
mut state: BoundedDualState,
bsf: &BoundedStandardForm,
a: &CscMatrix,
c: &[f64],
options: &SolverOptions,
ubs: &[f64],
) -> (BoundedOutcome, BoundedDualState) {
let m = bsf.m;
let n_total = bsf.n_total;
debug_assert_eq!(state.basis.len(), m);
debug_assert_eq!(state.x_b.len(), m);
debug_assert_eq!(state.at_upper.len(), n_total);
debug_assert_eq!(state.is_basic.len(), n_total);
let mut basis_mgr = match LuBasis::new(a, &state.basis, options.max_etas) {
Ok(bm) => bm,
Err(crate::error::SolverError::SingularBasis { .. }) => {
return (BoundedOutcome::SingularBasis, state);
}
Err(_) => {
let obj = bounded_obj(c, &state.basis, &state.x_b, &state.at_upper, &state.is_basic, ubs);
return (BoundedOutcome::Timeout(obj), state);
}
};
let c_perturbed: Vec<f64> = c.iter().map(|&v| v.max(0.0)).collect();
let mut buf = IterBuffers::new(m, n_total, ubs);
compute_reduced_costs_into(
a,
&c_perturbed,
&mut basis_mgr,
&state.is_basic,
n_total,
&state.basis,
&mut buf.y,
&mut state.reduced_costs,
);
loop {
state.iterations = state.iterations.saturating_add(1);
if state.iterations > BOUNDED_DUAL_ITER_HARD_CAP {
let obj = bounded_obj(c, &state.basis, &state.x_b, &state.at_upper, &state.is_basic, ubs);
return (BoundedOutcome::Timeout(obj), state);
}
let timed_out = options.deadline.is_some_and(|d| std::time::Instant::now() >= d);
let cancelled = options
.cancel_flag
.as_ref()
.is_some_and(|f| f.load(Ordering::Relaxed));
if timed_out || cancelled {
let obj = bounded_obj(c, &state.basis, &state.x_b, &state.at_upper, &state.is_basic, ubs);
return (BoundedOutcome::Timeout(obj), state);
}
let mut leaving_row: Option<usize> = None;
let mut best_viol = options.primal_tol;
let mut ub_violation_row: Option<usize> = None;
for i in 0..m {
let xi = state.x_b[i];
let ub_i = ubs[state.basis[i]];
if xi < -best_viol {
best_viol = -xi;
leaving_row = Some(i);
}
if ub_i.is_finite() && xi > ub_i + options.primal_tol {
ub_violation_row.get_or_insert(i);
}
}
if leaving_row.is_none() {
if let Some(row) = ub_violation_row {
return (BoundedOutcome::UbViolationOutOfScope { row }, state);
}
let obj = basic_obj(c, &state.basis, &state.x_b);
let mut y = vec![0.0; m];
compute_dual_vars_into(&c_perturbed, &mut basis_mgr, &state.basis, &mut y);
return (BoundedOutcome::Optimal(obj, y), state);
}
let r = leaving_row.unwrap();
for slot in buf.rho.iter_mut() {
*slot = 0.0;
}
buf.rho[r] = 1.0;
let mut rho_sv = SparseVec::from_dense(&buf.rho);
basis_mgr.btran(&mut rho_sv);
rho_sv.to_dense_into(&mut buf.rho);
for j in 0..n_total {
if state.is_basic[j] {
buf.trow[j] = 0.0;
continue;
}
let (rows, vals) = a.get_column(j).unwrap();
let mut dot = 0.0;
for (k, &row) in rows.iter().enumerate() {
dot += buf.rho[row] * vals[k];
}
buf.trow[j] = dot;
}
for j in 0..n_total {
buf.col_bounds[j].at_upper = state.at_upper[j];
}
let leaving_residual = state.x_b[r]; let bfrt = match bfrt_select_entering(
&buf.trow,
&state.reduced_costs,
&state.is_basic,
&buf.col_bounds,
n_total,
PIVOT_TOL,
leaving_residual,
) {
None => {
return (BoundedOutcome::Unbounded, state);
}
Some(res) => res,
};
let apply_flip = !flip_apply_disabled();
for &k in &bfrt.flips {
let u_k = ubs[k];
debug_assert!(
u_k.is_finite(),
"BFRT must not return infinite-upper flips"
);
if apply_flip {
ftran_column(a, &mut basis_mgr, k, m, &mut buf.alpha_flip);
let direction = if state.at_upper[k] { -1.0 } else { 1.0 };
let weight = direction * u_k;
for i in 0..m {
state.x_b[i] -= buf.alpha_flip[i] * weight;
}
}
state.at_upper[k] = !state.at_upper[k];
}
let entering_col = bfrt.entering_col;
let theta = bfrt.theta;
let entering_at_upper = state.at_upper[entering_col];
ftran_column(a, &mut basis_mgr, entering_col, m, &mut buf.alpha);
let pivot_element = buf.alpha[r];
if pivot_element.abs() < PIVOT_TOL {
basis_mgr.refactor_if_needed_timed(a, &state.basis, options.deadline);
if basis_mgr.refactor_failed {
if basis_mgr.singular_basis {
return (BoundedOutcome::SingularBasis, state);
}
let obj = bounded_obj(c, &state.basis, &state.x_b, &state.at_upper, &state.is_basic, ubs);
return (BoundedOutcome::Timeout(obj), state);
}
compute_reduced_costs_into(
a,
&c_perturbed,
&mut basis_mgr,
&state.is_basic,
n_total,
&state.basis,
&mut buf.y,
&mut state.reduced_costs,
);
continue;
}
let step = state.x_b[r] / pivot_element;
for i in 0..m {
state.x_b[i] -= buf.alpha[i] * step;
}
state.x_b[r] = step;
if entering_at_upper {
let u_q = ubs[entering_col];
debug_assert!(u_q.is_finite(), "at_upper entering must be finite");
state.x_b[r] += u_q;
}
for val in state.x_b.iter_mut() {
if val.abs() < options.clamp_tol {
*val = 0.0;
}
}
let leaving_col = state.basis[r];
for j in 0..n_total {
if !state.is_basic[j] {
state.reduced_costs[j] -= theta * buf.trow[j];
}
}
if leaving_col < n_total {
state.reduced_costs[leaving_col] = -theta;
}
state.is_basic[entering_col] = true;
state.at_upper[entering_col] = false;
if leaving_col < n_total {
state.is_basic[leaving_col] = false;
state.at_upper[leaving_col] = false;
}
let (col_rows, col_vals) = a.get_column(entering_col).unwrap();
let alpha_sv = SparseVec {
indices: col_rows.to_vec(),
values: col_vals.to_vec(),
len: m,
};
let mut alpha_sv_for_update = alpha_sv;
basis_mgr.ftran(&mut alpha_sv_for_update);
basis_mgr.update(entering_col, r, &alpha_sv_for_update);
state.basis[r] = entering_col;
if basis_mgr.needs_refactor() {
basis_mgr.refactor_if_needed_timed(a, &state.basis, options.deadline);
if basis_mgr.refactor_failed {
if basis_mgr.singular_basis {
return (BoundedOutcome::SingularBasis, state);
}
let obj = bounded_obj(c, &state.basis, &state.x_b, &state.at_upper, &state.is_basic, ubs);
return (BoundedOutcome::Timeout(obj), state);
}
compute_reduced_costs_into(
a,
&c_perturbed,
&mut basis_mgr,
&state.is_basic,
n_total,
&state.basis,
&mut buf.y,
&mut state.reduced_costs,
);
}
}
}
fn ftran_column(
a: &CscMatrix,
basis_mgr: &mut LuBasis,
col: usize,
m: usize,
out: &mut [f64],
) {
let (rows, vals) = a.get_column(col).unwrap();
let mut sv = SparseVec {
indices: rows.to_vec(),
values: vals.to_vec(),
len: m,
};
basis_mgr.ftran(&mut sv);
sv.to_dense_into(out);
}
#[cfg(test)]
thread_local! {
static AT_UPPER_APPLY_DISABLE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
#[cfg(test)]
pub(crate) fn set_at_upper_apply_disabled(v: bool) {
AT_UPPER_APPLY_DISABLE.with(|c| c.set(v));
}
#[cfg(test)]
fn at_upper_apply_disabled() -> bool {
AT_UPPER_APPLY_DISABLE.with(|c| c.get())
}
#[cfg(not(test))]
#[inline(always)]
fn at_upper_apply_disabled() -> bool {
false
}
pub(crate) fn extract_solution_bounded(
bsf: &BoundedStandardForm,
state: &BoundedDualState,
col_scale: &[f64],
) -> Vec<f64> {
use twofloat::TwoFloat;
let mut x_new = vec![0.0f64; bsf.n_shifted];
for i in 0..bsf.m {
let j = state.basis[i];
if j < bsf.n_shifted {
let scale = col_scale.get(j).copied().unwrap_or(1.0);
x_new[j] = state.x_b[i] * scale;
}
}
if !at_upper_apply_disabled() {
for j in 0..bsf.n_shifted {
if !state.is_basic[j] && state.at_upper[j] {
x_new[j] = bsf.upper_bounds[j];
}
}
}
let mut solution = vec![0.0f64; bsf.n_orig];
for (orig_j, sol_j) in solution.iter_mut().enumerate() {
let info = &bsf.orig_var_info[orig_j];
let mut value = TwoFloat::from(info.offset);
for &(new_idx, coeff) in &info.new_vars {
value += TwoFloat::new_mul(coeff, x_new[new_idx]);
}
*sol_j = f64::from(value);
}
solution
}
pub(crate) fn extract_dual_info_bounded(
bsf: &BoundedStandardForm,
problem: &LpProblem,
y_std: &[f64],
solution: &[f64],
row_scale: &[f64],
) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let m_orig = bsf.m;
let n_orig = bsf.n_orig;
let mut dual_solution = vec![0.0f64; m_orig];
for i in 0..m_orig {
let sign = if bsf.row_negated[i] { -1.0 } else { 1.0 };
let rs = row_scale.get(i).copied().unwrap_or(1.0);
dual_solution[i] = sign * rs * y_std[i];
}
let mut slack = problem.b.clone();
for (j, &sol_j) in solution.iter().enumerate().take(n_orig) {
if let Ok((rows, vals)) = problem.a.get_column(j) {
for (k, &row) in rows.iter().enumerate() {
slack[row] -= vals[k] * sol_j;
}
}
}
let mut reduced_costs = problem.c.clone();
for (j, rc_j) in reduced_costs.iter_mut().enumerate().take(n_orig) {
if let Ok((rows, vals)) = problem.a.get_column(j) {
for (k, &row) in rows.iter().enumerate() {
*rc_j -= dual_solution[row] * vals[k];
}
}
}
(dual_solution, reduced_costs, slack)
}
const PHASE2_PRIMAL_ITER_CAP: usize = SIMPLEX_ITER_HARD_CAP;
fn bounded_obj(
c: &[f64],
basis: &[usize],
x_b: &[f64],
at_upper: &[bool],
is_basic: &[bool],
ubs: &[f64],
) -> f64 {
let basic: f64 = basis.iter().zip(x_b.iter()).map(|(&j, &v)| c[j] * v).sum();
let at_ub: f64 = at_upper
.iter()
.enumerate()
.filter(|&(j, &flag)| flag && !is_basic.get(j).copied().unwrap_or(false))
.map(|(j, _)| c.get(j).copied().unwrap_or(0.0) * ubs.get(j).copied().unwrap_or(0.0))
.sum();
basic + at_ub
}
pub(crate) fn phase2_primal_bounded(
bsf: &BoundedStandardForm,
mut state: BoundedDualState,
a: &CscMatrix,
c: &[f64],
options: &SolverOptions,
iters: &mut usize,
ubs: &[f64],
) -> (SimplexOutcome, BoundedDualState) {
let m = bsf.m;
let n_total = bsf.n_total;
let mut basis_mgr = match LuBasis::new(a, &state.basis, options.max_etas) {
Ok(bm) => bm,
Err(_) => return (SimplexOutcome::SingularBasis, state),
};
let mut y = vec![0.0f64; m];
let mut rc = vec![0.0f64; n_total];
let mut alpha = vec![0.0f64; m];
loop {
*iters = iters.saturating_add(1);
if *iters > PHASE2_PRIMAL_ITER_CAP {
return (SimplexOutcome::Timeout(bounded_obj(c, &state.basis, &state.x_b, &state.at_upper, &state.is_basic, ubs)), state);
}
if options.deadline.is_some_and(|d| std::time::Instant::now() >= d) {
return (SimplexOutcome::Timeout(bounded_obj(c, &state.basis, &state.x_b, &state.at_upper, &state.is_basic, ubs)), state);
}
compute_reduced_costs_into(a, c, &mut basis_mgr, &state.is_basic, n_total,
&state.basis, &mut y, &mut rc);
let mut best_score = PIVOT_TOL;
let mut entering: Option<usize> = None;
for j in 0..n_total {
if state.is_basic[j] {
continue;
}
let score = if state.at_upper[j] { rc[j] } else { -rc[j] };
if score > best_score {
best_score = score;
entering = Some(j);
}
}
let q = match entering {
None => {
let obj = bounded_obj(
c, &state.basis, &state.x_b, &state.at_upper, &state.is_basic, ubs,
);
return (SimplexOutcome::Optimal(obj, y), state);
}
Some(q) => q,
};
let from_ub = state.at_upper[q];
let dir = if from_ub { -1.0f64 } else { 1.0 };
ftran_column(a, &mut basis_mgr, q, m, &mut alpha);
let mut min_step = f64::INFINITY;
let mut leaving_row: Option<usize> = None;
let mut leaving_at_ub = false;
for i in 0..m {
let eff = alpha[i] * dir;
let xi = state.x_b[i];
let ub_i = ubs[state.basis[i]];
if eff > PIVOT_TOL {
let step = xi / eff;
if step < min_step {
min_step = step.max(0.0);
leaving_row = Some(i);
leaving_at_ub = false;
}
} else if eff < -PIVOT_TOL && ub_i.is_finite() {
let step = (ub_i - xi) / (-eff);
if step < min_step {
min_step = step.max(0.0);
leaving_row = Some(i);
leaving_at_ub = true;
}
}
}
let ub_q = ubs[q];
if ub_q.is_finite() && ub_q < min_step {
bump_bfrt_flip_invocations();
for i in 0..m {
state.x_b[i] -= alpha[i] * dir * ub_q;
}
state.at_upper[q] = !from_ub;
basis_mgr.refactor_if_needed_timed(a, &state.basis, options.deadline);
if basis_mgr.refactor_failed {
return if basis_mgr.singular_basis {
(SimplexOutcome::SingularBasis, state)
} else {
(SimplexOutcome::Timeout(bounded_obj(c, &state.basis, &state.x_b, &state.at_upper, &state.is_basic, ubs)), state)
};
}
continue;
}
let r = match leaving_row {
None => return (SimplexOutcome::Unbounded, state),
Some(r) => r,
};
let theta = min_step;
let leaving_col = state.basis[r];
for i in 0..m {
state.x_b[i] -= alpha[i] * dir * theta;
}
state.x_b[r] = if from_ub { ub_q - theta } else { theta };
for v in state.x_b.iter_mut() {
if v.abs() < options.clamp_tol {
*v = 0.0;
}
}
state.at_upper[leaving_col] = leaving_at_ub;
state.at_upper[q] = false; state.is_basic[leaving_col] = false;
state.is_basic[q] = true;
state.basis[r] = q;
let (cr, cv) = a.get_column(q).unwrap();
let mut alpha_sv = SparseVec { indices: cr.to_vec(), values: cv.to_vec(), len: m };
basis_mgr.ftran(&mut alpha_sv);
basis_mgr.update(q, r, &alpha_sv);
if basis_mgr.needs_refactor() {
basis_mgr.refactor_if_needed_timed(a, &state.basis, options.deadline);
if basis_mgr.refactor_failed {
return if basis_mgr.singular_basis {
(SimplexOutcome::SingularBasis, state)
} else {
(SimplexOutcome::Timeout(bounded_obj(c, &state.basis, &state.x_b, &state.at_upper, &state.is_basic, ubs)), state)
};
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::options::SolverOptions;
use crate::problem::{ConstraintType, LpProblem};
use crate::simplex::dual_advanced::bound_flip::{
bfrt_flip_invocations, reset_bfrt_flip_invocations,
};
use crate::simplex::standard_form::build_bounded_standard_form;
use crate::sparse::CscMatrix;
const INVARIANT_TOL: f64 = 1e-6;
struct FlipApplyGuard;
impl FlipApplyGuard {
fn disabled() -> Self {
set_flip_apply_disabled(true);
Self
}
}
impl Drop for FlipApplyGuard {
fn drop(&mut self) {
set_flip_apply_disabled(false);
}
}
fn lp_boxed_2x2_degenerate() -> LpProblem {
let rows = vec![0, 0, 1, 1];
let cols = vec![0, 1, 0, 1];
let vals = vec![1.0, 1.0, 1.0, -1.0];
let a = CscMatrix::from_triplets(&rows, &cols, &vals, 2, 2).unwrap();
let b = vec![6.0, 2.0];
let c = vec![-1.0, -1.0];
let ctypes = vec![ConstraintType::Le, ConstraintType::Le];
let bounds = vec![(0.0, 4.0), (0.0, 4.0)];
LpProblem::new_general(c, a, b, ctypes, bounds, None).unwrap()
}
fn lp_mixed_bounds_degenerate() -> LpProblem {
let n = 4;
let m = 2;
let rows = vec![0, 0, 0, 0, 1, 1];
let cols = vec![0, 1, 2, 3, 0, 1];
let vals = vec![1.0, 1.0, 1.0, 1.0, 2.0, 1.0];
let a = CscMatrix::from_triplets(&rows, &cols, &vals, m, n).unwrap();
let b = vec![10.0, 8.0];
let c = vec![-1.0, -2.0, -1.0, 0.0];
let ctypes = vec![ConstraintType::Le, ConstraintType::Le];
let bounds = vec![
(0.0, 3.0),
(0.0, f64::INFINITY),
(0.0, 5.0),
(2.0, 2.0),
];
LpProblem::new_general(c, a, b, ctypes, bounds, None).unwrap()
}
struct InvariantFixture {
name: &'static str,
problem: LpProblem,
inject_negative_x_b: Vec<(usize, f64)>,
}
fn fixture_one_row_two_boxed() -> InvariantFixture {
let rows = vec![0, 0];
let cols = vec![0, 1];
let vals = vec![1.0, 1.0];
let a = CscMatrix::from_triplets(&rows, &cols, &vals, 1, 2).unwrap();
let b = vec![5.0];
let c = vec![1.0, 2.0];
let ctypes = vec![ConstraintType::Le];
let bounds = vec![(0.0, 4.0), (0.0, 3.0)];
let problem = LpProblem::new_general(c, a, b, ctypes, bounds, None).unwrap();
InvariantFixture {
name: "one_row_two_boxed",
problem,
inject_negative_x_b: vec![(0, 3.0)],
}
}
fn fixture_two_rows_three_boxed() -> InvariantFixture {
let rows = vec![0, 0, 0, 1, 1, 1];
let cols = vec![0, 1, 2, 0, 1, 2];
let vals = vec![1.0, 1.0, 1.0, 0.5, 1.0, 2.0];
let a = CscMatrix::from_triplets(&rows, &cols, &vals, 2, 3).unwrap();
let b = vec![7.0, 6.0];
let c = vec![1.0, 3.0, 5.0];
let ctypes = vec![ConstraintType::Le, ConstraintType::Le];
let bounds = vec![(0.0, 2.0), (0.0, 2.0), (0.0, 1.0)];
let problem = LpProblem::new_general(c, a, b, ctypes, bounds, None).unwrap();
InvariantFixture {
name: "two_rows_three_boxed",
problem,
inject_negative_x_b: vec![(0, 3.0)],
}
}
fn basis_rhs_residual(
state: &BoundedDualState,
bsf: &BoundedStandardForm,
) -> f64 {
let mut x_full = vec![0.0; bsf.n_total];
for (pos, &j) in state.basis.iter().enumerate() {
x_full[j] = state.x_b[pos];
}
for j in 0..bsf.n_total {
if state.at_upper[j] && !state.is_basic[j] {
x_full[j] = bsf.upper_bounds[j];
}
}
let mut residual = vec![0.0; bsf.m];
for j in 0..bsf.n_total {
let xj = x_full[j];
if xj == 0.0 {
continue;
}
if let Ok((rows, vals)) = bsf.a.get_column(j) {
for (k, &row) in rows.iter().enumerate() {
residual[row] += vals[k] * xj;
}
}
}
for i in 0..bsf.m {
residual[i] -= bsf.b[i];
}
residual.iter().fold(0.0_f64, |acc, &v| acc.max(v.abs()))
}
#[test]
fn cold_state_from_bsf_has_consistent_dimensions() {
let lp = lp_boxed_2x2_degenerate();
let bsf = build_bounded_standard_form(&lp);
let state = BoundedDualState::cold(&bsf, &bsf.b);
assert_eq!(state.basis.len(), bsf.m);
assert_eq!(state.x_b.len(), bsf.m);
assert_eq!(state.at_upper.len(), bsf.n_total);
assert_eq!(state.is_basic.len(), bsf.n_total);
for j in 0..bsf.n_shifted {
assert!(!state.is_basic[j]);
assert!(!state.at_upper[j]);
}
assert_eq!(state.x_b, bsf.b);
}
#[test]
fn cold_dual_le_only_terminates_immediately() {
let lp = lp_boxed_2x2_degenerate();
let bsf = build_bounded_standard_form(&lp);
let opts = SolverOptions::default();
let (outcome, state) = solve_bounded_dual(&bsf, &bsf.a, &bsf.b, &bsf.c, &opts, &bsf.upper_bounds);
match outcome {
BoundedOutcome::Optimal(_, _) => {}
other => panic!("expected Optimal, got {:?}", other),
}
assert_eq!(state.iterations, 1);
}
#[test]
fn fixed_variable_does_not_break_iteration() {
let lp = lp_mixed_bounds_degenerate();
let bsf = build_bounded_standard_form(&lp);
let mut state = BoundedDualState::cold(&bsf, &bsf.b);
state.x_b[0] = -0.5;
let opts = SolverOptions {
deadline: Some(std::time::Instant::now() + std::time::Duration::from_millis(500)),
..SolverOptions::default()
};
let (outcome, _state) = iterate(state, &bsf, &bsf.a, &bsf.c, &opts, &bsf.upper_bounds);
match outcome {
BoundedOutcome::Optimal(_, _) => {}
BoundedOutcome::Timeout(_) => {}
BoundedOutcome::UbViolationOutOfScope { .. } => {}
other => panic!("unexpected outcome {:?}", other),
}
}
fn measure_iterate_residual(
fx: &InvariantFixture,
deadline_ms: u64,
flip_disabled: bool,
) -> (f64, f64, u64) {
reset_bfrt_flip_invocations();
let bsf = build_bounded_standard_form(&fx.problem);
let mut state = BoundedDualState::cold(&bsf, &bsf.b);
for &(row, mag) in &fx.inject_negative_x_b {
state.x_b[row] = -mag;
}
let pre_residual = basis_rhs_residual(&state, &bsf);
let opts = SolverOptions {
deadline: Some(
std::time::Instant::now() + std::time::Duration::from_millis(deadline_ms),
),
..SolverOptions::default()
};
let _guard = if flip_disabled {
Some(FlipApplyGuard::disabled())
} else {
None
};
let (_outcome, post) = iterate(state, &bsf, &bsf.a, &bsf.c, &opts, &bsf.upper_bounds);
let post_residual = basis_rhs_residual(&post, &bsf);
let flips = bfrt_flip_invocations();
(pre_residual, post_residual, flips)
}
#[test]
fn flip_apply_preserves_basis_rhs_invariant() {
let fixtures = [fixture_one_row_two_boxed(), fixture_two_rows_three_boxed()];
let mut at_least_one_flip = false;
for fx in &fixtures {
let (pre, post, flips) = measure_iterate_residual(fx, 10, false);
let drift = (post - pre).abs();
assert!(
drift < INVARIANT_TOL,
"{}: production iterate drifted the algebraic invariant by \
{drift:.3e} (pre={pre:.3e}, post={post:.3e}, flips={flips}) \
— flip apply is no longer preserving A·x_full = b",
fx.name,
);
if flips > 0 {
at_least_one_flip = true;
}
}
assert!(
at_least_one_flip,
"no fixture exercised a BFRT flip — the sentinel proves nothing \
about the flip apply path"
);
}
#[test]
fn flip_apply_preserves_basis_rhs_invariant_noop_proof() {
let fixtures = [fixture_one_row_two_boxed(), fixture_two_rows_three_boxed()];
let mut max_drift = 0.0_f64;
let mut max_drift_fixture = "<none>";
let mut total_flips = 0;
for fx in &fixtures {
let (pre, post, flips) = measure_iterate_residual(fx, 10, true);
let drift = (post - pre).abs();
total_flips += flips;
if drift > max_drift {
max_drift = drift;
max_drift_fixture = fx.name;
}
}
assert!(
total_flips > 0,
"no BFRT flip happened under FLIP_APPLY_DISABLE either — the \
fixture set does not exercise the flip path at all"
);
assert!(
max_drift > INVARIANT_TOL,
"no-op flip apply produced max drift {max_drift:.3e} (fixture \
'{max_drift_fixture}', total_flips={total_flips}) ≤ {INVARIANT_TOL:.0e} \
— the production correctness sentinel could not have detected \
the broken flip apply"
);
}
#[test]
fn bfrt_flip_count_positive_when_residual_spans_breakpoints() {
let fx = fixture_two_rows_three_boxed();
let (pre, post, flips) = measure_iterate_residual(&fx, 10, false);
assert!(
flips >= 1,
"expected BFRT flip count ≥ 1, got {flips} — fixture no longer \
exercises BFRT"
);
let drift = (post - pre).abs();
assert!(
drift < INVARIANT_TOL,
"{}: invariant drifted by {drift:.3e} despite {flips} flips — \
flip apply not preserving A·x_full = b",
fx.name,
);
}
#[test]
fn inject_lb_violation_makes_progress_boxed() {
let fx = fixture_two_rows_three_boxed();
let (pre, post, flips) = measure_iterate_residual(&fx, 10, false);
assert!(
flips > 0,
"{}: zero BFRT invocations — loop did no flip work",
fx.name,
);
let drift = (post - pre).abs();
assert!(
drift < INVARIANT_TOL,
"{}: invariant drifted by {drift:.3e} during {flips} flips — \
pivot / flip algebra broken",
fx.name,
);
}
#[test]
fn ub_violation_returns_specialised_outcome() {
let fx = fixture_two_rows_three_boxed();
let bsf = build_bounded_standard_form(&fx.problem);
let mut state = BoundedDualState::cold(&bsf, &bsf.b);
let target_col = 2; assert!(bsf.upper_bounds[target_col].is_finite());
state.basis[0] = target_col;
state.is_basic[target_col] = true;
let prev_slack = bsf.initial_basis[0];
if prev_slack != target_col {
state.is_basic[prev_slack] = false;
}
state.x_b[0] = bsf.upper_bounds[target_col] + 1.5; state.x_b[1] = state.x_b[1].max(0.0);
let opts = SolverOptions {
deadline: Some(std::time::Instant::now() + std::time::Duration::from_millis(200)),
..SolverOptions::default()
};
let (outcome, _post) = iterate(state, &bsf, &bsf.a, &bsf.c, &opts, &bsf.upper_bounds);
match outcome {
BoundedOutcome::UbViolationOutOfScope { row, .. } => {
assert_eq!(row, 0);
}
BoundedOutcome::SingularBasis => {}
other => panic!(
"expected UbViolationOutOfScope or SingularBasis, got {:?}",
other
),
}
}
struct AtUpperApplyGuard;
impl AtUpperApplyGuard {
fn disabled() -> Self {
set_at_upper_apply_disabled(true);
Self
}
}
impl Drop for AtUpperApplyGuard {
fn drop(&mut self) {
set_at_upper_apply_disabled(false);
}
}
struct ExtractFixture {
name: &'static str,
problem: LpProblem,
flip_to_upper: Vec<usize>,
expected: Vec<f64>,
}
fn fixture_unbounded_compat() -> ExtractFixture {
let rows = vec![0, 0];
let cols = vec![0, 1];
let vals = vec![1.0, 1.0];
let a = CscMatrix::from_triplets(&rows, &cols, &vals, 1, 2).unwrap();
let problem = LpProblem::new_general(
vec![1.0, 2.0],
a,
vec![3.0],
vec![ConstraintType::Le],
vec![(0.0, f64::INFINITY), (0.0, f64::INFINITY)],
None,
).unwrap();
ExtractFixture {
name: "unbounded_compat",
problem,
flip_to_upper: vec![], expected: vec![0.0, 0.0],
}
}
fn fixture_boxed_ub1() -> ExtractFixture {
let rows = vec![0, 0];
let cols = vec![0, 1];
let vals = vec![1.0, 1.0];
let a = CscMatrix::from_triplets(&rows, &cols, &vals, 1, 2).unwrap();
let problem = LpProblem::new_general(
vec![1.0, 1.0],
a,
vec![5.0],
vec![ConstraintType::Le],
vec![(0.0, 1.0), (0.0, 1.0)],
None,
).unwrap();
ExtractFixture {
name: "boxed_ub1",
problem,
flip_to_upper: vec![0], expected: vec![1.0, 0.0],
}
}
fn fixture_nonzero_lb() -> ExtractFixture {
let rows = vec![0, 0];
let cols = vec![0, 1];
let vals = vec![1.0, 1.0];
let a = CscMatrix::from_triplets(&rows, &cols, &vals, 1, 2).unwrap();
let problem = LpProblem::new_general(
vec![1.0, 1.0],
a,
vec![5.0],
vec![ConstraintType::Le],
vec![(-5.0, 5.0), (-5.0, 5.0)],
None,
).unwrap();
ExtractFixture {
name: "nonzero_lb",
problem,
flip_to_upper: vec![0], expected: vec![5.0, -5.0],
}
}
fn state_with_flips(bsf: &BoundedStandardForm, flip_cols: &[usize]) -> BoundedDualState {
let mut state = BoundedDualState::cold(bsf, &bsf.b);
for &j in flip_cols {
assert!(!state.is_basic[j], "can only flip non-basic columns");
assert!(bsf.upper_bounds[j].is_finite(), "flip target must have finite ub");
state.at_upper[j] = true;
let (rows, vals) = bsf.a.get_column(j).unwrap();
for (k, &row) in rows.iter().enumerate() {
if row < bsf.m {
state.x_b[row] -= vals[k] * bsf.upper_bounds[j];
}
}
}
state
}
#[test]
fn extract_solution_bounded_multi_fixture() {
let fixtures = [
fixture_unbounded_compat(),
fixture_boxed_ub1(),
fixture_nonzero_lb(),
];
const EPS: f64 = 1e-10;
for fx in &fixtures {
let bsf = build_bounded_standard_form(&fx.problem);
let state = state_with_flips(&bsf, &fx.flip_to_upper);
let sol = extract_solution_bounded(&bsf, &state, &[]);
assert_eq!(
sol.len(),
fx.expected.len(),
"{}: solution length mismatch",
fx.name
);
for (i, (&got, &want)) in sol.iter().zip(fx.expected.iter()).enumerate() {
assert!(
(got - want).abs() < EPS,
"{}: solution[{}] = {got:.6e}, expected {want:.6e}",
fx.name,
i
);
}
}
}
#[test]
fn extract_solution_bounded_matches_unbounded_when_no_at_upper() {
use crate::simplex::primal::extract_solution;
use crate::simplex::standard_form::build_standard_form;
const EPS: f64 = 1e-10;
let fx = fixture_unbounded_compat();
let bsf = build_bounded_standard_form(&fx.problem);
let sf = build_standard_form(&fx.problem);
let opts = SolverOptions::default();
let (outcome, state) = solve_bounded_dual(&bsf, &bsf.a, &bsf.b, &bsf.c, &opts, &bsf.upper_bounds);
assert!(
matches!(outcome, BoundedOutcome::Optimal(..)),
"expected Optimal, got {:?}", outcome
);
let any_upper = state.at_upper.iter().enumerate()
.any(|(j, &u)| u && !state.is_basic[j]);
assert!(!any_upper, "unexpected at_upper set in unbounded-compat fixture");
let sol_bounded = extract_solution_bounded(&bsf, &state, &[]);
let sol_std = extract_solution(&sf, &state.basis, &state.x_b, &[]);
for (i, (&a, &b)) in sol_bounded.iter().zip(sol_std.iter()).enumerate() {
assert!(
(a - b).abs() < EPS,
"bounded[{}]={a:.3e} vs unbounded[{}]={b:.3e}", i, i
);
}
}
#[test]
fn extract_solution_bounded_noop_proof() {
const EPS: f64 = 1e-6;
let fx = fixture_boxed_ub1();
let bsf = build_bounded_standard_form(&fx.problem);
let state = state_with_flips(&bsf, &fx.flip_to_upper);
let sol_correct = extract_solution_bounded(&bsf, &state, &[]);
let sol_noop = {
let _guard = AtUpperApplyGuard::disabled();
extract_solution_bounded(&bsf, &state, &[])
};
assert!(
(sol_correct[0] - 1.0).abs() < EPS,
"correct solution[0] should be 1.0, got {}", sol_correct[0]
);
assert!(
sol_noop[0].abs() < EPS,
"noop solution[0] should be 0.0 (correction disabled), got {}", sol_noop[0]
);
let max_diff = sol_correct.iter().zip(sol_noop.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0f64, f64::max);
assert!(
max_diff > EPS,
"no-op proof FAILED: correct and noop solutions are identical (diff={max_diff:.3e}) \
— the at_upper correction is not load-bearing in this fixture"
);
}
#[test]
fn extract_dual_info_bounded_smoke() {
use crate::simplex::standard_form::extract_dual_info;
use crate::simplex::standard_form::build_standard_form;
let fx = fixture_unbounded_compat();
let bsf = build_bounded_standard_form(&fx.problem);
let sf = build_standard_form(&fx.problem);
let opts = SolverOptions::default();
let (outcome, state) = solve_bounded_dual(&bsf, &bsf.a, &bsf.b, &bsf.c, &opts, &bsf.upper_bounds);
let (_, y_std) = match outcome {
BoundedOutcome::Optimal(obj, y) => (obj, y),
other => panic!("expected Optimal, got {:?}", other),
};
let solution = extract_solution_bounded(&bsf, &state, &[]);
let (dual_b, rc_b, slack_b) = extract_dual_info_bounded(
&bsf, &fx.problem, &y_std, &solution, &[],
);
let (dual_s, rc_s, slack_s) = extract_dual_info(
&sf, &fx.problem, &y_std[..sf.m.min(y_std.len())], &solution, &[],
);
const EPS: f64 = 1e-8;
for i in 0..dual_b.len() {
assert!(
(dual_b[i] - dual_s[i]).abs() < EPS,
"dual[{}]: bounded={:.3e}, std={:.3e}", i, dual_b[i], dual_s[i]
);
}
for j in 0..rc_b.len() {
assert!(
(rc_b[j] - rc_s[j]).abs() < EPS,
"rc[{}]: bounded={:.3e}, std={:.3e}", j, rc_b[j], rc_s[j]
);
}
for i in 0..slack_b.len() {
assert!(
(slack_b[i] - slack_s[i]).abs() < EPS,
"slack[{}]: bounded={:.3e}, std={:.3e}", i, slack_b[i], slack_s[i]
);
}
}
#[test]
fn phase2_primal_bounded_reaches_known_optimal() {
let lp = lp_boxed_2x2_degenerate();
let bsf = build_bounded_standard_form(&lp);
let opts = SolverOptions::default();
let (dual_outcome, dual_state) = solve_bounded_dual(&bsf, &bsf.a, &bsf.b, &bsf.c, &opts, &bsf.upper_bounds);
assert!(matches!(dual_outcome, BoundedOutcome::Optimal(..)));
let mut iters = 0usize;
let (p2_outcome, p2_state) = phase2_primal_bounded(
&bsf, dual_state, &bsf.a, &bsf.c, &opts, &mut iters, &bsf.upper_bounds,
);
assert!(
matches!(p2_outcome, SimplexOutcome::Optimal(..)),
"Phase 2 did not reach Optimal: {:?}", p2_outcome
);
let sol = extract_solution_bounded(&bsf, &p2_state, &[]);
let obj: f64 = lp.c.iter().zip(sol.iter()).map(|(c, x)| c * x).sum();
assert!(
(obj - (-6.0)).abs() < 1e-6,
"expected obj=-6.0, got {obj:.6e}"
);
assert!(
(sol[0] - 4.0).abs() < 1e-6 && (sol[1] - 2.0).abs() < 1e-6,
"expected x=(4,2), got ({:.3e},{:.3e})", sol[0], sol[1]
);
assert!(iters > 0, "phase2 should have made at least one iteration");
}
#[test]
fn phase2_primal_bounded_noop_when_already_optimal() {
let fx = fixture_one_row_two_boxed(); let bsf = build_bounded_standard_form(&fx.problem);
let opts = SolverOptions::default();
let (dual_outcome, dual_state) = solve_bounded_dual(&bsf, &bsf.a, &bsf.b, &bsf.c, &opts, &bsf.upper_bounds);
assert!(matches!(dual_outcome, BoundedOutcome::Optimal(..)));
let mut iters = 0usize;
let (p2_outcome, _) = phase2_primal_bounded(
&bsf, dual_state, &bsf.a, &bsf.c, &opts, &mut iters, &bsf.upper_bounds,
);
assert!(
matches!(p2_outcome, SimplexOutcome::Optimal(..)),
"expected Optimal, got {:?}", p2_outcome
);
assert_eq!(iters, 1, "should terminate after one pricing pass (no improvement)");
}
#[test]
fn phase1_dual_timeout_obj_matches_bounded_obj() {
const EPS: f64 = 1e-10;
const MIN_CONTRIBUTION: f64 = 0.5;
let p1_problem = fixture_one_row_two_boxed().problem;
let p2_problem = fixture_two_rows_three_boxed().problem;
let bsfs = [
("one_row_two_boxed_x1_at_upper", build_bounded_standard_form(&p1_problem), 1usize),
("two_rows_three_boxed_x0_at_upper", build_bounded_standard_form(&p2_problem), 0usize),
];
for (name, bsf, flip_col) in &bsfs {
let state = state_with_flips(bsf, &[*flip_col]);
let exp_bounded = bounded_obj(
&bsf.c, &state.basis, &state.x_b,
&state.at_upper, &state.is_basic, &bsf.upper_bounds,
);
let exp_basic = basic_obj(&bsf.c, &state.basis, &state.x_b);
assert!(
(exp_bounded - exp_basic).abs() >= MIN_CONTRIBUTION,
"{name}: fixture degenerate — bounded_obj={exp_bounded:.6e} \
basic_obj={exp_basic:.6e} differ by {:.3e} < {MIN_CONTRIBUTION:.1e}",
(exp_bounded - exp_basic).abs()
);
let deadline = std::time::Instant::now()
- std::time::Duration::from_millis(1);
let opts = SolverOptions { deadline: Some(deadline), ..SolverOptions::default() };
let (outcome, _) = iterate(state, bsf, &bsf.a, &bsf.c, &opts, &bsf.upper_bounds);
match outcome {
BoundedOutcome::Timeout(obj) => {
assert!(
(obj - exp_bounded).abs() < EPS,
"{name}: Phase 1 dual Timeout obj={obj:.6e} differs from \
bounded_obj={exp_bounded:.6e} by {:.3e}; \
basic_obj={exp_basic:.6e} — at_upper contributions missing",
(obj - exp_bounded).abs()
);
}
other => panic!("{name}: expected Timeout (expired deadline), got {other:?}"),
}
}
}
#[test]
fn phase2_primal_timeout_obj_matches_bounded_obj() {
const EPS: f64 = 1e-10;
const MIN_CONTRIBUTION: f64 = 0.5;
let problem = fixture_boxed_ub1().problem;
let bsf = build_bounded_standard_form(&problem);
let state = state_with_flips(&bsf, &[0]);
let exp_bounded = bounded_obj(
&bsf.c, &state.basis, &state.x_b,
&state.at_upper, &state.is_basic, &bsf.upper_bounds,
);
let exp_basic = basic_obj(&bsf.c, &state.basis, &state.x_b);
assert!(
(exp_bounded - exp_basic).abs() >= MIN_CONTRIBUTION,
"fixture degenerate — bounded_obj={exp_bounded:.6e} \
basic_obj={exp_basic:.6e} differ by {:.3e} < {MIN_CONTRIBUTION:.1e}",
(exp_bounded - exp_basic).abs()
);
let deadline = std::time::Instant::now() - std::time::Duration::from_millis(1);
let opts = SolverOptions { deadline: Some(deadline), ..SolverOptions::default() };
let mut iters = 0usize;
let (outcome, _) = phase2_primal_bounded(
&bsf, state, &bsf.a, &bsf.c, &opts, &mut iters, &bsf.upper_bounds,
);
match outcome {
SimplexOutcome::Timeout(obj) => {
assert!(
(obj - exp_bounded).abs() < EPS,
"Phase 2 primal Timeout obj={obj:.6e} differs from \
bounded_obj={exp_bounded:.6e} by {:.3e}; \
basic_obj={exp_basic:.6e} — at_upper contributions missing",
(obj - exp_bounded).abs()
);
}
other => panic!("expected Timeout (expired deadline), got {other:?}"),
}
}
}