use super::super::crash;
use super::super::pricing::{DualLeavingStrategy, SteepestEdgePricing};
use super::super::{extract_dual_info, extract_solution, SimplexOutcome, StandardForm};
use super::core::dual_simplex_core_advanced;
use crate::basis::{BasisManager, LuBasis};
use crate::options::{SolverOptions, WarmStartBasis};
use crate::problem::{LpProblem, SolveStatus, SolverResult};
use crate::sparse::{CscMatrix, SparseVec};
use crate::tolerances::{DROP_TOL, PIVOT_TOL};
fn farkas_direction_certified(
a_aug: &CscMatrix,
b: &[f64],
y: &[f64],
n_total: usize,
tol: f64,
) -> bool {
let by: f64 = b.iter().zip(y.iter()).map(|(&bi, &yi)| bi * yi).sum();
if by <= tol {
return false;
}
for j in 0..n_total {
let (rows, vals) = a_aug.get_column(j).unwrap();
let aty: f64 = rows.iter().zip(vals.iter()).map(|(&r, &v)| v * y[r]).sum();
if aty > tol {
return false;
}
}
true
}
fn farkas_infeasibility_certified(
a_aug: &CscMatrix,
b: &[f64],
basis_aug: &[usize],
m: usize,
n_total: usize,
options: &SolverOptions,
) -> bool {
let art_rows: Vec<usize> = (0..m).filter(|&i| basis_aug[i] >= n_total).collect();
if art_rows.is_empty() {
return false;
}
let mut basis_mgr =
match LuBasis::new_timed(a_aug, basis_aug, options.max_etas, options.deadline) {
Ok(bm) => bm,
Err(_) => return false,
};
let b_norm = b.iter().fold(0.0_f64, |acc, &v| acc.max(v.abs()));
let tol = options.dual_tol * (1.0_f64).max(b_norm);
{
let mut y: Vec<f64> = (0..m)
.map(|i| if basis_aug[i] >= n_total { 1.0 } else { 0.0 })
.collect();
basis_mgr.btran_dense(&mut y);
if farkas_direction_certified(a_aug, b, &y, n_total, tol) {
return true;
}
}
for &row in &art_rows {
let mut e_i = vec![0.0_f64; m];
e_i[row] = 1.0;
basis_mgr.btran_dense(&mut e_i);
if farkas_direction_certified(a_aug, b, &e_i, n_total, tol) {
return true;
}
}
false
}
struct ArtificialPriorityLeaving {
n_total: usize,
}
impl DualLeavingStrategy for ArtificialPriorityLeaving {
fn select_leaving(&mut self, x_b: &[f64], primal_tol: f64, basis: &[usize]) -> Option<usize> {
let mut best_row: Option<usize> = None;
let mut max_violation = primal_tol;
for (i, &val) in x_b.iter().enumerate() {
if val < -max_violation {
max_violation = -val;
best_row = Some(i);
}
}
if best_row.is_some() {
return best_row;
}
let mut best_art: Option<usize> = None;
let mut max_art_val = primal_tol;
for (i, &val) in x_b.iter().enumerate() {
if basis[i] >= self.n_total && val > max_art_val {
max_art_val = val;
best_art = Some(i);
}
}
best_art
}
fn bland_leaving(&mut self, x_b: &[f64], primal_tol: f64, basis: &[usize]) -> Option<usize> {
let mut best_row: Option<usize> = None;
let mut best_var = usize::MAX;
for (i, &v) in x_b.iter().enumerate() {
if v < -primal_tol && basis[i] < best_var {
best_var = basis[i];
best_row = Some(i);
}
}
if best_row.is_some() {
return best_row;
}
for (i, &v) in x_b.iter().enumerate() {
if basis[i] >= self.n_total && v > primal_tol && basis[i] < best_var {
best_var = basis[i];
best_row = Some(i);
}
}
best_row
}
fn progress_metric(&mut self, x_b: &[f64], basis: &[usize]) -> f64 {
let neg_sum: f64 = x_b.iter().map(|&v| (-v).max(0.0)).sum();
let art_sum: f64 = (0..x_b.len())
.filter(|&i| basis[i] >= self.n_total)
.map(|i| x_b[i].max(0.0))
.sum();
neg_sum + art_sum
}
fn allows_lb_repair(&self) -> bool {
true
}
}
const BIG_M_COST_MULT: f64 = 1e3;
const BIG_M_FLOOR: f64 = 1e6;
struct BigMPhase1State {
a_aug: CscMatrix,
basis_aug: Vec<usize>,
c_aug_p1: Vec<f64>,
x_b: Vec<f64>,
artificial_col_of_row: Vec<Option<usize>>,
n_aug: usize,
}
fn build_a_aug(
a: &CscMatrix,
artificial_col_of_row: &[Option<usize>],
m: usize,
n_total: usize,
n_aug: usize,
) -> Option<CscMatrix> {
let n_art_estimate = n_aug - n_total;
let mut trip_rows: Vec<usize> = Vec::with_capacity(a.nnz() + n_art_estimate);
let mut trip_cols: Vec<usize> = Vec::with_capacity(a.nnz() + n_art_estimate);
let mut trip_vals: Vec<f64> = Vec::with_capacity(a.nnz() + n_art_estimate);
for j in 0..n_total {
let (rows, vals) = a.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 artificial_col_of_row.iter().enumerate() {
if let Some(col) = col_opt {
trip_rows.push(i);
trip_cols.push(*col);
trip_vals.push(1.0);
}
}
CscMatrix::from_triplets(&trip_rows, &trip_cols, &trip_vals, m, n_aug).ok()
}
fn build_identity_phase1_state(
a: &CscMatrix,
b: &[f64],
c: &[f64],
sf: &StandardForm,
big_m: f64,
n_total: usize,
) -> Option<BigMPhase1State> {
let m = sf.m;
let mut artificial_col_of_row: Vec<Option<usize>> = vec![None; m];
let mut n_art = 0usize;
for i in 0..m {
if sf.needs_artificial[i] {
artificial_col_of_row[i] = Some(n_total + n_art);
n_art += 1;
}
}
let n_aug = n_total + n_art;
let a_aug = build_a_aug(a, &artificial_col_of_row, m, n_total, n_aug)?;
let mut c_aug_p1 = vec![0.0_f64; n_aug];
for j in 0..n_total {
let (rows, vals) = a.get_column(j).unwrap();
let mut sum_art = 0.0_f64;
for (k, &row) in rows.iter().enumerate() {
if sf.needs_artificial[row] {
sum_art += vals[k];
}
}
let need = big_m * sum_art - c[j];
let delta = need.max(0.0);
c_aug_p1[j] = c[j] + delta;
}
for col in artificial_col_of_row.iter().flatten() {
c_aug_p1[*col] = big_m;
}
let mut basis_aug = sf.initial_basis.clone();
for i in 0..m {
if let Some(col) = artificial_col_of_row[i] {
basis_aug[i] = col;
}
}
let x_b = b.to_vec();
Some(BigMPhase1State {
a_aug,
basis_aug,
c_aug_p1,
x_b,
artificial_col_of_row,
n_aug,
})
}
#[cfg(test)]
mod crash_probe {
use std::cell::Cell;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Outcome {
DisabledOption,
NoArtificial,
NotReduced,
BuildAaugFailed,
LuFailed,
XbNegative,
Adopted(usize),
}
thread_local! {
static LAST_OUTCOME: Cell<Option<Outcome>> = const { Cell::new(None) };
}
pub fn record(out: Outcome) {
LAST_OUTCOME.with(|c| c.set(Some(out)));
}
pub fn take() -> Option<Outcome> {
LAST_OUTCOME.with(|c| c.replace(None))
}
pub fn clear() {
LAST_OUTCOME.with(|c| c.set(None));
}
}
#[allow(clippy::too_many_arguments)]
fn try_build_crash_phase1_state(
a: &CscMatrix,
b: &[f64],
c: &[f64],
sf: &StandardForm,
options: &SolverOptions,
big_m: f64,
n_total: usize,
) -> Option<BigMPhase1State> {
if !options.use_lp_crash_basis {
#[cfg(test)]
crash_probe::record(crash_probe::Outcome::DisabledOption);
return None;
}
if sf.num_artificial == 0 {
#[cfg(test)]
crash_probe::record(crash_probe::Outcome::NoArtificial);
return None;
}
let m = sf.m;
let (basis_pre, needs_artificial, n_art) = crash::compute_crash_basis(
a,
b,
m,
sf.n_shifted,
&sf.initial_basis,
&sf.needs_artificial,
);
if n_art >= sf.num_artificial {
#[cfg(test)]
crash_probe::record(crash_probe::Outcome::NotReduced);
return None;
}
let mut artificial_col_of_row: Vec<Option<usize>> = vec![None; m];
let mut art_idx = 0usize;
for i in 0..m {
if needs_artificial[i] {
artificial_col_of_row[i] = Some(n_total + art_idx);
art_idx += 1;
}
}
debug_assert_eq!(art_idx, n_art);
let n_aug = n_total + n_art;
let a_aug = match build_a_aug(a, &artificial_col_of_row, m, n_total, n_aug) {
Some(a) => a,
None => {
#[cfg(test)]
crash_probe::record(crash_probe::Outcome::BuildAaugFailed);
return None;
}
};
let mut basis_aug = basis_pre;
for i in 0..m {
if let Some(col) = artificial_col_of_row[i] {
basis_aug[i] = col;
}
}
let mut basis_mgr =
match LuBasis::new_timed(&a_aug, &basis_aug, options.max_etas, options.deadline) {
Ok(bm) => bm,
Err(_) => {
#[cfg(test)]
crash_probe::record(crash_probe::Outcome::LuFailed);
return None;
}
};
let mut x_b_sv = SparseVec::from_dense(b);
basis_mgr.ftran(&mut x_b_sv);
let x_b = x_b_sv.to_dense();
if x_b.iter().any(|&v| v < -PIVOT_TOL) {
#[cfg(test)]
crash_probe::record(crash_probe::Outcome::XbNegative);
return None;
}
let mut c_b: Vec<f64> = (0..m)
.map(|i| {
let col = basis_aug[i];
if col >= n_total {
big_m
} else {
c[col]
}
})
.collect();
basis_mgr.btran_dense(&mut c_b);
let y = c_b;
let mut in_basis = vec![false; n_aug];
for &col in &basis_aug {
in_basis[col] = true;
}
let mut c_aug_p1 = vec![0.0_f64; n_aug];
for col in artificial_col_of_row.iter().flatten() {
c_aug_p1[*col] = big_m;
}
for j in 0..n_total {
if in_basis[j] {
c_aug_p1[j] = c[j];
} else {
let (rows, vals) = a_aug.get_column(j).unwrap();
let mut aty = 0.0_f64;
for (k, &row) in rows.iter().enumerate() {
aty += vals[k] * y[row];
}
let delta = (aty - c[j]).max(0.0);
c_aug_p1[j] = c[j] + delta;
}
}
#[cfg(test)]
crash_probe::record(crash_probe::Outcome::Adopted(n_art));
Some(BigMPhase1State {
a_aug,
basis_aug,
c_aug_p1,
x_b,
artificial_col_of_row,
n_aug,
})
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn big_m_cold_start(
sf: &StandardForm,
problem: &LpProblem,
options: &SolverOptions,
a: &CscMatrix,
b: &[f64],
c: &[f64],
row_scale: &[f64],
col_scale: &[f64],
) -> SolverResult {
let m = sf.m;
let n_total = sf.n_total;
let c_norm = c.iter().fold(0.0_f64, |acc, &v| acc.max(v.abs()));
let b_norm = b.iter().fold(0.0_f64, |acc, &v| acc.max(v.abs()));
let big_m = (c_norm * BIG_M_COST_MULT)
.max(b_norm * BIG_M_COST_MULT)
.max(BIG_M_FLOOR);
let crash_state = try_build_crash_phase1_state(a, b, c, sf, options, big_m, n_total);
let BigMPhase1State {
a_aug,
mut basis_aug,
c_aug_p1,
mut x_b,
artificial_col_of_row,
n_aug,
} = match crash_state {
Some(s) => s,
None => match build_identity_phase1_state(a, b, c, sf, big_m, n_total) {
Some(s) => s,
None => return SolverResult::numerical_error(),
},
};
let mut leaving = ArtificialPriorityLeaving { n_total };
let mut total_iters: usize = 0;
let phase1_outcome = dual_simplex_core_advanced(
&a_aug,
&mut x_b,
&c_aug_p1,
&mut basis_aug,
m,
n_aug,
n_total,
true, options,
&mut leaving,
&mut total_iters,
);
match phase1_outcome {
SimplexOutcome::Unbounded => {
if farkas_infeasibility_certified(&a_aug, b, &basis_aug, m, n_total, options) {
let mut r = SolverResult::infeasible();
r.iterations = total_iters;
return r;
}
let x_b_check: Vec<f64> = match crate::basis::LuBasis::new_timed(
&a_aug, &basis_aug, options.max_etas, options.deadline,
) {
Ok(mut bm) => {
let mut rhs = b.to_vec();
bm.ftran_dense(&mut rhs);
rhs
}
Err(_) => x_b.clone(),
};
let any_positive_art = (0..m)
.any(|i| basis_aug[i] >= n_total && x_b_check[i] > options.primal_tol);
if any_positive_art {
let mut r = SolverResult::infeasible();
r.iterations = total_iters;
return r;
}
return super::super::timeout_result_with_incumbent(
sf,
problem,
&basis_aug,
&x_b,
col_scale,
total_iters,
);
}
SimplexOutcome::Timeout(_) => {
let any_artificial_left =
(0..m).any(|i| basis_aug[i] >= n_total && x_b[i].abs() > options.primal_tol);
if any_artificial_left
&& farkas_infeasibility_certified(&a_aug, b, &basis_aug, m, n_total, options)
{
let mut r = SolverResult::infeasible();
r.iterations = total_iters;
return r;
}
if any_artificial_left {
let mut leaving2 = ArtificialPriorityLeaving { n_total };
let phase1b_outcome = dual_simplex_core_advanced(
&a_aug,
&mut x_b,
&c_aug_p1,
&mut basis_aug,
m,
n_aug,
n_total,
false, options,
&mut leaving2,
&mut total_iters,
);
match phase1b_outcome {
SimplexOutcome::Unbounded => {
if farkas_infeasibility_certified(
&a_aug, b, &basis_aug, m, n_total, options,
) {
let mut r = SolverResult::infeasible();
r.iterations = total_iters;
return r;
}
return super::super::timeout_result_with_incumbent(
sf, problem, &basis_aug, &x_b, col_scale, total_iters,
);
}
SimplexOutcome::Timeout(_) => {
return super::super::timeout_result_with_incumbent(
sf, problem, &basis_aug, &x_b, col_scale, total_iters,
);
}
SimplexOutcome::SingularBasis => return SolverResult::numerical_error(),
SimplexOutcome::Optimal(_, _) => {
if let Ok(mut bm) = LuBasis::new_timed(
&a_aug, &basis_aug, options.max_etas, options.deadline,
) {
let mut rhs = SparseVec::from_dense(b);
bm.ftran(&mut rhs);
let fresh = rhs.to_dense();
x_b.copy_from_slice(&fresh);
}
let any_art = (0..m).any(|i| basis_aug[i] >= n_total);
if any_art
&& farkas_infeasibility_certified(
&a_aug, b, &basis_aug, m, n_total, options,
)
{
let mut r = SolverResult::infeasible();
r.iterations = total_iters;
return r;
}
}
}
} else {
return super::super::timeout_result_with_incumbent(
sf,
problem,
&basis_aug,
&x_b,
col_scale,
total_iters,
);
}
}
SimplexOutcome::SingularBasis => {
return SolverResult::numerical_error();
}
SimplexOutcome::Optimal(_, _) => {
if let Ok(mut bm) =
LuBasis::new_timed(&a_aug, &basis_aug, options.max_etas, options.deadline)
{
let mut rhs = SparseVec::from_dense(b);
bm.ftran(&mut rhs);
let fresh = rhs.to_dense();
x_b.copy_from_slice(&fresh);
}
let any_artificial_in_basis = (0..m).any(|i| basis_aug[i] >= n_total);
if any_artificial_in_basis
&& farkas_infeasibility_certified(&a_aug, b, &basis_aug, m, n_total, options)
{
let mut r = SolverResult::infeasible();
r.iterations = total_iters;
return r;
}
}
}
let mut c_aug_p2 = vec![0.0_f64; n_aug];
c_aug_p2[..n_total].copy_from_slice(c);
for col in artificial_col_of_row.iter().flatten() {
c_aug_p2[*col] = big_m;
}
for i in 0..m {
if x_b[i].abs() < crate::tolerances::PIVOT_TOL {
x_b[i] = crate::tolerances::PIVOT_TOL * (i as f64 + 1.0);
}
}
for v in x_b.iter_mut() {
if *v < 0.0 {
*v = 0.0;
}
}
let mut pricing = SteepestEdgePricing::new(n_aug);
let phase2_outcome = super::super::revised_simplex_core(
&a_aug,
&mut x_b,
&c_aug_p2,
b,
&mut basis_aug,
m,
n_aug,
n_aug,
&mut pricing,
options,
&mut total_iters,
false,
);
match phase2_outcome {
SimplexOutcome::Optimal(_obj_aug, y) => {
for i in 0..m {
if basis_aug[i] >= n_total && x_b[i].abs() > options.primal_tol {
if farkas_infeasibility_certified(&a_aug, b, &basis_aug, m, n_total, options) {
let mut r = SolverResult::infeasible();
r.iterations = total_iters;
return r;
}
return super::super::timeout_result_with_incumbent(
sf,
problem,
&basis_aug,
&x_b,
col_scale,
total_iters,
);
}
}
let solution = extract_solution(sf, &basis_aug, &x_b, col_scale);
let (dual_solution, reduced_costs, slack) =
extract_dual_info(sf, problem, &y, &solution, row_scale);
let ws = if basis_aug.iter().all(|&idx| idx < n_total) {
Some(WarmStartBasis {
basis: basis_aug.clone(),
x_b: x_b.clone(),
})
} else {
None
};
let obj_orig: f64 = problem
.c
.iter()
.zip(solution.iter())
.map(|(&ci, &xi)| ci * xi)
.sum();
SolverResult {
status: SolveStatus::Optimal,
objective: obj_orig,
solution,
dual_solution,
reduced_costs,
slack,
warm_start_basis: ws,
iterations: total_iters,
..Default::default()
}
}
SimplexOutcome::Unbounded => {
if super::super::dual_common::lp_unbounded_ray_verified(
&a_aug, &basis_aug, &c_aug_p2, m, n_aug, n_total, options,
) {
SolverResult {
status: SolveStatus::Unbounded,
objective: f64::NEG_INFINITY,
solution: vec![],
dual_solution: vec![],
reduced_costs: vec![],
slack: vec![],
warm_start_basis: None,
iterations: total_iters,
..Default::default()
}
} else {
super::super::timeout_result_with_incumbent(
sf,
problem,
&basis_aug,
&x_b,
col_scale,
total_iters,
)
}
}
SimplexOutcome::Timeout(_) => super::super::timeout_result_with_incumbent(
sf,
problem,
&basis_aug,
&x_b,
col_scale,
total_iters,
),
SimplexOutcome::SingularBasis => SolverResult::numerical_error(),
}
}
#[cfg(test)]
#[allow(clippy::print_stdout, clippy::print_stderr)]
mod tests {
use crate::options::SolverOptions;
use crate::problem::{ConstraintType, LpProblem, SolveStatus};
use crate::simplex::solve_with;
use crate::sparse::CscMatrix;
use crate::test_kkt::assert_kkt_optimal;
#[test]
fn big_m_phase1_feasible_eq() {
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();
assert_kkt_optimal(&lp, 3.0, "big_m_phase1_feasible_eq");
}
#[test]
fn big_m_phase1_artificial_lb_repair_edge_recovers_via_fallback() {
let a = CscMatrix::from_triplets(
&[0, 1, 0, 1],
&[0, 0, 1, 1],
&[-2.0, -2.0, 1.0, 2.0],
2,
2,
)
.unwrap();
let lp = LpProblem::new_general(
vec![1.0, 1.0],
a,
vec![1.0, 3.0],
vec![ConstraintType::Eq, ConstraintType::Eq],
vec![(0.0, f64::INFINITY); 2],
None,
)
.unwrap();
let opts = SolverOptions {
presolve: false,
use_lp_crash_basis: false,
..SolverOptions::default()
};
let r = solve_with(&lp, &opts);
assert_eq!(
r.status,
SolveStatus::Optimal,
"bare Big-M (crash off) must still reach Optimal via the primal fallback; got {:?}",
r.status
);
assert!(
(r.objective - 2.5).abs() < 1e-6,
"expected obj 2.5, got {}",
r.objective
);
}
#[test]
fn big_m_phase1_feasible_ge() {
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::Ge],
vec![(0.0, f64::INFINITY); 2],
None,
)
.unwrap();
assert_kkt_optimal(&lp, 5.0, "big_m_phase1_feasible_ge");
}
#[test]
fn big_m_phase1_infeasible_eq_contradiction() {
let a = CscMatrix::from_triplets(&[0, 0, 1, 1], &[0, 1, 0, 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, 2.0],
vec![ConstraintType::Eq, ConstraintType::Eq],
vec![(0.0, f64::INFINITY); 2],
None,
)
.unwrap();
let result = solve_with(&lp, &SolverOptions::default());
assert_eq!(
result.status,
SolveStatus::Infeasible,
"got {:?}",
result.status
);
}
#[test]
fn big_m_phase1_infeasible_ge_eq_mix() {
let a = CscMatrix::from_triplets(&[0, 0, 1, 1], &[0, 1, 0, 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, 2.0],
vec![ConstraintType::Ge, ConstraintType::Eq],
vec![(0.0, f64::INFINITY); 2],
None,
)
.unwrap();
let result = solve_with(&lp, &SolverOptions::default());
assert_eq!(
result.status,
SolveStatus::Infeasible,
"got {:?}",
result.status
);
}
#[test]
fn big_m_phase1_le_ge_range_feasible() {
let a = CscMatrix::from_triplets(&[0, 0, 1, 1], &[0, 1, 0, 1], &[1.0, 1.0, 1.0, 1.0], 2, 2)
.unwrap();
let lp = LpProblem::new_general(
vec![1.0, 1.0],
a,
vec![7.0, 3.0],
vec![ConstraintType::Le, ConstraintType::Ge],
vec![(0.0, f64::INFINITY); 2],
None,
)
.unwrap();
assert_kkt_optimal(&lp, 3.0, "big_m_phase1_le_ge_range_feasible");
}
#[test]
fn big_m_phase1_ge_b_zero_bypasses_bigm() {
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![0.0],
vec![ConstraintType::Ge],
vec![(0.0, f64::INFINITY); 2],
None,
)
.unwrap();
assert_kkt_optimal(&lp, 0.0, "big_m_phase1_ge_b_zero_bypasses_bigm");
}
#[test]
fn big_m_phase1_degenerate_eq_zero_rhs() {
let a = CscMatrix::from_triplets(&[0, 0, 1, 1], &[0, 1, 0, 2], &[1.0, 1.0, 1.0, 1.0], 2, 3)
.unwrap();
let lp = LpProblem::new_general(
vec![0.0, 0.0, 1.0],
a,
vec![0.0, 1.0],
vec![ConstraintType::Eq, ConstraintType::Eq],
vec![(0.0, f64::INFINITY); 3],
None,
)
.unwrap();
assert_kkt_optimal(&lp, 1.0, "big_m_phase1_degenerate_eq_zero_rhs");
}
#[test]
fn big_m_phase1_large_coeff_eq_ge_mix() {
let a =
CscMatrix::from_triplets(&[0, 0, 1, 1], &[0, 1, 0, 1], &[1.0e6, 1.0, 1.0, 1.0], 2, 2)
.unwrap();
let lp = LpProblem::new_general(
vec![1.0, 1.0],
a,
vec![2.0e6, 1.0],
vec![ConstraintType::Eq, ConstraintType::Ge],
vec![(0.0, f64::INFINITY); 2],
None,
)
.unwrap();
assert_kkt_optimal(&lp, 2.0, "big_m_phase1_large_coeff_eq_ge_mix");
}
#[test]
fn artificial_priority_bland_picks_artificial_when_xb_nonneg() {
use super::ArtificialPriorityLeaving;
use crate::simplex::pricing::DualLeavingStrategy;
let n_total = 3usize;
let mut strat = ArtificialPriorityLeaving { n_total };
let basis = vec![1usize, n_total]; let x_b = vec![0.5_f64, 2.0_f64];
let pick = strat.bland_leaving(&x_b, 1e-9, &basis);
assert_eq!(
pick,
Some(1),
"bland_leaving must select artificial row when x_B >= 0"
);
let basis2 = vec![0usize, 1usize];
let pick2 = strat.bland_leaving(&x_b, 1e-9, &basis2);
assert_eq!(pick2, None);
}
#[test]
fn artificial_priority_progress_metric_includes_artificial_sum() {
use super::ArtificialPriorityLeaving;
use crate::simplex::pricing::DualLeavingStrategy;
let n_total = 2usize;
let mut strat = ArtificialPriorityLeaving { n_total };
let basis = vec![0usize, n_total]; let x_b = vec![3.0_f64, 5.0_f64];
assert!((strat.progress_metric(&x_b, &basis) - 5.0).abs() < 1e-12);
let basis2 = vec![0usize, 1usize];
assert!(strat.progress_metric(&x_b, &basis2) < 1e-12);
}
#[test]
fn big_m_phase1_no_false_infeasible_when_blandmode_triggers() {
let a = CscMatrix::from_triplets(
&[0, 0, 0, 1, 1, 1, 2, 2, 2],
&[0, 1, 2, 0, 1, 2, 0, 1, 2],
&[5.0, 3.0, 2.0, 2.0, 7.0, 1.0, 1.0, 1.0, 1.0],
3,
3,
)
.unwrap();
let lp = LpProblem::new_general(
vec![1.0, 1.0, 1.0],
a,
vec![10.0, 5.0, 3.0],
vec![ConstraintType::Eq; 3],
vec![(0.0, f64::INFINITY); 3],
None,
)
.unwrap();
assert_kkt_optimal(
&lp,
3.0,
"big_m_phase1_no_false_infeasible_when_blandmode_triggers",
);
}
#[test]
fn big_m_phase1_free_var_eq() {
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![2.0],
vec![ConstraintType::Eq],
vec![(f64::NEG_INFINITY, f64::INFINITY), (0.0, f64::INFINITY)],
None,
)
.unwrap();
assert_kkt_optimal(&lp, 2.0, "big_m_phase1_free_var_eq");
}
use std::sync::Mutex;
static SERIAL_LOCK: Mutex<()> = Mutex::new(());
fn invoke_big_m_with_option(
lp: &LpProblem,
use_crash: bool,
) -> (
crate::problem::SolverResult,
usize,
super::crash_probe::Outcome,
) {
invoke_big_m_with_option_deadline_secs(lp, use_crash, 60.0)
}
fn invoke_big_m_with_option_deadline_secs(
lp: &LpProblem,
use_crash: bool,
deadline_secs: f64,
) -> (
crate::problem::SolverResult,
usize,
super::crash_probe::Outcome,
) {
use crate::presolve::LpEquilibration;
let sf = crate::simplex::build_standard_form(lp);
let (a, b, c, row_scale, col_scale) = LpEquilibration::scale(&sf.a, &sf.b, &sf.c);
let opts = SolverOptions {
use_lp_crash_basis: use_crash,
timeout_secs: Some(deadline_secs),
max_etas: crate::options::default_max_etas(sf.m),
deadline: Some(
std::time::Instant::now() + std::time::Duration::from_secs_f64(deadline_secs),
),
..Default::default()
};
super::crash_probe::clear();
let result = super::big_m_cold_start(&sf, lp, &opts, &a, &b, &c, &row_scale, &col_scale);
let outcome = super::crash_probe::take()
.expect("crash_probe must record an Outcome on every big_m_cold_start invocation");
let n_art_obs = match outcome {
super::crash_probe::Outcome::Adopted(n) => n,
_ => sf.num_artificial,
};
(result, n_art_obs, outcome)
}
fn build_network_eq_lp(n_flow: usize, n_hub: usize, seed_init: u64) -> LpProblem {
let mut seed = seed_init;
let mut next = || -> f64 {
seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((seed >> 16) as f64 / (u64::MAX >> 16) as f64) * 2.0 - 1.0
};
let n = n_flow + n_hub;
let m_eq = n_flow;
let mut a_rows = Vec::new();
let mut a_cols = Vec::new();
let mut a_vals = Vec::new();
for i in 0..n_flow {
a_rows.push(i);
a_cols.push(i);
a_vals.push(1.0);
}
for h in 0..n_hub {
for i in 0..n_flow {
a_rows.push(i);
a_cols.push(n_flow + h);
a_vals.push(0.01 + 0.02 * (next() + 1.0) * 0.5);
}
}
let a = CscMatrix::from_triplets(&a_rows, &a_cols, &a_vals, m_eq, n).unwrap();
let b: Vec<f64> = (0..m_eq).map(|_| 1.0 + (next() + 1.0) * 0.25).collect();
let c: Vec<f64> = (0..n).map(|_| next()).collect();
let bounds = vec![(0.0_f64, 10.0_f64); n];
LpProblem::new_general(c, a, b, vec![ConstraintType::Eq; m_eq], bounds, None).unwrap()
}
fn build_ge_eq_mix_lp(
n_eq: usize,
n_ge: usize,
n_struct_extra: usize,
seed_init: u64,
) -> LpProblem {
let mut seed = seed_init;
let mut next = || -> f64 {
seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((seed >> 16) as f64 / (u64::MAX >> 16) as f64) * 2.0 - 1.0
};
let m = n_eq + n_ge;
let n = m + n_struct_extra;
let mut a_rows = Vec::new();
let mut a_cols = Vec::new();
let mut a_vals = Vec::new();
for i in 0..m {
a_rows.push(i);
a_cols.push(i);
a_vals.push(1.0); }
for j in 0..n_struct_extra {
for i in 0..m {
a_rows.push(i);
a_cols.push(m + j);
a_vals.push(0.05 * (next() + 1.0));
}
}
let a = CscMatrix::from_triplets(&a_rows, &a_cols, &a_vals, m, n).unwrap();
let b: Vec<f64> = (0..m).map(|i| if i < n_eq { 1.0 } else { 0.5 }).collect();
let c: Vec<f64> = (0..n).map(|_| (next() + 1.0) * 0.5).collect();
let mut ct = vec![ConstraintType::Eq; n_eq];
ct.extend(std::iter::repeat_n(ConstraintType::Ge, n_ge));
let bounds = vec![(0.0_f64, 10.0_f64); n];
LpProblem::new_general(c, a, b, ct, bounds, None).unwrap()
}
fn build_beale_eq_lp() -> LpProblem {
let a = CscMatrix::from_triplets(
&[0, 1, 2, 0, 1, 2, 0, 1, 2, 0],
&[0, 1, 2, 3, 3, 3, 0, 1, 2, 1], &[1.0, 1.0, 1.0, 0.1, 0.1, 0.1, 0.3, 0.3, 0.3, 0.0001],
3,
4,
)
.unwrap();
let b = vec![1.0, 2.0, 3.0];
let c = vec![1.0, 1.0, 1.0, 0.5];
let bounds = vec![(0.0_f64, 100.0_f64); 4];
LpProblem::new_general(c, a, b, vec![ConstraintType::Eq; 3], bounds, None).unwrap()
}
#[test]
fn crash_reduces_num_artificial_network() {
let _guard = SERIAL_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let lp = build_network_eq_lp(80, 3, 0xF1F2_F3F4_F5F6_F7F8);
let (_r_off, n_art_off, out_off) = invoke_big_m_with_option(&lp, false);
let (_r_on, n_art_on, out_on) = invoke_big_m_with_option(&lp, true);
eprintln!(
"CRASH_BIGM_NETWORK: n_art_off={} n_art_on={} out_off={:?} out_on={:?}",
n_art_off, n_art_on, out_off, out_on
);
assert!(
matches!(out_off, super::crash_probe::Outcome::DisabledOption),
"off path must short-circuit on use_lp_crash_basis=false; got {:?}",
out_off
);
assert!(
matches!(out_on, super::crash_probe::Outcome::Adopted(_)),
"on path must adopt crash state; got {:?}",
out_on
);
assert!(
n_art_on < n_art_off,
"crash must reduce num_artificial: off={} on={}",
n_art_off,
n_art_on
);
let reduction_ratio = (n_art_off - n_art_on) as f64 / n_art_off.max(1) as f64;
assert!(
reduction_ratio >= 0.30,
"crash artificial reduction {:.2} < 0.30 (off={} on={})",
reduction_ratio,
n_art_off,
n_art_on
);
}
#[test]
fn crash_reduces_iters_ge_eq_mix() {
let _guard = SERIAL_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let lp = build_ge_eq_mix_lp(40, 30, 4, 0xA1A2_A3A4_A5A6_A7A8);
let (r_off, n_art_off, out_off) = invoke_big_m_with_option(&lp, false);
let (r_on, n_art_on, out_on) = invoke_big_m_with_option(&lp, true);
eprintln!(
"CRASH_BIGM_MIX: n_art_off={} n_art_on={} iter_off={} iter_on={} status_off={:?} status_on={:?} out_on={:?}",
n_art_off, n_art_on, r_off.iterations, r_on.iterations, r_off.status, r_on.status, out_on,
);
assert_eq!(r_off.status, SolveStatus::Optimal, "off must be Optimal");
assert_eq!(r_on.status, SolveStatus::Optimal, "on must be Optimal");
assert!(matches!(
out_off,
super::crash_probe::Outcome::DisabledOption
));
assert!(matches!(out_on, super::crash_probe::Outcome::Adopted(_)));
let obj_diff = (r_on.objective - r_off.objective).abs() / (1.0 + r_off.objective.abs());
assert!(obj_diff < 1e-6, "crash obj drift: {:.3e}", obj_diff);
assert!(
n_art_on < n_art_off,
"crash artif reduction expected: off={} on={}",
n_art_off,
n_art_on
);
const ITER_REDUCTION_THRESHOLD: f64 = 0.7;
assert!(
(r_on.iterations as f64) < (r_off.iterations as f64) * ITER_REDUCTION_THRESHOLD,
"crash iter reduction insufficient: off={} on={} (need on < {:.0})",
r_off.iterations,
r_on.iterations,
(r_off.iterations as f64) * ITER_REDUCTION_THRESHOLD,
);
}
#[test]
fn crash_handles_beale_degenerate_eq() {
let _guard = SERIAL_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let lp = build_beale_eq_lp();
let (r_off, n_art_off, out_off) = invoke_big_m_with_option(&lp, false);
let (r_on, n_art_on, out_on) = invoke_big_m_with_option(&lp, true);
eprintln!(
"CRASH_BIGM_BEALE: n_art_off={} n_art_on={} iter_off={} iter_on={} out_off={:?} out_on={:?}",
n_art_off, n_art_on, r_off.iterations, r_on.iterations, out_off, out_on,
);
assert_eq!(r_off.status, SolveStatus::Optimal, "off Optimal");
assert_eq!(r_on.status, SolveStatus::Optimal, "on Optimal");
assert!(
matches!(out_on, super::crash_probe::Outcome::Adopted(0)),
"Beale: crash must adopt with n_art=0; got {:?}",
out_on
);
assert!(n_art_on <= n_art_off);
assert_eq!(n_art_on, 0, "Beale 縮約は全 artif を crash で除去できる");
}
#[test]
fn crash_reduces_num_artificial_multi_seed() {
let _guard = SERIAL_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let seeds: &[u64] = &[
0xC0FF_EE00_DEAD_BEEF,
0x1234_5678_9ABC_DEF0,
0xF00D_BABE_FACE_CAFE,
0xA5A5_5A5A_3C3C_C3C3,
0x1111_2222_3333_4444,
];
let mut wins = 0usize;
let mut adopt_count = 0usize;
for &seed in seeds {
let lp = build_network_eq_lp(50, 2, seed);
let (_, n_off, _) = invoke_big_m_with_option(&lp, false);
let (_, n_on, out_on) = invoke_big_m_with_option(&lp, true);
eprintln!(
"CRASH_BIGM_SEED 0x{:x}: off={} on={} out_on={:?}",
seed, n_off, n_on, out_on
);
if matches!(out_on, super::crash_probe::Outcome::Adopted(_)) {
adopt_count += 1;
}
if n_on < n_off {
wins += 1;
}
}
assert!(
wins >= 4,
"crash reduced num_artificial on {}/{} seeds (need ≥ 4)",
wins,
seeds.len()
);
assert!(
adopt_count >= 4,
"crash actually adopted on {}/{} seeds (need ≥ 4)",
adopt_count,
seeds.len()
);
}
#[test]
fn crash_disabled_option_collapses_to_identity() {
let _guard = SERIAL_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let lp = build_network_eq_lp(60, 2, 0xDEAD_BEEF_CAFE_F00D);
let (r_off, n_off, out_off) = invoke_big_m_with_option(&lp, false);
assert!(
matches!(out_off, super::crash_probe::Outcome::DisabledOption),
"use_lp_crash_basis=false must short-circuit on DisabledOption; got {:?}",
out_off
);
let (r_on, n_on, out_on) = invoke_big_m_with_option(&lp, true);
assert!(
matches!(out_on, super::crash_probe::Outcome::Adopted(_)),
"use_lp_crash_basis=true must adopt crash; got {:?}",
out_on
);
assert!(
n_on < n_off,
"crash must reduce num_artificial: off={} on={}",
n_off,
n_on
);
assert_eq!(r_off.status, SolveStatus::Optimal, "off must be Optimal");
assert_eq!(r_on.status, SolveStatus::Optimal, "on must be Optimal");
let obj_diff = (r_off.objective - r_on.objective).abs() / (1.0 + r_off.objective.abs());
assert!(
obj_diff < 1e-6,
"objective must match regardless of crash: {:.3e}",
obj_diff
);
}
#[test]
fn crash_lu_failure_falls_back_to_identity() {
let _guard = SERIAL_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let a = CscMatrix::from_triplets(
&[0, 1, 0, 1, 2],
&[0, 0, 1, 1, 2],
&[1.0, 1.0, 1.0, 1.0, 1.0],
3,
3,
)
.unwrap();
let lp = LpProblem::new_general(
vec![1.0, 1.0, 1.0],
a,
vec![1.0, 1.0, 1.0],
vec![ConstraintType::Eq; 3],
vec![(0.0, f64::INFINITY); 3],
None,
)
.unwrap();
let (_r, _n, out) = invoke_big_m_with_option(&lp, true);
assert!(
matches!(
out,
super::crash_probe::Outcome::LuFailed | super::crash_probe::Outcome::NotReduced
),
"duplicate-col LP must trigger LU failure or NotReduced fallback; got {:?}",
out,
);
}
#[test]
fn crash_xb_negative_falls_back_to_identity() {
let _guard = SERIAL_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let mut seed: u64 = 0xCAFEBABE_DEADBEEF;
let mut next = || {
seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((seed >> 11) as f64 / ((1u64 << 53) as f64)) * 2.0 - 1.0
};
const RANDOM_PROBE_RETRIES: usize = 20;
let mut found = false;
let mut last_outcome = super::crash_probe::Outcome::DisabledOption;
for _ in 0..RANDOM_PROBE_RETRIES {
let n = 6usize;
let m = 4usize;
let mut rows = Vec::new();
let mut cols = Vec::new();
let mut vals = Vec::new();
for i in 0..m {
rows.push(i);
cols.push(i);
vals.push(if next() < 0.0 { -1.0 } else { 1.0 });
rows.push(i);
cols.push((i + 1) % m);
vals.push(next() * 0.5);
}
for j in m..n {
for i in 0..m {
rows.push(i);
cols.push(j);
vals.push(next() * 0.3);
}
}
let a = match CscMatrix::from_triplets(&rows, &cols, &vals, m, n) {
Ok(a) => a,
Err(_) => continue,
};
let b: Vec<f64> = (0..m).map(|_| 0.5 + next().abs()).collect();
let c: Vec<f64> = (0..n).map(|_| next().abs()).collect();
let lp = match LpProblem::new_general(
c,
a,
b,
vec![ConstraintType::Eq; m],
vec![(0.0, f64::INFINITY); n],
None,
) {
Ok(lp) => lp,
Err(_) => continue,
};
let (_, _, out) = invoke_big_m_with_option_deadline_secs(&lp, true, 0.5);
last_outcome = out;
if matches!(out, super::crash_probe::Outcome::XbNegative) {
found = true;
break;
}
}
assert!(
found || !matches!(last_outcome, super::crash_probe::Outcome::Adopted(_)),
"x_B < 0 fallback path unreachable; last_outcome={:?}",
last_outcome,
);
}
#[test]
fn artificial_priority_allows_lb_repair_is_true() {
use super::ArtificialPriorityLeaving;
use crate::simplex::pricing::DualLeavingStrategy;
let strat = ArtificialPriorityLeaving { n_total: 4 };
assert!(
strat.allows_lb_repair(),
"ArtificialPriorityLeaving must allow lb-repair so Priority-2-manufactured \
lb-violations are sign-flip-repaired (blanket false re-introduces the \
beaconfd/scrs8 Phase-I 2-cycle)"
);
}
#[test]
fn most_infeasible_allows_lb_repair_is_true() {
use crate::simplex::pricing::{DualLeavingStrategy, MostInfeasibleLeaving};
let strat = MostInfeasibleLeaving;
assert!(
strat.allows_lb_repair(),
"MostInfeasibleLeaving must allow lb-repair (allows_lb_repair == true)"
);
}
#[test]
fn farkas_direction_certified_accepts_valid_direction() {
use super::farkas_direction_certified;
let a_aug = CscMatrix::from_triplets(
&[0, 0, 1, 1],
&[0, 1, 0, 2],
&[1.0, 1.0, -1.0, 1.0],
2,
3,
)
.unwrap();
let b = [1.0_f64, 2.0_f64];
let y = [1.0_f64, 1.0];
let tol = 1e-6_f64;
assert!(
farkas_direction_certified(&a_aug, &b, &y, 1, tol),
"valid Farkas direction (b^Ty=3>0, A^Ty_x0=0<=tol) must be certified"
);
}
#[test]
fn farkas_direction_not_certified_when_aty_positive() {
use super::farkas_direction_certified;
let a_aug =
CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
let b = [2.0_f64];
let y = [1.0_f64];
let tol = 1e-6_f64;
assert!(
!farkas_direction_certified(&a_aug, &b, &y, 1, tol),
"A^T y = 1 > tol: direction must NOT be certified (removing A^T check breaks this)"
);
}
#[test]
fn big_m_phase1_infeasible_eq_ge_cancellation_class() {
let a = CscMatrix::from_triplets(
&[0, 0, 1, 1],
&[0, 1, 0, 1],
&[1.0, 1.0, 1.0, 1.0],
2,
2,
)
.unwrap();
let lp = LpProblem::new_general(
vec![1.0, 1.0],
a,
vec![3.0, 5.0],
vec![ConstraintType::Eq, ConstraintType::Ge],
vec![(0.0, f64::INFINITY); 2],
None,
)
.unwrap();
let result = solve_with(&lp, &SolverOptions::default());
assert_eq!(
result.status,
SolveStatus::Infeasible,
"x0+x1=3 AND x0+x1>=5 must be detected as Infeasible; got {:?}. \
If Timeout: per-row Farkas probe was removed or joint-only is insufficient \
for this Eq+Ge combination.",
result.status
);
}
#[test]
fn big_m_phase1_unbounded_with_positive_art_declares_infeasible() {
let a = CscMatrix::from_triplets(
&[0, 1],
&[0, 0],
&[1.0, -1.0],
2,
1,
)
.unwrap();
let lp = LpProblem::new_general(
vec![1.0],
a,
vec![2.0, 1.0],
vec![ConstraintType::Eq, ConstraintType::Ge],
vec![(0.0, f64::INFINITY)],
None,
)
.unwrap();
let result = solve_with(&lp, &SolverOptions::default());
assert_eq!(
result.status,
SolveStatus::Infeasible,
"x0=2 AND -x0>=1 is infeasible; Big-M Phase I Unbounded+positive_art must \
return Infeasible (not Timeout). got {:?}",
result.status
);
}
}