use crate::basis::{BasisManager, LuBasis};
use crate::error::SolverError;
use crate::options::SolverOptions;
use crate::problem::LpProblem;
use crate::sparse::{CscMatrix, SparseVec};
use crate::tolerances::PIVOT_TOL;
use std::sync::atomic::Ordering;
use std::time::Instant;
use super::super::dual_common::{
basic_obj, compute_dual_vars_into, made_progress_with_floor, recompute_gamma_truth,
NO_PROGRESS_MIN, NO_PROGRESS_TRIGGER_FACTOR,
};
use super::super::pricing::DualLeavingStrategy;
use super::super::standard_form::{BoundedStandardForm, SimplexOutcome};
use super::super::trace::IterTrace;
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
}
#[inline]
fn deadline_expired(deadline: Option<Instant>) -> bool {
deadline.is_some_and(|d| Instant::now() >= d)
}
fn compute_reduced_costs_into_timed(
a: &CscMatrix,
c: &[f64],
basis_mgr: &mut LuBasis,
is_basic: &[bool],
n_price: usize,
basis: &[usize],
y_buf: &mut [f64],
rc_out: &mut [f64],
deadline: Option<Instant>,
) -> bool {
if deadline_expired(deadline) {
return false;
}
compute_dual_vars_into(c, basis_mgr, basis, y_buf);
for j in 0..n_price {
if deadline_expired(deadline) {
return false;
}
if is_basic[j] {
rc_out[j] = 0.0;
continue;
}
let (rows, vals) = a.get_column(j).unwrap();
let mut ya = 0.0;
for (k, &row) in rows.iter().enumerate() {
ya += y_buf[row] * vals[k];
}
rc_out[j] = c[j] - ya;
}
true
}
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>,
sigma: 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],
sigma: 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],
leaving: &mut dyn DualLeavingStrategy,
) -> (BoundedOutcome, BoundedDualState) {
let state = BoundedDualState::cold(bsf, b);
iterate(state, bsf, a, c, options, ubs, leaving)
}
pub(crate) fn iterate(
mut state: BoundedDualState,
bsf: &BoundedStandardForm,
a: &CscMatrix,
c: &[f64],
options: &SolverOptions,
ubs: &[f64],
leaving: &mut dyn DualLeavingStrategy,
) -> (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_timed(a, &state.basis, options.max_etas, options.deadline) {
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);
}
};
if options
.deadline
.is_some_and(|d| std::time::Instant::now() >= d)
|| options
.cancel_flag
.as_ref()
.is_some_and(|f| f.load(Ordering::Relaxed))
{
let obj = bounded_obj(
c,
&state.basis,
&state.x_b,
&state.at_upper,
&state.is_basic,
ubs,
);
return (BoundedOutcome::Timeout(obj), state);
}
let needs_sigma = leaving.needs_sigma();
if needs_sigma {
match recompute_gamma_truth(
&mut basis_mgr,
m,
options.deadline,
options.cancel_flag.as_deref(),
) {
None => {
let obj = bounded_obj(
c,
&state.basis,
&state.x_b,
&state.at_upper,
&state.is_basic,
ubs,
);
return (BoundedOutcome::Timeout(obj), state);
}
Some(gamma_truth) => leaving.set_initial_gamma(&gamma_truth),
}
}
let c_perturbed: Vec<f64> = c.iter().map(|&v| v.max(0.0)).collect();
let mut buf = IterBuffers::new(m, n_total, ubs);
if !compute_reduced_costs_into_timed(
a,
&c_perturbed,
&mut basis_mgr,
&state.is_basic,
n_total,
&state.basis,
&mut buf.y,
&mut state.reduced_costs,
options.deadline,
) {
let obj = bounded_obj(
c,
&state.basis,
&state.x_b,
&state.at_upper,
&state.is_basic,
ubs,
);
return (BoundedOutcome::Timeout(obj), state);
}
let k_trigger = (NO_PROGRESS_TRIGGER_FACTOR * m).max(NO_PROGRESS_MIN);
let mut best_infeas = leaving.progress_metric(&state.x_b, &state.basis);
let mut iters_since_progress: usize = 0;
let mut bland_mode = false;
let mut trace = IterTrace::new("bounded-dual");
loop {
state.iterations = state.iterations.saturating_add(1);
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);
}
if let Some(t) = trace.as_mut() {
let obj = bounded_obj(
c,
&state.basis,
&state.x_b,
&state.at_upper,
&state.is_basic,
ubs,
);
t.log(state.iterations, obj, &state.basis, bland_mode);
}
let mut ub_violation_row: Option<usize> = None;
for i in 0..m {
if deadline_expired(options.deadline) {
let obj = bounded_obj(
c,
&state.basis,
&state.x_b,
&state.at_upper,
&state.is_basic,
ubs,
);
return (BoundedOutcome::Timeout(obj), state);
}
let xi = state.x_b[i];
let ub_i = ubs[state.basis[i]];
if ub_i.is_finite() && xi > ub_i + options.primal_tol {
ub_violation_row.get_or_insert(i);
}
}
let leaving_row = if bland_mode {
leaving.bland_leaving(&state.x_b, options.primal_tol, &state.basis)
} else {
leaving.select_leaving(&state.x_b, options.primal_tol, &state.basis)
};
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);
if needs_sigma {
let mut sigma_sv = SparseVec::from_dense(&buf.rho);
basis_mgr.ftran(&mut sigma_sv);
sigma_sv.to_dense_into(&mut buf.sigma);
}
for j in 0..n_total {
if deadline_expired(options.deadline) {
let obj = bounded_obj(
c,
&state.basis,
&state.x_b,
&state.at_upper,
&state.is_basic,
ubs,
);
return (BoundedOutcome::Timeout(obj), state);
}
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 {
if deadline_expired(options.deadline) {
let obj = bounded_obj(
c,
&state.basis,
&state.x_b,
&state.at_upper,
&state.is_basic,
ubs,
);
return (BoundedOutcome::Timeout(obj), state);
}
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);
}
leaving.after_refactor(m);
if !compute_reduced_costs_into_timed(
a,
&c_perturbed,
&mut basis_mgr,
&state.is_basic,
n_total,
&state.basis,
&mut buf.y,
&mut state.reduced_costs,
options.deadline,
) {
let obj = bounded_obj(
c,
&state.basis,
&state.x_b,
&state.at_upper,
&state.is_basic,
ubs,
);
return (BoundedOutcome::Timeout(obj), state);
}
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 deadline_expired(options.deadline) {
let obj = bounded_obj(
c,
&state.basis,
&state.x_b,
&state.at_upper,
&state.is_basic,
ubs,
);
return (BoundedOutcome::Timeout(obj), state);
}
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;
leaving.after_pivot(r, &buf.alpha, &buf.sigma, pivot_element);
if !bland_mode {
let current = leaving.progress_metric(&state.x_b, &state.basis);
if made_progress_with_floor(best_infeas, current, 0.0) {
best_infeas = current;
iters_since_progress = 0;
} else {
iters_since_progress += 1;
if iters_since_progress >= k_trigger {
bland_mode = true;
}
}
}
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);
}
leaving.after_refactor(m);
if !compute_reduced_costs_into_timed(
a,
&c_perturbed,
&mut basis_mgr,
&state.is_basic,
n_total,
&state.basis,
&mut buf.y,
&mut state.reduced_costs,
options.deadline,
) {
let obj = bounded_obj(
c,
&state.basis,
&state.x_b,
&state.at_upper,
&state.is_basic,
ubs,
);
return (BoundedOutcome::Timeout(obj), state);
}
}
}
}
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)
}
fn bounded_obj(
c: &[f64],
basis: &[usize],
x_b: &[f64],
at_upper: &[bool],
is_basic: &[bool],
ubs: &[f64],
) -> f64 {
debug_assert_eq!(
at_upper.len(),
is_basic.len(),
"at_upper/is_basic length mismatch"
);
debug_assert_eq!(at_upper.len(), c.len(), "at_upper/c length mismatch");
debug_assert_eq!(at_upper.len(), ubs.len(), "at_upper/ubs length mismatch");
debug_assert_eq!(basis.len(), x_b.len(), "basis/x_b length mismatch");
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(|&(_, &flag)| flag)
.inspect(|&(j, _)| {
debug_assert!(
!is_basic[j],
"invariant at_upper[j] => !is_basic[j] violated at j={j}"
)
})
.map(|(j, _)| c[j] * ubs[j])
.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 timeout_obj = |state: &BoundedDualState| {
SimplexOutcome::Timeout(bounded_obj(
c,
&state.basis,
&state.x_b,
&state.at_upper,
&state.is_basic,
ubs,
))
};
if deadline_expired(options.deadline) {
return (timeout_obj(&state), state);
}
let mut basis_mgr =
match LuBasis::new_timed(a, &state.basis, options.max_etas, options.deadline) {
Ok(bm) => bm,
Err(SolverError::DeadlineExceeded) => return (timeout_obj(&state), state),
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];
let mut trace = IterTrace::new("bounded-primal");
loop {
*iters = iters.saturating_add(1);
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,
);
}
if let Some(t) = trace.as_mut() {
let obj = bounded_obj(
c,
&state.basis,
&state.x_b,
&state.at_upper,
&state.is_basic,
ubs,
);
t.log(*iters, obj, &state.basis, false);
}
if !compute_reduced_costs_into_timed(
a,
c,
&mut basis_mgr,
&state.is_basic,
n_total,
&state.basis,
&mut y,
&mut rc,
options.deadline,
) {
return (
SimplexOutcome::Timeout(bounded_obj(
c,
&state.basis,
&state.x_b,
&state.at_upper,
&state.is_basic,
ubs,
)),
state,
);
}
let mut best_score = PIVOT_TOL;
let mut entering: Option<usize> = None;
for j in 0..n_total {
if deadline_expired(options.deadline) {
return (
SimplexOutcome::Timeout(bounded_obj(
c,
&state.basis,
&state.x_b,
&state.at_upper,
&state.is_basic,
ubs,
)),
state,
);
}
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 {
if deadline_expired(options.deadline) {
return (
SimplexOutcome::Timeout(bounded_obj(
c,
&state.basis,
&state.x_b,
&state.at_upper,
&state.is_basic,
ubs,
)),
state,
);
}
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::pricing::MostInfeasibleLeaving;
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,
&mut MostInfeasibleLeaving,
);
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,
&mut MostInfeasibleLeaving,
);
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,
&mut MostInfeasibleLeaving,
);
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,
&mut MostInfeasibleLeaving,
);
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,
&mut MostInfeasibleLeaving,
);
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::build_standard_form;
use crate::simplex::standard_form::extract_dual_info;
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,
&mut MostInfeasibleLeaving,
);
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,
&mut MostInfeasibleLeaving,
);
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,
&mut MostInfeasibleLeaving,
);
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,
&mut MostInfeasibleLeaving,
);
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:?}"),
}
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "invariant at_upper")]
fn bounded_obj_invariant_violation_panics_in_debug() {
let c = vec![1.0, 2.0];
let basis = vec![0usize];
let x_b = vec![0.0];
let at_upper = vec![false, true]; let is_basic = vec![true, true]; let ubs = vec![5.0, 3.0];
let _ = bounded_obj(&c, &basis, &x_b, &at_upper, &is_basic, &ubs);
}
#[test]
fn dse_expired_deadline_returns_timeout_before_gamma_init() {
use crate::simplex::dual_advanced::steepest_edge::DualSteepestEdgeLeaving;
const EPS: f64 = 1e-10;
let fx = fixture_one_row_two_boxed();
let bsf = build_bounded_standard_form(&fx.problem);
let state_fresh = || BoundedDualState::cold(&bsf, &bsf.b);
let opts_live = SolverOptions {
deadline: Some(std::time::Instant::now() + std::time::Duration::from_millis(500)),
..SolverOptions::default()
};
let (live_outcome, _) = iterate(
state_fresh(),
&bsf,
&bsf.a,
&bsf.c,
&opts_live,
&bsf.upper_bounds,
&mut DualSteepestEdgeLeaving::new(bsf.m),
);
match live_outcome {
BoundedOutcome::Optimal(_, _) => {}
other => panic!("no-op proof: expected Optimal with live deadline, got {other:?}"),
}
let expired = std::time::Instant::now() - std::time::Duration::from_millis(1);
let opts_expired = SolverOptions {
deadline: Some(expired),
..SolverOptions::default()
};
let (outcome, state_out) = iterate(
state_fresh(),
&bsf,
&bsf.a,
&bsf.c,
&opts_expired,
&bsf.upper_bounds,
&mut DualSteepestEdgeLeaving::new(bsf.m),
);
match outcome {
BoundedOutcome::Timeout(obj) => {
let exp = bounded_obj(
&bsf.c,
&state_out.basis,
&state_out.x_b,
&state_out.at_upper,
&state_out.is_basic,
&bsf.upper_bounds,
);
assert!(
(obj - exp).abs() < EPS,
"Timeout obj={obj:.6e} ≠ bounded_obj={exp:.6e}; delta={:.3e}",
(obj - exp).abs()
);
assert_eq!(
state_out.iterations, 0,
"expected 0 iterations (early-exit before loop), got {}",
state_out.iterations
);
}
other => panic!("expected Timeout with expired deadline, got {other:?}"),
}
}
}