use super::*;
use crate::error::SolverError;
use crate::problem::{ConstraintType, LpProblem};
use crate::sparse::CscMatrix;
#[allow(clippy::too_many_arguments)]
fn make_lp_general(
c: Vec<f64>,
rows: &[usize],
cols: &[usize],
vals: &[f64],
nrows: usize,
ncols: usize,
b: Vec<f64>,
cts: Vec<ConstraintType>,
bounds: Vec<(f64, f64)>,
) -> LpProblem {
let a = CscMatrix::from_triplets(rows, cols, vals, nrows, ncols).unwrap();
LpProblem::new_general(c, a, b, cts, bounds, None).unwrap()
}
fn make_lp(
c: Vec<f64>,
rows: &[usize],
cols: &[usize],
vals: &[f64],
nrows: usize,
ncols: usize,
b: Vec<f64>,
) -> LpProblem {
let n = c.len();
make_lp_general(
c,
rows,
cols,
vals,
nrows,
ncols,
b,
vec![ConstraintType::Le; nrows],
vec![(0.0, f64::INFINITY); n],
)
}
#[test]
fn test_fixed_variable_removal() {
let lp = make_lp_general(
vec![1.0, 1.0],
&[0, 0],
&[0, 1],
&[1.0, 1.0],
1,
2,
vec![5.0],
vec![ConstraintType::Le],
vec![(2.0, 2.0), (0.0, f64::INFINITY)],
);
let result = run_presolve(&lp, None).unwrap();
assert_eq!(result.reduced_problem.num_vars, 0);
assert_eq!(result.reduced_problem.num_constraints, 0);
assert!(result.was_reduced);
assert!((result.obj_offset - 2.0).abs() < 1e-10);
}
#[test]
fn test_fixed_infeasible() {
let a = CscMatrix::new(0, 1);
let res = LpProblem::new_general(vec![1.0], a, vec![], vec![], vec![(3.0, 2.0)], None);
assert!(
matches!(res, Err(SolverError::InvalidBounds { index: 0, lb, ub }) if lb == 3.0 && ub == 2.0),
"lb > ub must be rejected at construction"
);
}
#[test]
fn test_presolve_detects_lb_gt_ub() {
let mut lp = make_lp_general(
vec![1.0],
&[],
&[],
&[],
0,
1,
vec![],
vec![],
vec![(0.0, 1.0)],
);
lp.bounds[0] = (3.0, 2.0); assert!(
matches!(run_presolve(&lp, None), Err(PresolveStatus::Infeasible)),
"presolve must report Infeasible for lb > ub bounds"
);
}
#[test]
fn test_empty_row_feasible() {
let lp = make_lp_general(
vec![1.0],
&[1],
&[0],
&[1.0],
2,
1,
vec![5.0, 3.0],
vec![ConstraintType::Le, ConstraintType::Le],
vec![(0.0, f64::INFINITY)],
);
let result = run_presolve(&lp, None).unwrap();
assert_eq!(result.reduced_problem.num_constraints, 0);
}
#[test]
fn test_empty_row_infeasible() {
let lp = make_lp_general(
vec![1.0],
&[1],
&[0],
&[1.0],
2,
1,
vec![-1.0, 3.0],
vec![ConstraintType::Le, ConstraintType::Le],
vec![(0.0, f64::INFINITY)],
);
assert!(matches!(
run_presolve(&lp, None),
Err(PresolveStatus::Infeasible)
));
}
#[test]
fn test_empty_column_min_with_finite_lb() {
let lp = LpProblem::new_general(
vec![1.0, 1.0],
CscMatrix::new(0, 2),
vec![],
vec![],
vec![(0.0, f64::INFINITY), (1.0, f64::INFINITY)],
None,
)
.unwrap();
let result = run_presolve(&lp, None).unwrap();
assert_eq!(result.reduced_problem.num_vars, 0);
assert!((result.obj_offset - 1.0).abs() < 1e-10);
}
#[test]
fn test_empty_column_unbounded() {
let lp = LpProblem::new_general(
vec![-1.0],
CscMatrix::new(0, 1),
vec![],
vec![],
vec![(0.0, f64::INFINITY)],
None,
)
.unwrap();
assert!(matches!(
run_presolve(&lp, None),
Err(PresolveStatus::Unbounded)
));
}
#[test]
fn test_singleton_row_eq() {
let lp = make_lp_general(
vec![1.0, 1.0],
&[0, 1, 1],
&[0, 0, 1],
&[2.0, 1.0, 1.0],
2,
2,
vec![6.0, 10.0],
vec![ConstraintType::Eq, ConstraintType::Le],
vec![(0.0, f64::INFINITY), (0.0, f64::INFINITY)],
);
let result = run_presolve(&lp, None).unwrap();
assert_eq!(result.reduced_problem.num_vars, 0);
assert_eq!(result.reduced_problem.num_constraints, 0);
assert!((result.obj_offset - 3.0).abs() < 1e-10);
}
#[test]
fn test_singleton_row_infeasible() {
let lp = make_lp_general(
vec![1.0],
&[0],
&[0],
&[2.0],
1,
1,
vec![6.0],
vec![ConstraintType::Eq],
vec![(0.0, 1.0)],
);
assert!(matches!(
run_presolve(&lp, None),
Err(PresolveStatus::Infeasible)
));
}
#[test]
fn test_redundant_le() {
let lp = make_lp_general(
vec![1.0, 1.0],
&[0, 0, 1, 2],
&[0, 1, 0, 1],
&[1.0, 1.0, 1.0, 1.0],
3,
2,
vec![10.0, 3.0, 3.0],
vec![ConstraintType::Le, ConstraintType::Le, ConstraintType::Le],
vec![(0.0, 3.0), (0.0, 3.0)],
);
let result = run_presolve(&lp, None).unwrap();
assert_eq!(
result.reduced_problem.num_constraints, 0,
"all 3 constraints should be redundant"
);
assert_eq!(
result.reduced_problem.num_vars, 0,
"vars removed as empty cols after constraints gone"
);
let lp2 = make_lp_general(
vec![-1.0, -1.0],
&[0, 0],
&[0, 1],
&[1.0, 1.0],
1,
2,
vec![2.0],
vec![ConstraintType::Le],
vec![(0.0, 10.0), (0.0, 10.0)],
);
let result2 = run_presolve(&lp2, None).unwrap();
assert_eq!(
result2.reduced_problem.num_constraints, 1,
"x1+x2<=2 is not redundant"
);
}
#[test]
fn test_bounds_tightening() {
let lp = make_lp_general(
vec![-1.0, -1.0],
&[0, 0],
&[0, 1],
&[1.0, 1.0],
1,
2,
vec![5.0],
vec![ConstraintType::Le],
vec![(0.0, 10.0), (0.0, 10.0)],
);
let result = run_presolve(&lp, None).unwrap();
let _ = result.was_reduced;
assert_eq!(result.reduced_problem.num_vars, 2);
}
#[test]
fn test_bounds_tightening_negative_coeff_le_feasible() {
let lp = make_lp_general(
vec![1.0, 1.0],
&[0, 0],
&[0, 1],
&[1.0, -1.0],
1,
2,
vec![5.0],
vec![ConstraintType::Le],
vec![(0.0, 10.0), (0.0, 3.0)],
);
assert!(
run_presolve(&lp, None).is_ok(),
"x - y <= 5 should be feasible"
);
}
#[test]
fn test_bounds_tightening_negative_coeff_ge_feasible() {
let lp = make_lp_general(
vec![1.0, 1.0],
&[0, 0],
&[0, 1],
&[-1.0, 1.0],
1,
2,
vec![3.0],
vec![ConstraintType::Ge],
vec![(0.0, 5.0), (0.0, 8.0)],
);
assert!(
run_presolve(&lp, None).is_ok(),
"-x + y >= 3 should be feasible"
);
}
#[test]
fn test_presolve_no_crash_netlib_like() {
let lp = make_lp(
vec![-1.0, -1.0, -1.0],
&[0, 0, 0, 1, 2, 3],
&[0, 1, 2, 0, 1, 2],
&[1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
4,
3,
vec![4.0, 3.0, 3.0, 3.0],
);
let result = run_presolve(&lp, None).unwrap();
assert_eq!(result.reduced_problem.num_vars, 3);
assert_eq!(result.reduced_problem.num_constraints, 4);
}
#[test]
fn test_pre001_deadline_fires_immediately() {
let lp = make_lp_general(
vec![1.0, 1.0],
&[0, 0],
&[0, 1],
&[1.0, 1.0],
1,
2,
vec![5.0],
vec![ConstraintType::Le],
vec![(2.0, 2.0), (0.0, f64::INFINITY)],
);
let expired = std::time::Instant::now() - std::time::Duration::from_secs(1);
let result = run_presolve(&lp, Some(expired)).unwrap();
assert!(
!result.was_reduced,
"期限切れ deadline では early-exit し was_reduced=false を返すこと"
);
}
#[test]
fn presolve_doubleton_eq_basic() {
let lp = make_lp_general(
vec![1.0, 1.0, 1.0],
&[0, 0, 1, 1, 1],
&[0, 1, 0, 1, 2],
&[1.0, 1.0, 1.0, 1.0, 1.0],
2,
3,
vec![3.0, 10.0],
vec![ConstraintType::Eq, ConstraintType::Le],
vec![(0.0, 5.0), (0.0, 5.0), (0.0, f64::INFINITY)],
);
let result = run_presolve(&lp, None).unwrap();
assert!(result.was_reduced);
let has_subst = result
.postsolve_stack
.iter()
.any(|s| matches!(s, PostsolveStep::LinearSubstitution { .. }));
assert!(
has_subst,
"Doubleton equation should produce LinearSubstitution"
);
}
#[test]
fn presolve_doubleton_eq_solution_consistency() {
let lp = make_lp_general(
vec![1.0, 1.0],
&[0, 0],
&[0, 1],
&[1.0, 1.0],
1,
2,
vec![4.0],
vec![ConstraintType::Eq],
vec![(0.0, 3.0), (0.0, 3.0)],
);
let result = run_presolve(&lp, None).unwrap();
assert!((result.obj_offset - 4.0).abs() < 1e-10, "obj_offset = 4");
}
#[test]
fn presolve_doubleton_eq_infeasible() {
let lp = make_lp_general(
vec![1.0, 1.0],
&[0, 0],
&[0, 1],
&[1.0, 1.0],
1,
2,
vec![10.0],
vec![ConstraintType::Eq],
vec![(0.0, 3.0), (0.0, 3.0)],
);
let res = run_presolve(&lp, None);
assert!(matches!(res, Err(PresolveStatus::Infeasible)));
}
#[test]
fn presolve_free_var_subst_basic() {
let lp = make_lp_general(
vec![1.0, 1.0, 1.0],
&[0, 0, 0, 1, 1],
&[0, 1, 2, 0, 1],
&[1.0, 1.0, 1.0, 1.0, 1.0],
2,
3,
vec![5.0, 10.0],
vec![ConstraintType::Eq, ConstraintType::Le],
vec![(0.0, 10.0), (0.0, 10.0), (f64::NEG_INFINITY, f64::INFINITY)],
);
let result = run_presolve(&lp, None).unwrap();
assert!(result.was_reduced);
let has_subst = result
.postsolve_stack
.iter()
.any(|s| matches!(s, PostsolveStep::LinearSubstitution { .. }));
assert!(
has_subst,
"Free var substitution should produce LinearSubstitution"
);
assert!(
result.col_map[2].is_none(),
"z (col 2) should be eliminated"
);
}
#[test]
fn presolve_free_var_subst_multi_constraint() {
let lp = make_lp_general(
vec![1.0, 1.0, 1.0],
&[0, 0, 1, 1],
&[0, 2, 1, 2],
&[1.0, 1.0, 1.0, 1.0],
2,
3,
vec![4.0, 5.0],
vec![ConstraintType::Eq, ConstraintType::Eq],
vec![(0.0, 10.0), (0.0, 10.0), (f64::NEG_INFINITY, f64::INFINITY)],
);
let result = run_presolve(&lp, None).unwrap();
assert!(result.was_reduced);
assert!(result.col_map[2].is_none());
}
#[test]
fn presolve_doubleton_dual_recovery_eq_le() {
let lp = make_lp_general(
vec![1.0, 2.0],
&[0, 0, 1],
&[0, 1, 1],
&[1.0, 1.0, 1.0],
2,
2,
vec![6.0, 5.0],
vec![ConstraintType::Eq, ConstraintType::Le],
vec![(0.0, f64::INFINITY), (0.0, f64::INFINITY)],
);
let result = run_presolve(&lp, None).unwrap();
let lin = result.postsolve_stack.iter().find_map(|s| match s {
PostsolveStep::LinearSubstitution { c_orig, pivot, .. } => Some((*c_orig, *pivot)),
_ => None,
});
assert!(lin.is_some(), "LinearSubstitution expected");
let (c_orig, pivot) = lin.unwrap();
assert!((pivot - 1.0).abs() < 1e-12);
assert!(
(c_orig - 1.0).abs() < 1e-12,
"c_orig must capture pre-distribution c[x1]=1"
);
}
#[test]
fn presolve_free_singleton_col_basic() {
let lp = make_lp_general(
vec![1.0, 1.0, 1.0],
&[0, 0, 1, 1],
&[0, 1, 0, 2],
&[1.0, 1.0, 1.0, 1.0],
2,
3,
vec![3.0, 7.0],
vec![ConstraintType::Ge, ConstraintType::Eq],
vec![(0.0, 10.0), (0.0, 10.0), (f64::NEG_INFINITY, f64::INFINITY)],
);
let result = run_presolve(&lp, None).unwrap();
assert!(result.was_reduced);
assert!(result.col_map[2].is_none(), "z should be eliminated");
assert!(result.row_map[1].is_none(), "Eq row should be eliminated");
}
mod roundtrip_kkt {
use super::*;
use crate::test_kkt::assert_kkt_optimal;
#[test]
fn roundtrip_doubleton_eq_simple() {
let lp = make_lp_general(
vec![1.0, 1.0],
&[0, 0],
&[0, 1],
&[1.0, 1.0],
1,
2,
vec![4.0],
vec![ConstraintType::Eq],
vec![(0.0, 3.0), (0.0, 3.0)],
);
assert_kkt_optimal(&lp, 4.0, "roundtrip_doubleton_eq_simple");
}
#[test]
fn roundtrip_doubleton_eq_nonunit_coeffs() {
let lp = make_lp_general(
vec![1.0, 2.0],
&[0, 0],
&[0, 1],
&[2.0, 3.0],
1,
2,
vec![12.0],
vec![ConstraintType::Eq],
vec![(0.0, 4.0), (0.0, 4.0)],
);
assert_kkt_optimal(&lp, 20.0 / 3.0, "roundtrip_doubleton_eq_nonunit_coeffs");
}
#[test]
fn roundtrip_free_var_subst() {
let lp = make_lp_general(
vec![1.0, 1.0, 1.0],
&[0, 0, 0, 1, 1],
&[0, 1, 2, 0, 1],
&[1.0, 1.0, 1.0, 1.0, 1.0],
2,
3,
vec![5.0, 10.0],
vec![ConstraintType::Eq, ConstraintType::Le],
vec![(0.0, 10.0), (0.0, 10.0), (f64::NEG_INFINITY, f64::INFINITY)],
);
assert_kkt_optimal(&lp, 5.0, "roundtrip_free_var_subst");
}
#[test]
fn roundtrip_free_singleton_col() {
let lp = make_lp_general(
vec![1.0, 1.0, 1.0],
&[0, 0, 1, 1],
&[0, 1, 0, 2],
&[1.0, 1.0, 1.0, 1.0],
2,
3,
vec![3.0, 7.0],
vec![ConstraintType::Ge, ConstraintType::Eq],
vec![(0.0, 10.0), (0.0, 10.0), (f64::NEG_INFINITY, f64::INFINITY)],
);
assert_kkt_optimal(&lp, 7.0, "roundtrip_free_singleton_col");
}
#[test]
fn roundtrip_singleton_row_eq_with_doubleton() {
let lp = make_lp_general(
vec![1.0, 1.0, 1.0],
&[0, 1, 1],
&[0, 1, 2],
&[1.0, 1.0, 1.0],
2,
3,
vec![5.0, 4.0],
vec![ConstraintType::Eq, ConstraintType::Eq],
vec![(0.0, 10.0), (0.0, 3.0), (0.0, 3.0)],
);
assert_kkt_optimal(&lp, 9.0, "roundtrip_singleton_row_eq_with_doubleton");
}
#[test]
fn roundtrip_redundant_le_with_active_eq() {
let lp = make_lp_general(
vec![2.0, 1.0],
&[0, 0, 1, 1],
&[0, 1, 0, 1],
&[1.0, 1.0, 1.0, 1.0],
2,
2,
vec![100.0, 4.0],
vec![ConstraintType::Le, ConstraintType::Eq],
vec![(0.0, 3.0), (0.0, 3.0)],
);
assert_kkt_optimal(&lp, 5.0, "roundtrip_redundant_le_with_active_eq");
}
#[test]
fn roundtrip_mixed_transforms() {
let lp = make_lp_general(
vec![1.0, 1.0, 1.0, 1.0],
&[0, 0, 1, 1, 2, 2],
&[0, 1, 2, 3, 0, 2],
&[1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
3,
4,
vec![3.0, 2.0, 100.0],
vec![ConstraintType::Eq, ConstraintType::Eq, ConstraintType::Le],
vec![
(0.0, 2.0),
(0.0, 2.0),
(f64::NEG_INFINITY, f64::INFINITY),
(0.0, 5.0),
],
);
assert_kkt_optimal(&lp, 5.0, "roundtrip_mixed_transforms");
}
#[test]
fn roundtrip_ge_constraint_dual_sign() {
let lp = make_lp_general(
vec![1.0, 1.0],
&[0, 0],
&[0, 1],
&[1.0, 1.0],
1,
2,
vec![3.0],
vec![ConstraintType::Ge],
vec![(0.0, 5.0), (0.0, 5.0)],
);
assert_kkt_optimal(&lp, 3.0, "roundtrip_ge_constraint_dual_sign");
}
}
#[test]
fn test_step7_free_var_fillin_must_not_blow_up() {
use std::time::{Duration, Instant};
const N_HUBS: usize = 1500; const N_LOADS: usize = 9000; const MAX_PRESOLVE_SECS: f64 = 1.0; const SAFETY_DEADLINE_SECS: f64 = 8.0;
let n_hub = N_HUBS + 1;
let idx_g = n_hub; let idx_d0 = n_hub + 1; let ncols = n_hub + 1 + N_LOADS;
let nrows = N_HUBS + N_LOADS;
let mut rows: Vec<usize> = Vec::new();
let mut cols: Vec<usize> = Vec::new();
let mut vals: Vec<f64> = Vec::new();
for i in 0..N_HUBS {
rows.push(i);
cols.push(i);
vals.push(2.0);
rows.push(i);
cols.push(i + 1);
vals.push(-1.0);
rows.push(i);
cols.push(idx_g);
vals.push(-0.5);
}
for r in 0..N_LOADS {
let row = N_HUBS + r;
rows.push(row);
cols.push(0);
vals.push(1.0);
rows.push(row);
cols.push(idx_d0 + r);
vals.push(1.0);
}
let a = CscMatrix::from_triplets(&rows, &cols, &vals, nrows, ncols).unwrap();
let c = vec![0.0; ncols];
let mut b = vec![0.0; nrows];
let mut cts = vec![ConstraintType::Eq; nrows];
let mut bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); ncols]; bounds[idx_g] = (0.0, 1.0);
for r in 0..N_LOADS {
b[N_HUBS + r] = 2.0;
cts[N_HUBS + r] = ConstraintType::Le;
bounds[idx_d0 + r] = (0.0, 1.0);
}
let lp = LpProblem::new_general(c, a, b, cts, bounds, None).unwrap();
let deadline = Some(Instant::now() + Duration::from_secs_f64(SAFETY_DEADLINE_SECS));
let t0 = Instant::now();
let _ = run_presolve(&lp, deadline).expect("presolve must not error on this LP");
let elapsed = t0.elapsed().as_secs_f64();
assert!(
elapsed < MAX_PRESOLVE_SECS,
"step7 free-var substitution fill-in cascade: presolve took {:.3}s on a \
{}-var / {}-constraint LP (budget {:.1}s). The free-var elimination cost \
grows super-linearly as columns densify — see fill_in_exceeds_budget / \
eliminate_variable_via_eq_row in presolve/transforms.",
elapsed, ncols, nrows, MAX_PRESOLVE_SECS
);
}