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");
}
}