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};
use super::super::{StandardForm, SimplexOutcome, extract_solution, extract_dual_info};
use super::super::crash;
use super::super::pricing::{DualLeavingStrategy, SteepestEdgePricing};
use super::core::dual_simplex_core_advanced;
fn farkas_infeasibility_certified(
a_aug: &CscMatrix,
b: &[f64],
basis_aug: &[usize],
m: usize,
n_total: usize,
options: &SolverOptions,
) -> bool {
let c_phase1: Vec<f64> = (0..m)
.map(|i| if basis_aug[i] >= n_total { 1.0 } else { 0.0 })
.collect();
let mut basis_mgr = match LuBasis::new(a_aug, basis_aug, options.max_etas) {
Ok(bm) => bm,
Err(_) => return false,
};
let mut y = c_phase1;
basis_mgr.btran_dense(&mut y);
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 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 mut aty = 0.0_f64;
for (k, &row) in rows.iter().enumerate() {
aty += vals[k] * y[row];
}
if aty > tol {
return false;
}
}
true
}
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
}
}
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,
n_art: 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);
}
}
let _ = m;
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, n_art,
})
}
fn crash_disabled_by_env() -> bool {
std::env::var("LP_CRASH_DUAL_ADV_DISABLE").ok().as_deref() == Some("1")
}
#[cfg(test)]
mod crash_probe {
use std::cell::Cell;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Outcome {
DisabledOption,
DisabledEnv,
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 crash_disabled_by_env() {
#[cfg(test)] crash_probe::record(crash_probe::Outcome::DisabledEnv);
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(&a_aug, &basis_aug, options.max_etas) {
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, n_art,
})
}
#[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 crash_used = crash_state.is_some();
let BigMPhase1State {
a_aug,
mut basis_aug,
c_aug_p1,
mut x_b,
artificial_col_of_row,
n_aug,
n_art,
} = 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 _ = (crash_used, n_art);
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, options, &mut leaving,
&mut total_iters,
);
match phase1_outcome {
SimplexOutcome::Unbounded => {
let mut r = SolverResult::infeasible();
r.iterations = total_iters;
return r;
}
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;
}
let r = super::super::timeout_result_with_incumbent(
sf, problem, &basis_aug, &x_b, col_scale, total_iters,
);
return r;
}
SimplexOutcome::SingularBasis => {
return SolverResult::numerical_error();
}
SimplexOutcome::Optimal(_, _) => {
if let Ok(mut bm) = LuBasis::new(&a_aug, &basis_aug, options.max_etas) {
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;
}
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 {
let mut r = SolverResult::infeasible();
r.iterations = total_iters;
return r;
}
}
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 + sf.obj_offset,
solution,
dual_solution,
reduced_costs,
slack,
warm_start_basis: 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,
iterations: total_iters,
..Default::default()
},
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_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::RuizScaler;
let sf = crate::simplex::build_standard_form(lp);
let (a, b, c, row_scale, col_scale) = RuizScaler::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_env_var_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),
"off baseline must take DisabledOption; got {:?}", out_off);
unsafe { std::env::set_var("LP_CRASH_DUAL_ADV_DISABLE", "1"); }
let (r_on_disabled, _n_disabled, out_disabled) = invoke_big_m_with_option(&lp, true);
unsafe { std::env::remove_var("LP_CRASH_DUAL_ADV_DISABLE"); }
assert!(matches!(out_disabled, super::crash_probe::Outcome::DisabledEnv),
"env 抑止が hook で検知できない: {:?}", out_disabled);
assert_eq!(r_off.status, r_on_disabled.status,
"env disable で status drift: off={:?} disabled={:?}",
r_off.status, r_on_disabled.status);
assert_eq!(r_off.iterations, r_on_disabled.iterations,
"env disable で iter drift: off={} disabled={}",
r_off.iterations, r_on_disabled.iterations);
if r_off.status == SolveStatus::Optimal {
let obj_diff = (r_off.objective - r_on_disabled.objective).abs()
/ (1.0 + r_off.objective.abs());
assert!(obj_diff < 1e-9,
"env disable で obj drift: {:.3e}", obj_diff);
}
let (_r_on, n_on, out_on) = invoke_big_m_with_option(&lp, true);
assert!(matches!(out_on, super::crash_probe::Outcome::Adopted(_)),
"env 解除後 crash が adopt されない: {:?}", out_on);
assert!(n_on < n_off,
"env 解除後 crash が機能していない: off={} on={}", n_off, n_on);
}
#[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,
);
}
}