use super::dual_common::outcome_to_result;
use super::pricing::{DualLeavingStrategy, MostInfeasibleLeaving};
use super::{build_bounded_standard_form_with_deadline, scale_upper_bounds, BoundedStandardForm};
use super::{extract_dual_info, extract_solution, SimplexOutcome, StandardForm};
use crate::basis::{BasisManager, LuBasis};
use crate::options::{DualPricing, SolverOptions, WarmStartBasis};
use crate::presolve::LpEquilibration;
use crate::problem::{ConstraintType, LpProblem, SolveStatus, SolverResult};
use crate::sparse::{CscMatrix, SparseVec};
use crate::tolerances::DROP_TOL;
use bounded_core::{
bounded_primal_phase1, bounded_primal_phase2_aug, bump_eq_ub_dispatch_count,
extract_dual_info_bounded, extract_solution_bounded, iterate as bounded_iterate,
phase2_primal_bounded, solve_bounded_dual, BoundedDualState, BoundedOutcome,
};
pub mod bound_flip;
mod bounded_core;
mod core;
mod phase1;
pub mod ratio_test;
mod steepest_edge;
fn deadline_expired(deadline: Option<std::time::Instant>) -> bool {
deadline.is_some_and(|d| std::time::Instant::now() >= d)
}
fn timeout_result() -> SolverResult {
SolverResult {
status: SolveStatus::Timeout,
objective: f64::INFINITY,
..Default::default()
}
}
fn bounded_obj_from_state(c: &[f64], ubs: &[f64], state: &BoundedDualState) -> f64 {
let basic: f64 = state
.basis
.iter()
.zip(state.x_b.iter())
.map(|(&j, &v)| c[j] * v)
.sum();
let at_ub: f64 = state
.at_upper
.iter()
.enumerate()
.filter(|&(j, &flag)| flag && !state.is_basic[j])
.map(|(j, _)| c[j] * ubs[j])
.sum();
basic + at_ub
}
enum BoundedTerminalReconcile {
Optimal(f64),
Timeout(f64),
BoundViolation,
MatrixAccessError,
SingularBasis,
}
fn reconcile_bounded_terminal_state(
a: &CscMatrix,
b: &[f64],
c: &[f64],
ubs: &[f64],
state: &mut BoundedDualState,
options: &SolverOptions,
) -> BoundedTerminalReconcile {
let mut rhs = b.to_vec();
for (j, &at_ub) in state.at_upper.iter().enumerate() {
if state.is_basic[j] || !at_ub {
continue;
}
let ub = ubs[j];
if !ub.is_finite() {
continue;
}
let Ok((rows, vals)) = a.get_column(j) else {
return BoundedTerminalReconcile::MatrixAccessError;
};
for (k, &row) in rows.iter().enumerate() {
rhs[row] -= vals[k] * ub;
}
}
let mut basis_mgr =
match LuBasis::new_timed(a, &state.basis, options.max_etas, options.deadline) {
Ok(bm) => bm,
Err(crate::error::SolverError::DeadlineExceeded) => {
return BoundedTerminalReconcile::Timeout(bounded_obj_from_state(c, ubs, state));
}
Err(_) => return BoundedTerminalReconcile::SingularBasis,
};
let mut x_b_sv = SparseVec::from_dense(&rhs);
basis_mgr.ftran(&mut x_b_sv);
state.x_b = x_b_sv.to_dense();
for v in state.x_b.iter_mut() {
if v.abs() < options.clamp_tol {
*v = 0.0;
}
}
for (i, &x_i) in state.x_b.iter().enumerate() {
if x_i < -options.primal_tol {
return BoundedTerminalReconcile::BoundViolation;
}
let ub = ubs[state.basis[i]];
if ub.is_finite() && x_i > ub + options.primal_tol {
return BoundedTerminalReconcile::BoundViolation;
}
}
let obj = bounded_obj_from_state(c, ubs, state);
BoundedTerminalReconcile::Optimal(obj)
}
fn make_leaving_strategy(pricing: DualPricing, m: usize) -> Box<dyn DualLeavingStrategy> {
match pricing {
DualPricing::MostInfeasible => Box::new(MostInfeasibleLeaving),
DualPricing::SteepestEdge => Box::new(steepest_edge::DualSteepestEdgeLeaving::new(m)),
}
}
fn warm_basis_is_dual_feasible(
a: &crate::sparse::CscMatrix,
c: &[f64],
basis_mgr: &mut LuBasis,
basis: &[usize],
is_basic: &[bool],
n_price: usize,
m: usize,
dual_tol: f64,
) -> bool {
let rc =
super::dual_common::compute_reduced_costs(a, c, basis_mgr, is_basic, n_price, m, basis);
rc.iter()
.enumerate()
.all(|(j, &r)| is_basic[j] || r >= -dual_tol)
}
pub(crate) fn solve_dual_advanced(
sf: &StandardForm,
problem: &LpProblem,
options: &SolverOptions,
) -> SolverResult {
if deadline_expired(options.deadline) {
return timeout_result();
}
if !bounded_dispatch_disabled() && problem.bounds.iter().any(|&(_, ub)| ub.is_finite()) {
let Some(bsf) = build_bounded_standard_form_with_deadline(problem, options.deadline) else {
return timeout_result();
};
if bsf.num_artificial == 0 {
if let Some(result) = try_bounded(&bsf, problem, options) {
return result;
}
} else {
let has_ge = problem
.constraint_types
.iter()
.any(|t| matches!(t, ConstraintType::Ge));
if !has_ge {
if let Some(result) = try_bounded_phase1_eq(&bsf, problem, options) {
return result;
}
}
}
}
let m = sf.m;
let Some((a, b, c, row_scale, col_scale)) =
LpEquilibration::scale_with_deadline(&sf.a, &sf.b, &sf.c, options.deadline)
else {
return timeout_result();
};
if let Some(warm) = &options.warm_start {
if warm.basis.len() == m && warm.basis.iter().all(|&idx| idx < sf.n_total) {
let mut basis = warm.basis.clone();
match LuBasis::new_timed(&a, &basis, options.max_etas, options.deadline) {
Ok(mut basis_mgr) => {
let mut x_b_sv = SparseVec::from_dense(&b);
basis_mgr.ftran(&mut x_b_sv);
let mut x_b = x_b_sv.to_dense();
let is_basic: Vec<bool> = {
let mut v = vec![false; sf.n_total];
for &j in &basis {
v[j] = true;
}
v
};
if !warm_basis_is_dual_feasible(
&a,
&c,
&mut basis_mgr,
&basis,
&is_basic,
sf.n_total,
m,
options.dual_tol,
) {
} else {
let mut leaving = make_leaving_strategy(options.dual_pricing, m);
let mut total_iters: usize = 0;
let outcome = core::dual_simplex_core_advanced(
&a,
&mut x_b,
&c,
&mut basis,
m,
sf.n_total,
sf.n_total,
false, options,
leaving.as_mut(),
&mut total_iters,
);
let mut result = outcome_to_result(
outcome, sf, problem, &basis, &x_b, &col_scale, &row_scale, true,
);
result.iterations = total_iters;
return result;
}
}
Err(_) => {
}
}
}
}
if sf.num_artificial == 0 {
return cold_start_advanced(sf, problem, options, &a, &b, &c, &row_scale, &col_scale);
}
let primal_result = super::dual::two_phase_dual_simplex(sf, problem, options);
match primal_result.status {
SolveStatus::Timeout if primal_result.solution.is_empty() => {
let bigm_result =
phase1::big_m_cold_start(sf, problem, options, &a, &b, &c, &row_scale, &col_scale);
if bigm_result.status == SolveStatus::Timeout {
let mut r = primal_result;
r.iterations = r.iterations.saturating_add(bigm_result.iterations);
r
} else {
bigm_result
}
}
SolveStatus::Infeasible if !primal_result.dual_solution.is_empty() => primal_result,
SolveStatus::Infeasible => {
let bigm_result =
phase1::big_m_cold_start(sf, problem, options, &a, &b, &c, &row_scale, &col_scale);
if bigm_result.status == SolveStatus::Timeout {
SolverResult {
status: SolveStatus::Timeout,
iterations: primal_result
.iterations
.saturating_add(bigm_result.iterations),
..primal_result
}
} else {
bigm_result
}
}
_ => primal_result,
}
}
#[cfg(test)]
thread_local! {
static BOUNDED_DISPATCH_DISABLE: std::cell::Cell<bool> =
const { std::cell::Cell::new(false) };
}
#[cfg(test)]
pub(crate) fn set_bounded_dispatch_disabled(v: bool) {
BOUNDED_DISPATCH_DISABLE.with(|c| c.set(v));
}
fn bounded_dispatch_disabled() -> bool {
#[cfg(test)]
{
BOUNDED_DISPATCH_DISABLE.with(|c| c.get())
}
#[cfg(not(test))]
{
false
}
}
fn try_bounded(
bsf: &BoundedStandardForm,
problem: &LpProblem,
options: &SolverOptions,
) -> Option<SolverResult> {
if deadline_expired(options.deadline) {
return Some(timeout_result());
}
let Some((a, b, c, row_scale, col_scale)) =
LpEquilibration::scale_with_deadline(&bsf.a, &bsf.b, &bsf.c, options.deadline)
else {
return Some(timeout_result());
};
let ubs = scale_upper_bounds(&bsf.upper_bounds, &col_scale);
let mut total_iters: usize;
if let Some(warm) = &options.warm_start {
if warm.basis.len() == bsf.m && warm.basis.iter().all(|&idx| idx < bsf.n_total) {
if let Ok(mut basis_mgr) =
LuBasis::new_timed(&a, &warm.basis, options.max_etas, options.deadline)
{
let mut x_b_sv = SparseVec::from_dense(&b);
basis_mgr.ftran(&mut x_b_sv);
let x_b = x_b_sv.to_dense();
let has_lb_violation = super::has_lb_violation(&x_b, options.primal_tol);
let is_basic_bounded: Vec<bool> = {
let mut v = vec![false; bsf.n_total];
for &j in &warm.basis {
v[j] = true;
}
v
};
if !has_lb_violation
&& warm_basis_is_dual_feasible(
&a,
&c,
&mut basis_mgr,
&warm.basis,
&is_basic_bounded,
bsf.n_total,
bsf.m,
options.dual_tol,
)
{
let state = BoundedDualState {
basis: warm.basis.clone(),
at_upper: vec![false; bsf.n_total],
x_b,
reduced_costs: vec![0.0; bsf.n_total],
is_basic: is_basic_bounded,
iterations: 0,
};
let mut leaving = make_leaving_strategy(options.dual_pricing, bsf.m);
let (dual_out, dual_state) =
bounded_iterate(state, bsf, &a, &c, options, &ubs, leaving.as_mut());
total_iters = dual_state.iterations;
let result = finish_bounded(
dual_out,
dual_state,
bsf,
&a,
&b,
&c,
&row_scale,
&col_scale,
&ubs,
problem,
options,
&mut total_iters,
);
if result.is_some() {
return result;
}
} }
}
}
let mut leaving = make_leaving_strategy(options.dual_pricing, bsf.m);
let (dual_out, dual_state) =
solve_bounded_dual(bsf, &a, &b, &c, options, &ubs, leaving.as_mut());
total_iters = dual_state.iterations;
finish_bounded(
dual_out,
dual_state,
bsf,
&a,
&b,
&c,
&row_scale,
&col_scale,
&ubs,
problem,
options,
&mut total_iters,
)
}
fn build_a_aug_for_eq(
bsf: &BoundedStandardForm,
a_scaled: &CscMatrix,
needs_artificial: &[bool],
) -> (CscMatrix, Vec<Option<usize>>, usize) {
let m = bsf.m;
let n_total = bsf.n_total;
let mut art_col_of_row: Vec<Option<usize>> = vec![None; m];
let mut n_art = 0usize;
for i in 0..m {
if needs_artificial[i] {
art_col_of_row[i] = Some(n_total + n_art);
n_art += 1;
}
}
let n_aug = n_total + n_art;
let mut trip_rows: Vec<usize> = Vec::with_capacity(a_scaled.nnz() + n_art);
let mut trip_cols: Vec<usize> = Vec::with_capacity(a_scaled.nnz() + n_art);
let mut trip_vals: Vec<f64> = Vec::with_capacity(a_scaled.nnz() + n_art);
for j in 0..n_total {
let (rows, vals) = a_scaled.get_column(j).unwrap();
for (k, &row) in rows.iter().enumerate() {
let v = vals[k];
if v.abs() > DROP_TOL {
trip_rows.push(row);
trip_cols.push(j);
trip_vals.push(v);
}
}
}
for (i, col_opt) in art_col_of_row.iter().enumerate() {
if let Some(col) = col_opt {
trip_rows.push(i);
trip_cols.push(*col);
trip_vals.push(1.0);
}
}
let a_aug = CscMatrix::from_triplets(&trip_rows, &trip_cols, &trip_vals, m, n_aug)
.expect("augmented matrix construction must succeed (deduplicated by build)");
(a_aug, art_col_of_row, n_art)
}
fn diag_basis_initial_x_b(a_aug: &CscMatrix, basis: &[usize], b: &[f64]) -> Vec<f64> {
let m = basis.len();
let mut x_b = vec![0.0f64; m];
for i in 0..m {
let (rows, vals) = a_aug.get_column(basis[i]).unwrap();
let mut diag = 0.0f64;
for (k, &r) in rows.iter().enumerate() {
if r == i {
diag = vals[k];
break;
}
}
debug_assert!(
diag != 0.0,
"slack/artificial starting basis must be diagonal (nonzero pivot at its own row)"
);
let v = b[i] / diag;
x_b[i] = if v < 0.0 { 0.0 } else { v };
}
x_b
}
fn try_bounded_phase1_eq(
bsf: &BoundedStandardForm,
problem: &LpProblem,
options: &SolverOptions,
) -> Option<SolverResult> {
if deadline_expired(options.deadline) {
return Some(timeout_result());
}
let Some((a, b, c, row_scale, col_scale)) =
LpEquilibration::scale_with_deadline(&bsf.a, &bsf.b, &bsf.c, options.deadline)
else {
return Some(timeout_result());
};
let ubs = scale_upper_bounds(&bsf.upper_bounds, &col_scale);
bump_eq_ub_dispatch_count();
let identity_state = || {
let (a_aug, art_col_of_row, n_art) = build_a_aug_for_eq(bsf, &a, &bsf.needs_artificial);
let n_aug = bsf.n_total + n_art;
let mut ubs_aug = vec![f64::INFINITY; n_aug];
ubs_aug[..bsf.n_total].copy_from_slice(&ubs);
let mut basis: Vec<usize> = bsf.initial_basis.clone();
for (i, col_opt) in art_col_of_row.iter().enumerate() {
if let Some(col) = col_opt {
basis[i] = *col;
}
}
let mut is_basic = vec![false; n_aug];
for &j in &basis {
is_basic[j] = true;
}
let x_b = diag_basis_initial_x_b(&a_aug, &basis, &b);
(a_aug, art_col_of_row, ubs_aug, basis, is_basic, x_b)
};
let (a_aug, art_col_of_row, ubs_aug, basis, is_basic, x_b) = if options.use_lp_crash_basis {
let (basis_pre, needs_artificial, _n_art_post) = super::crash::compute_crash_basis(
&a,
&b,
bsf.m,
bsf.n_shifted,
&bsf.initial_basis,
&bsf.needs_artificial,
);
let (a_aug, art_col_of_row, n_art) = build_a_aug_for_eq(bsf, &a, &needs_artificial);
let n_aug = bsf.n_total + n_art;
let mut ubs_aug = vec![f64::INFINITY; n_aug];
ubs_aug[..bsf.n_total].copy_from_slice(&ubs);
let mut basis: Vec<usize> = basis_pre;
for (i, col_opt) in art_col_of_row.iter().enumerate() {
if let Some(col) = col_opt {
basis[i] = *col;
}
}
let mut is_basic = vec![false; n_aug];
for &j in &basis {
is_basic[j] = true;
}
let needs_ftran = needs_artificial
.iter()
.zip(bsf.needs_artificial.iter())
.any(|(post, orig)| post != orig);
let x_b = if needs_ftran {
match LuBasis::new_timed(&a_aug, &basis, options.max_etas, options.deadline) {
Ok(mut bm) => {
let mut sv = SparseVec::from_dense(&b);
bm.ftran(&mut sv);
let xb = sv.to_dense();
let infeasible = xb.iter().enumerate().any(|(i, &v)| {
if v < -options.primal_tol {
return true;
}
let ub_i = ubs_aug[basis[i]];
ub_i.is_finite() && v > ub_i + options.primal_tol
});
if infeasible {
return None;
}
xb
}
Err(_) => {
return run_phase1_then_phase2(
bsf,
problem,
options,
identity_state,
&b,
&c,
&row_scale,
&col_scale,
);
}
}
} else {
diag_basis_initial_x_b(&a_aug, &basis, &b)
};
(a_aug, art_col_of_row, ubs_aug, basis, is_basic, x_b)
} else {
identity_state()
};
run_phase1_then_phase2(
bsf,
problem,
options,
move || (a_aug, art_col_of_row, ubs_aug, basis, is_basic, x_b),
&b,
&c,
&row_scale,
&col_scale,
)
}
#[allow(clippy::too_many_arguments)]
fn run_phase1_then_phase2<F>(
bsf: &BoundedStandardForm,
problem: &LpProblem,
options: &SolverOptions,
state_factory: F,
b: &[f64],
c: &[f64],
row_scale: &[f64],
col_scale: &[f64],
) -> Option<SolverResult>
where
F: FnOnce() -> (
CscMatrix,
Vec<Option<usize>>,
Vec<f64>,
Vec<usize>,
Vec<bool>,
Vec<f64>,
),
{
fn mark_eq_ub_path(mut r: SolverResult) -> SolverResult {
r.stats.bounded_eq_ub_path = true;
r
}
let (a_aug, art_col_of_row, mut ubs_aug, basis, is_basic, x_b) = state_factory();
let n_aug = a_aug.ncols;
let mut state = BoundedDualState {
basis,
at_upper: vec![false; n_aug],
x_b,
reduced_costs: vec![0.0; n_aug],
is_basic,
iterations: 0,
};
let mut c_p1 = vec![0.0f64; n_aug];
for col in art_col_of_row.iter().flatten() {
c_p1[*col] = 1.0;
}
let mut iters: usize = 0;
let p1_out = bounded_primal_phase1(
&a_aug,
&c_p1,
&ubs_aug,
bsf.n_total,
&mut state,
options,
&mut iters,
);
match p1_out {
SimplexOutcome::SingularBasis => {
return Some(mark_eq_ub_path(SolverResult::numerical_error()));
}
SimplexOutcome::Unbounded => {
return None;
}
SimplexOutcome::Timeout(_) => {
let solution = extract_solution_bounded(bsf, &state, col_scale);
return Some(mark_eq_ub_path(SolverResult {
status: SolveStatus::Timeout,
objective: bsf.obj_offset,
solution,
iterations: iters,
..Default::default()
}));
}
SimplexOutcome::Optimal(_, _) => {
let art_sum = match reconcile_bounded_terminal_state(
&a_aug, b, &c_p1, &ubs_aug, &mut state, options,
) {
BoundedTerminalReconcile::Optimal(obj) => obj,
BoundedTerminalReconcile::Timeout(_) => {
let solution = extract_solution_bounded(bsf, &state, col_scale);
return Some(mark_eq_ub_path(SolverResult {
status: SolveStatus::Timeout,
objective: bsf.obj_offset,
solution,
iterations: iters,
..Default::default()
}));
}
BoundedTerminalReconcile::BoundViolation => return None,
BoundedTerminalReconcile::MatrixAccessError
| BoundedTerminalReconcile::SingularBasis => {
return Some(mark_eq_ub_path(SolverResult::numerical_error()));
}
};
if art_sum > options.primal_tol {
let mut r = SolverResult::infeasible();
r.iterations = iters;
return Some(mark_eq_ub_path(r));
}
}
}
for col in art_col_of_row.iter().flatten() {
ubs_aug[*col] = 0.0;
}
let mut c_p2 = vec![0.0f64; n_aug];
c_p2[..bsf.n_total].copy_from_slice(c);
let p2_out = bounded_primal_phase2_aug(
&a_aug,
&c_p2,
&ubs_aug,
bsf.n_total,
&mut state,
options,
&mut iters,
);
match p2_out {
SimplexOutcome::Optimal(_, y) => {
let pre_reconcile_x_b = state.x_b.clone();
let obj = match reconcile_bounded_terminal_state(
&a_aug, b, &c_p2, &ubs_aug, &mut state, options,
) {
BoundedTerminalReconcile::Optimal(obj) => obj,
BoundedTerminalReconcile::Timeout(obj) => {
let solution = extract_solution_bounded(bsf, &state, col_scale);
return Some(mark_eq_ub_path(SolverResult {
status: SolveStatus::Timeout,
objective: obj + bsf.obj_offset,
solution,
iterations: iters,
..Default::default()
}));
}
BoundedTerminalReconcile::BoundViolation => {
state.x_b = pre_reconcile_x_b;
let obj = bounded_obj_from_state(&c_p2, &ubs_aug, &state);
let solution = extract_solution_bounded(bsf, &state, col_scale);
return Some(mark_eq_ub_path(SolverResult {
status: SolveStatus::Timeout,
objective: obj + bsf.obj_offset,
solution,
iterations: iters,
..Default::default()
}));
}
BoundedTerminalReconcile::MatrixAccessError
| BoundedTerminalReconcile::SingularBasis => {
return Some(mark_eq_ub_path(SolverResult::numerical_error()));
}
};
let solution = extract_solution_bounded(bsf, &state, col_scale);
let (dual_solution, reduced_costs, slack) =
extract_dual_info_bounded(bsf, problem, &y, &solution, row_scale);
let ws = if state.basis.iter().all(|&j| j < bsf.n_total) {
Some(WarmStartBasis {
basis: state.basis.clone(),
x_b: state.x_b.clone(),
})
} else {
None
};
Some(mark_eq_ub_path(SolverResult {
status: SolveStatus::Optimal,
objective: obj + bsf.obj_offset,
solution,
dual_solution,
reduced_costs,
slack,
warm_start_basis: ws,
iterations: iters,
..Default::default()
}))
}
SimplexOutcome::Unbounded => Some(mark_eq_ub_path(SolverResult {
status: SolveStatus::Unbounded,
objective: f64::NEG_INFINITY,
iterations: iters,
..Default::default()
})),
SimplexOutcome::Timeout(obj) => {
let solution = extract_solution_bounded(bsf, &state, col_scale);
Some(mark_eq_ub_path(SolverResult {
status: SolveStatus::Timeout,
objective: obj + bsf.obj_offset,
solution,
iterations: iters,
..Default::default()
}))
}
SimplexOutcome::SingularBasis => Some(mark_eq_ub_path(SolverResult::numerical_error())),
}
}
#[allow(clippy::too_many_arguments)]
fn finish_bounded(
dual_out: BoundedOutcome,
dual_state: BoundedDualState,
bsf: &BoundedStandardForm,
a: &crate::sparse::CscMatrix,
b: &[f64],
c: &[f64],
row_scale: &[f64],
col_scale: &[f64],
ubs: &[f64],
problem: &LpProblem,
options: &SolverOptions,
total_iters: &mut usize,
) -> Option<SolverResult> {
match dual_out {
BoundedOutcome::UbViolationOutOfScope { .. } => None,
BoundedOutcome::Unbounded => Some(SolverResult {
status: SolveStatus::Infeasible,
objective: f64::INFINITY,
solution: vec![],
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
..Default::default()
}),
BoundedOutcome::Timeout(obj) => {
let solution = extract_solution_bounded(bsf, &dual_state, col_scale);
Some(SolverResult {
status: SolveStatus::Timeout,
objective: obj + bsf.obj_offset,
solution,
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
iterations: *total_iters,
..Default::default()
})
}
BoundedOutcome::SingularBasis => Some(SolverResult::numerical_error()),
BoundedOutcome::Optimal(_, _) => {
let (p2_out, mut p2_state) =
phase2_primal_bounded(bsf, dual_state, a, c, options, total_iters, ubs);
let p2_out = match p2_out {
SimplexOutcome::Optimal(_, y) => {
let pre_reconcile_x_b = p2_state.x_b.clone();
match reconcile_bounded_terminal_state(a, b, c, ubs, &mut p2_state, options) {
BoundedTerminalReconcile::Optimal(obj) => SimplexOutcome::Optimal(obj, y),
BoundedTerminalReconcile::Timeout(obj) => SimplexOutcome::Timeout(obj),
BoundedTerminalReconcile::BoundViolation => {
p2_state.x_b = pre_reconcile_x_b;
let obj = bounded_obj_from_state(c, ubs, &p2_state);
SimplexOutcome::Timeout(obj)
}
BoundedTerminalReconcile::MatrixAccessError
| BoundedTerminalReconcile::SingularBasis => SimplexOutcome::SingularBasis,
}
}
other => other,
};
Some(finish_bounded_phase2(
p2_out,
p2_state,
bsf,
col_scale,
row_scale,
problem,
*total_iters,
))
}
}
}
fn finish_bounded_phase2(
out: SimplexOutcome,
state: BoundedDualState,
bsf: &BoundedStandardForm,
col_scale: &[f64],
row_scale: &[f64],
problem: &LpProblem,
total_iters: usize,
) -> SolverResult {
match out {
SimplexOutcome::Optimal(obj, y) => {
let solution = extract_solution_bounded(bsf, &state, col_scale);
let (dual_solution, reduced_costs, slack) =
extract_dual_info_bounded(bsf, problem, &y, &solution, row_scale);
let ws = WarmStartBasis {
basis: state.basis,
x_b: state.x_b,
};
SolverResult {
status: SolveStatus::Optimal,
objective: obj + bsf.obj_offset,
solution,
dual_solution,
reduced_costs,
slack,
warm_start_basis: Some(ws),
iterations: total_iters,
..Default::default()
}
}
SimplexOutcome::Unbounded => SolverResult {
status: SolveStatus::Unbounded,
objective: f64::NEG_INFINITY,
solution: vec![],
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
..Default::default()
},
SimplexOutcome::Timeout(obj) => {
let solution = extract_solution_bounded(bsf, &state, col_scale);
SolverResult {
status: SolveStatus::Timeout,
objective: obj + bsf.obj_offset,
solution,
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
iterations: total_iters,
..Default::default()
}
}
SimplexOutcome::SingularBasis => SolverResult::numerical_error(),
}
}
#[allow(clippy::too_many_arguments)]
fn cold_start_advanced(
sf: &StandardForm,
problem: &LpProblem,
options: &SolverOptions,
a: &crate::sparse::CscMatrix,
b: &[f64],
c: &[f64],
row_scale: &[f64],
col_scale: &[f64],
) -> SolverResult {
let m = sf.m;
let mut basis = sf.initial_basis.clone();
let mut x_b = b.to_vec();
let c_perturbed: Vec<f64> = c.iter().map(|&v| v.max(0.0)).collect();
let mut leaving = make_leaving_strategy(options.dual_pricing, m);
let mut total_iters: usize = 0;
let phase1_outcome = core::dual_simplex_core_advanced(
a,
&mut x_b,
&c_perturbed,
&mut basis,
m,
sf.n_total,
sf.n_total,
false, options,
leaving.as_mut(),
&mut total_iters,
);
match phase1_outcome {
SimplexOutcome::Unbounded => {
return SolverResult {
status: SolveStatus::Infeasible,
objective: f64::INFINITY,
solution: vec![],
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
..Default::default()
};
}
SimplexOutcome::Timeout(_) => {
return super::timeout_result_with_incumbent(
sf,
problem,
&basis,
&x_b,
col_scale,
total_iters,
);
}
SimplexOutcome::SingularBasis => {
return SolverResult::numerical_error();
}
SimplexOutcome::Optimal(_, _) => {
}
}
use super::pricing::SteepestEdgePricing;
let mut pricing = SteepestEdgePricing::new(sf.n_total);
let phase2_outcome = super::revised_simplex_core(
a,
&mut x_b,
c,
b,
&mut basis,
m,
sf.n_total,
sf.n_total,
&mut pricing,
options,
&mut total_iters,
false,
);
let mut result = match phase2_outcome {
SimplexOutcome::Optimal(obj, y) => {
let solution = extract_solution(sf, &basis, &x_b, col_scale);
let (dual_solution, reduced_costs, slack) =
extract_dual_info(sf, problem, &y, &solution, row_scale);
let ws = WarmStartBasis {
basis: basis.to_vec(),
x_b: x_b.to_vec(),
};
SolverResult {
status: SolveStatus::Optimal,
objective: obj + sf.obj_offset,
solution,
dual_solution,
reduced_costs,
slack,
warm_start_basis: Some(ws),
iterations: total_iters,
..Default::default()
}
}
SimplexOutcome::Unbounded => SolverResult {
status: SolveStatus::Unbounded,
objective: f64::NEG_INFINITY,
solution: vec![],
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
..Default::default()
},
SimplexOutcome::Timeout(obj) => {
let solution = extract_solution(sf, &basis, &x_b, col_scale);
SolverResult {
status: SolveStatus::Timeout,
objective: obj + sf.obj_offset,
solution,
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
iterations: total_iters,
..Default::default()
}
}
SimplexOutcome::SingularBasis => SolverResult::numerical_error(),
};
result.iterations = total_iters;
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::options::{SolverOptions, WarmStartBasis};
use crate::problem::{ConstraintType, LpProblem, SolveStatus};
use crate::simplex::dual_advanced::bound_flip::{
bfrt_flip_invocations, reset_bfrt_flip_invocations,
};
use crate::simplex::standard_form::build_standard_form;
fn lp_2x2_boxed() -> LpProblem {
use crate::sparse::CscMatrix;
let a =
CscMatrix::from_triplets(&[0, 1, 0, 1], &[0, 0, 1, 1], &[1.0, 1.0, 1.0, -1.0], 2, 2)
.unwrap();
LpProblem::new_general(
vec![-1.0, -1.0],
a,
vec![6.0, 2.0],
vec![ConstraintType::Le, ConstraintType::Le],
vec![(0.0, 4.0), (0.0, 4.0)],
None,
)
.unwrap()
}
fn lp_flip_trigger() -> LpProblem {
use crate::sparse::CscMatrix;
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
LpProblem::new_general(
vec![-1.0, -3.0],
a,
vec![5.0],
vec![ConstraintType::Le],
vec![(0.0, 4.0), (0.0, 2.0)],
None,
)
.unwrap()
}
fn lp_no_ub() -> LpProblem {
use crate::sparse::CscMatrix;
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
LpProblem::new_general(
vec![1.0, 2.0],
a,
vec![5.0],
vec![ConstraintType::Le],
vec![(0.0, f64::INFINITY), (0.0, f64::INFINITY)],
None,
)
.unwrap()
}
#[test]
fn bound_violation_timeout_objective_uses_restored_incumbent() {
use crate::sparse::CscMatrix;
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
let b = vec![1.0];
let c = vec![10.0, 1.0];
let ubs = vec![3.0, 10.0];
let mut state = BoundedDualState {
basis: vec![1],
at_upper: vec![true, false],
x_b: vec![0.5],
reduced_costs: vec![0.0, 0.0],
is_basic: vec![false, true],
iterations: 0,
};
let pre_reconcile_x_b = state.x_b.clone();
assert!(matches!(
reconcile_bounded_terminal_state(
&a,
&b,
&c,
&ubs,
&mut state,
&SolverOptions::default()
),
BoundedTerminalReconcile::BoundViolation
));
let invalid_obj = bounded_obj_from_state(&c, &ubs, &state);
state.x_b = pre_reconcile_x_b;
let timeout_obj = bounded_obj_from_state(&c, &ubs, &state);
let restored_solution = [ubs[0], state.x_b[0]];
let recomputed_obj: f64 = c
.iter()
.zip(restored_solution.iter())
.map(|(&c_j, &x_j)| c_j * x_j)
.sum();
assert_ne!(invalid_obj, timeout_obj);
assert_eq!(timeout_obj, recomputed_obj);
}
#[test]
fn bfrt_wiring_flip_count_positive() {
let lp = lp_flip_trigger();
let sf = build_standard_form(&lp);
reset_bfrt_flip_invocations();
let result = solve_dual_advanced(&sf, &lp, &SolverOptions::default());
let flips = bfrt_flip_invocations();
assert_eq!(
result.status,
SolveStatus::Optimal,
"expected Optimal, got {:?}",
result.status
);
assert!(
(result.objective - (-9.0)).abs() < 1e-5,
"expected obj=-9, got {:.6e}",
result.objective
);
assert!(
flips > 0,
"bfrt_wiring_flip_count_positive: flip count = 0, bounded path not exercised"
);
}
#[test]
fn bfrt_wiring_flip_count_positive_noop_proof() {
let lp = lp_flip_trigger();
let sf = build_standard_form(&lp);
let _guard = crate::ScopedDisable::new(
|| set_bounded_dispatch_disabled(true),
|| set_bounded_dispatch_disabled(false),
);
reset_bfrt_flip_invocations();
let result = solve_dual_advanced(&sf, &lp, &SolverOptions::default());
let flips_disabled = bfrt_flip_invocations();
assert_eq!(
flips_disabled, 0,
"noop proof: expected 0 flips with bounded dispatch disabled, got {flips_disabled}"
);
assert_eq!(result.status, SolveStatus::Optimal);
}
#[test]
fn bfrt_wiring_multi_pattern_correct() {
{
let lp = lp_2x2_boxed();
let sf = build_standard_form(&lp);
let r = solve_dual_advanced(&sf, &lp, &SolverOptions::default());
assert_eq!(r.status, SolveStatus::Optimal, "pattern 1 status");
assert!(
(r.objective - (-6.0)).abs() < 1e-5,
"pattern 1 obj={}",
r.objective
);
}
{
let lp = lp_flip_trigger();
let sf = build_standard_form(&lp);
reset_bfrt_flip_invocations();
let r = solve_dual_advanced(&sf, &lp, &SolverOptions::default());
let flips = bfrt_flip_invocations();
assert_eq!(r.status, SolveStatus::Optimal, "pattern 2 status");
assert!(
(r.objective - (-9.0)).abs() < 1e-5,
"pattern 2 obj={}",
r.objective
);
assert!(
flips > 0,
"pattern 2: flip count = 0, bounded path not exercised"
);
}
{
let lp = lp_no_ub();
let sf = build_standard_form(&lp);
let r = solve_dual_advanced(&sf, &lp, &SolverOptions::default());
assert_eq!(r.status, SolveStatus::Optimal, "pattern 3 status");
}
}
#[test]
fn legacy_warm_start_lb_violation_repairs_and_converges() {
use crate::sparse::CscMatrix;
const OBJ_TOL: f64 = 1e-5;
let make_lp = |b: Vec<f64>| {
LpProblem::new_general(
vec![-3.0, -1.0],
CscMatrix::from_triplets(&[0, 0, 1, 2], &[0, 1, 0, 1], &[1.0, 1.0, 1.0, 1.0], 3, 2)
.unwrap(),
b,
vec![ConstraintType::Le, ConstraintType::Le, ConstraintType::Le],
vec![(0.0, f64::INFINITY), (0.0, f64::INFINITY)],
None,
)
.unwrap()
};
let lp_orig = make_lp(vec![4.0, 3.0, 2.0]);
let sf_orig = build_standard_form(&lp_orig);
let r_cold = solve_dual_advanced(&sf_orig, &lp_orig, &SolverOptions::default());
assert_eq!(
r_cold.status,
SolveStatus::Optimal,
"cold: {:?}",
r_cold.status
);
assert!(
(r_cold.objective - (-10.0)).abs() < OBJ_TOL,
"cold obj={:.6e} expected -10",
r_cold.objective
);
let warm = r_cold
.warm_start_basis
.expect("cold solve must return warm_start_basis");
let lp_p = make_lp(vec![1.0, 3.0, 2.0]);
let sf_p = build_standard_form(&lp_p);
let r_warm = solve_dual_advanced(
&sf_p,
&lp_p,
&SolverOptions {
warm_start: Some(warm),
..SolverOptions::default()
},
);
assert_eq!(
r_warm.status,
SolveStatus::Optimal,
"warm re-solve: {:?} — guard still present?",
r_warm.status
);
assert!(
(r_warm.objective - (-3.0)).abs() < OBJ_TOL,
"warm re-solve obj={:.6e} expected -3",
r_warm.objective
);
let r_cold_p = solve_dual_advanced(&sf_p, &lp_p, &SolverOptions::default());
assert_eq!(r_cold_p.status, SolveStatus::Optimal);
assert!(
(r_cold_p.objective - r_warm.objective).abs() < OBJ_TOL,
"warm {:.6e} != cold {:.6e}",
r_warm.objective,
r_cold_p.objective
);
}
#[test]
fn bfrt_wiring_warm_start_reuse() {
let lp = lp_flip_trigger();
let sf = build_standard_form(&lp);
reset_bfrt_flip_invocations();
let r1 = solve_dual_advanced(&sf, &lp, &SolverOptions::default());
let flips = bfrt_flip_invocations();
assert_eq!(r1.status, SolveStatus::Optimal);
assert!(
flips > 0,
"warm_start_reuse cold solve: flip count = 0, bounded path not exercised"
);
let ws = r1
.warm_start_basis
.expect("bounded path must return warm_start_basis");
let r2 = solve_dual_advanced(
&sf,
&lp,
&SolverOptions {
warm_start: Some(ws),
..SolverOptions::default()
},
);
assert_eq!(
r2.status,
SolveStatus::Optimal,
"warm restart: {:?}",
r2.status
);
assert!(
(r2.objective - r1.objective).abs() < 1e-5,
"warm restart obj drift: {} vs {}",
r2.objective,
r1.objective
);
}
#[test]
fn warm_start_dual_infeasible_cost_change_falls_through_to_cold_start() {
use crate::sparse::CscMatrix;
const OBJ_TOL: f64 = 1e-6;
let make_lp = |c: Vec<f64>| {
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
LpProblem::new_general(
c,
a,
vec![3.0],
vec![ConstraintType::Le],
vec![(0.0, f64::INFINITY), (0.0, f64::INFINITY)],
None,
)
.unwrap()
};
let lp1 = make_lp(vec![1.0, 1.0]);
let sf1 = build_standard_form(&lp1);
let r1 = solve_dual_advanced(&sf1, &lp1, &SolverOptions::default());
assert_eq!(
r1.status,
SolveStatus::Optimal,
"LP1 cold solve: {:?}",
r1.status
);
assert!(
r1.objective.abs() < OBJ_TOL,
"LP1 obj={:.6e} expected 0",
r1.objective
);
let ws = r1
.warm_start_basis
.expect("LP1 must return warm_start_basis");
let lp2 = make_lp(vec![-1.0, -1.0]);
let sf2 = build_standard_form(&lp2);
let r2 = solve_dual_advanced(
&sf2,
&lp2,
&SolverOptions {
warm_start: Some(ws),
..SolverOptions::default()
},
);
assert_eq!(
r2.status,
SolveStatus::Optimal,
"LP2 warm-solve status: {:?} (expected Optimal)",
r2.status
);
assert!(
(r2.objective - (-3.0)).abs() < OBJ_TOL,
"LP2 warm-solve obj={:.6e} expected -3 (got 0 = guard missing)",
r2.objective
);
let r2_cold = solve_dual_advanced(&sf2, &lp2, &SolverOptions::default());
assert_eq!(r2_cold.status, SolveStatus::Optimal);
assert!(
(r2_cold.objective - r2.objective).abs() < OBJ_TOL,
"cold {:.6e} != warm {:.6e}",
r2_cold.objective,
r2.objective
);
}
#[test]
fn ge_eq_cold_start_primal_first_dispatch_solves_optimally() {
use crate::sparse::CscMatrix;
const OBJ_TOL: f64 = 1e-6;
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
let lp = LpProblem::new_general(
vec![1.0, 1.0],
a,
vec![3.0],
vec![ConstraintType::Eq],
vec![(0.0, f64::INFINITY); 2],
None,
)
.unwrap();
let sf = build_standard_form(&lp);
let result = solve_dual_advanced(&sf, &lp, &SolverOptions::default());
assert_eq!(result.status, SolveStatus::Optimal);
assert!(
(result.objective - 3.0).abs() < OBJ_TOL,
"Ge/Eq LP should have obj=3.0, got {:.6e}",
result.objective
);
}
#[test]
fn warm_basis_from_bounded_dispatch_does_not_mask_farkas_infeasibility() {
use crate::sparse::CscMatrix;
const OBJ_TOL: f64 = 1e-6;
let make_lp = |b_rhs: f64| {
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
LpProblem::new_general(
vec![-1.0, -3.0],
a,
vec![b_rhs],
vec![ConstraintType::Le],
vec![(0.0, 4.0), (0.0, 2.0)],
None,
)
.unwrap()
};
let lp1 = make_lp(5.0);
let sf1 = build_standard_form(&lp1);
let r1 = solve_dual_advanced(&sf1, &lp1, &SolverOptions::default());
assert_eq!(r1.status, SolveStatus::Optimal, "LP1 cold: {:?}", r1.status);
assert!(
(r1.objective - (-9.0)).abs() < OBJ_TOL,
"LP1 obj={:.6e} expected -9",
r1.objective
);
let warm = r1
.warm_start_basis
.expect("bounded cold solve must return warm_start_basis");
let lp2 = make_lp(-1.0);
let sf2 = build_standard_form(&lp2);
let r2 = solve_dual_advanced(
&sf2,
&lp2,
&SolverOptions {
warm_start: Some(warm),
..SolverOptions::default()
},
);
assert_eq!(
r2.status,
SolveStatus::Infeasible,
"LP2 (x0+x1 ≤ -1, finite UBs) must be Infeasible; got {:?}",
r2.status
);
let r2_cold = solve_dual_advanced(&sf2, &lp2, &SolverOptions::default());
assert_eq!(
r2_cold.status,
SolveStatus::Infeasible,
"LP2 cold: expected Infeasible, got {:?}",
r2_cold.status
);
}
#[test]
fn warm_start_with_expired_deadline_returns_timeout() {
let lp = lp_2x2_boxed();
let sf = build_standard_form(&lp);
let options = SolverOptions {
warm_start: Some(WarmStartBasis {
basis: sf.initial_basis.clone(),
x_b: vec![],
}),
deadline: Some(std::time::Instant::now() - std::time::Duration::from_millis(1)),
..SolverOptions::default()
};
let result = solve_dual_advanced(&sf, &lp, &options);
assert_eq!(result.status, SolveStatus::Timeout);
}
fn lp_eq_with_finite_ubs() -> LpProblem {
use crate::sparse::CscMatrix;
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
LpProblem::new_general(
vec![1.0, 1.0],
a,
vec![3.0],
vec![ConstraintType::Eq],
vec![(0.0, 2.0), (0.0, 2.0)],
None,
)
.unwrap()
}
#[test]
fn eq_ub_dispatch_count_positive() {
let lp = lp_eq_with_finite_ubs();
let sf = build_standard_form(&lp);
bounded_core::reset_eq_ub_dispatch_count();
let result = solve_dual_advanced(&sf, &lp, &SolverOptions::default());
assert_eq!(result.status, SolveStatus::Optimal, "expected Optimal");
assert!(
(result.objective - 3.0).abs() < 1e-6,
"expected obj=3, got {:.6e}",
result.objective
);
let count = bounded_core::eq_ub_dispatch_count();
assert!(
count > 0,
"eq_ub_dispatch_count_positive: counter = 0, new path not exercised"
);
}
#[test]
fn eq_ub_dispatch_noop_proof() {
let lp = lp_eq_with_finite_ubs();
let sf = build_standard_form(&lp);
let _guard = crate::ScopedDisable::new(
|| set_bounded_dispatch_disabled(true),
|| set_bounded_dispatch_disabled(false),
);
bounded_core::reset_eq_ub_dispatch_count();
let result = solve_dual_advanced(&sf, &lp, &SolverOptions::default());
let count = bounded_core::eq_ub_dispatch_count();
assert_eq!(
count, 0,
"noop proof: expected 0 dispatches with bounded disabled, got {count}"
);
assert_eq!(result.status, SolveStatus::Optimal);
assert!((result.objective - 3.0).abs() < 1e-6);
}
#[test]
fn eq_ub_phase1_solution_recovers_at_upper_variable() {
let lp = lp_eq_with_finite_ubs();
let sf = build_standard_form(&lp);
let result = solve_dual_advanced(&sf, &lp, &SolverOptions::default());
assert_eq!(result.status, SolveStatus::Optimal);
assert_eq!(result.solution.len(), 2);
assert!(
(result.solution[0] - 2.0).abs() < 1e-6,
"expected x0=2, got {:.6e}",
result.solution[0]
);
assert!(
(result.solution[1] - 1.0).abs() < 1e-6,
"expected x1=1, got {:.6e}",
result.solution[1]
);
}
#[test]
fn eq_ub_phase1_detects_infeasibility() {
use crate::sparse::CscMatrix;
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
let lp = LpProblem::new_general(
vec![1.0, 1.0],
a,
vec![10.0],
vec![ConstraintType::Eq],
vec![(0.0, 2.0), (0.0, 2.0)],
None,
)
.unwrap();
let sf = build_standard_form(&lp);
let result = solve_dual_advanced(&sf, &lp, &SolverOptions::default());
assert_eq!(
result.status,
SolveStatus::Infeasible,
"expected Infeasible, got {:?}",
result.status
);
}
#[test]
fn eq_ub_phase1_multi_pattern_correct() {
use crate::sparse::CscMatrix;
{
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
let lp = LpProblem::new_general(
vec![1.0, 1.0],
a,
vec![5.0],
vec![ConstraintType::Eq],
vec![(0.0, 3.0), (0.0, f64::INFINITY)],
None,
)
.unwrap();
let sf = build_standard_form(&lp);
let r = solve_dual_advanced(&sf, &lp, &SolverOptions::default());
assert_eq!(r.status, SolveStatus::Optimal, "pattern A");
assert!(
(r.objective - 5.0).abs() < 1e-6,
"pattern A obj={}",
r.objective
);
}
{
let a =
CscMatrix::from_triplets(&[0, 1, 0, 1], &[0, 0, 1, 1], &[1.0, 1.0, 1.0, 1.0], 2, 2)
.unwrap();
let lp = LpProblem::new_general(
vec![-1.0, -1.0],
a,
vec![5.0, 4.0],
vec![ConstraintType::Le, ConstraintType::Eq],
vec![(0.0, 3.0), (0.0, 3.0)],
None,
)
.unwrap();
let sf = build_standard_form(&lp);
let r = solve_dual_advanced(&sf, &lp, &SolverOptions::default());
assert_eq!(r.status, SolveStatus::Optimal, "pattern B");
assert!(
(r.objective - (-4.0)).abs() < 1e-6,
"pattern B obj={}",
r.objective
);
}
}
#[test]
fn diag_basis_initial_x_b_divides_by_diagonal() {
use crate::sparse::CscMatrix;
let a = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 4.0], 2, 2).unwrap();
let x_b = diag_basis_initial_x_b(&a, &[0, 1], &[10.0, 12.0]);
assert!(
(x_b[0] - 5.0).abs() < 1e-12,
"row0: expected b/diag=5, got {}",
x_b[0]
);
assert!(
(x_b[1] - 3.0).abs() < 1e-12,
"row1: expected b/diag=3, got {}",
x_b[1]
);
}
#[test]
fn eq_ub_phase1_scaled_le_slack_feasible() {
use crate::sparse::CscMatrix;
let a =
CscMatrix::from_triplets(&[0, 0, 1], &[0, 1, 1], &[1.0, -1.0, 100.0], 2, 2).unwrap();
let lp = LpProblem::new_general(
vec![0.0, -1.0],
a,
vec![0.0, 100.0],
vec![ConstraintType::Eq, ConstraintType::Le],
vec![(0.0, 2.0), (0.0, 2.0)],
None,
)
.unwrap();
let sf = build_standard_form(&lp);
bounded_core::reset_eq_ub_dispatch_count();
let r = solve_dual_advanced(&sf, &lp, &SolverOptions::default());
assert!(
bounded_core::eq_ub_dispatch_count() > 0,
"new Eq+UB path not dispatched"
);
assert_eq!(
r.status,
SolveStatus::Optimal,
"feasible LP misreported as {:?} (scaled-slack init bug)",
r.status
);
assert!(
(r.objective - (-1.0)).abs() < 1e-6,
"expected obj=-1, got {:.6e}",
r.objective
);
assert!(
(r.solution[0] - r.solution[1]).abs() < 1e-6,
"Eq x0-x1=0 violated: x0={}, x1={}",
r.solution[0],
r.solution[1]
);
}
#[test]
fn eq_ub_phase1_bfrt_flip_count_positive() {
use crate::sparse::CscMatrix;
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
let lp = LpProblem::new_general(
vec![-3.0, -1.0],
a,
vec![4.0],
vec![ConstraintType::Eq],
vec![(0.0, 2.0), (0.0, 3.0)],
None,
)
.unwrap();
let sf = build_standard_form(&lp);
let opts = SolverOptions {
use_lp_crash_basis: false,
..SolverOptions::default()
};
reset_bfrt_flip_invocations();
let r = solve_dual_advanced(&sf, &lp, &opts);
let flips = bfrt_flip_invocations();
assert_eq!(r.status, SolveStatus::Optimal);
assert!(
(r.objective - (-8.0)).abs() < 1e-6,
"expected obj=-8, got {:.6e}",
r.objective
);
assert!(
flips > 0,
"eq_ub_phase1_bfrt_flip_count_positive: flip count = 0, BFRT not exercised in new path"
);
}
#[test]
fn p1_hypothesis_art_goes_positive_in_phase2() {
use crate::sparse::CscMatrix;
let a = CscMatrix::from_triplets(&[0, 0, 1], &[0, 1, 1], &[1.0, -1.0, 1.0], 2, 2).unwrap();
let lp = LpProblem::new_general(
vec![0.0, -1.0],
a,
vec![0.0, 3.0],
vec![ConstraintType::Eq, ConstraintType::Le],
vec![(0.0, 2.0), (0.0, 3.0)],
None,
)
.unwrap();
let sf = build_standard_form(&lp);
let opts = SolverOptions {
use_lp_crash_basis: false,
..SolverOptions::default()
};
bounded_core::reset_eq_ub_dispatch_count();
let result = solve_dual_advanced(&sf, &lp, &opts);
let dispatch_count = bounded_core::eq_ub_dispatch_count();
assert!(
dispatch_count > 0,
"new Eq+UB path was NOT dispatched (count=0), test is not exercising the new code"
);
assert_eq!(
result.status,
SolveStatus::Optimal,
"expected Optimal, got {:?}",
result.status
);
assert_eq!(result.solution.len(), 2, "solution length wrong");
let x0 = result.solution[0];
let x1 = result.solution[1];
assert!(
(x0 - x1).abs() < 1e-6,
"Eq constraint violated: x0={x0:.6e}, x1={x1:.6e}, diff={:.6e}",
(x0 - x1).abs()
);
assert!(
(result.objective - (-2.0)).abs() < 1e-6,
"expected obj=-2, got {:.6e}",
result.objective
);
}
}