use super::bounds::step5_bounds_tightening;
use super::doubleton::step6_doubleton_equation;
use super::empty_redundant::{step3a_empty_row, step3b_empty_column, step4_redundant_constraint};
use super::fixed::step1_fixed_variable;
use super::free::{step7_free_var_substitution, step8_free_singleton_col};
use super::singleton::step2_singleton_row;
use super::state::{PostsolveStep, PresolveState, PresolveStatus};
use super::substitution::fill_in_exceeds_budget;
use crate::problem::{ConstraintType, LpProblem};
use crate::sparse::CscMatrix;
fn make_state(
c: Vec<f64>,
rows: &[usize],
cols: &[usize],
vals: &[f64],
nrows: usize,
ncols: usize,
b: Vec<f64>,
cts: Vec<ConstraintType>,
bounds: Vec<(f64, f64)>,
) -> PresolveState {
let a = CscMatrix::from_triplets(rows, cols, vals, nrows, ncols).unwrap();
let lp = LpProblem::new_general(c, a, b, cts, bounds, None).unwrap();
PresolveState::from_problem(&lp)
}
fn count_bounds_tightened(st: &PresolveState) -> usize {
st.postsolve_stack
.iter()
.filter(|s| matches!(s, PostsolveStep::BoundsTightened))
.count()
}
fn count_linear_subst(st: &PresolveState) -> usize {
st.postsolve_stack
.iter()
.filter(|s| matches!(s, PostsolveStep::LinearSubstitution { .. }))
.count()
}
#[test]
fn step5_le_positive_coeff_tightens_ub() {
let mut st = make_state(
vec![0.0, 0.0],
&[0, 0],
&[0, 1],
&[1.0, 2.0],
1,
2,
vec![4.0],
vec![ConstraintType::Le],
vec![(0.0, 1.0), (0.0, 10.0)],
);
let mut fixed = 0usize;
step5_bounds_tightening(&mut st, &mut fixed, None).unwrap();
let (lb_y, ub_y) = st.bounds[1];
assert_eq!(lb_y, 0.0);
assert!(
(ub_y - 2.0).abs() < 1e-10,
"y_ub should tighten to 2, got {ub_y}"
);
assert!(count_bounds_tightened(&st) >= 1);
}
#[test]
fn step5_ge_positive_coeff_tightens_lb() {
let mut st = make_state(
vec![0.0, 0.0],
&[0, 0],
&[0, 1],
&[2.0, 1.0],
1,
2,
vec![6.0],
vec![ConstraintType::Ge],
vec![(0.0, 2.0), (0.0, 5.0)],
);
let mut fixed = 0usize;
step5_bounds_tightening(&mut st, &mut fixed, None).unwrap();
let (lb_x, _) = st.bounds[0];
assert!(
(lb_x - 0.5).abs() < 1e-10,
"x_lb should tighten to 0.5, got {lb_x}"
);
assert!(count_bounds_tightened(&st) >= 1);
}
#[test]
fn step5_le_infeasible_negative_rhs() {
let mut st = make_state(
vec![0.0, 0.0],
&[0, 0],
&[0, 1],
&[1.0, 1.0],
1,
2,
vec![-1.0],
vec![ConstraintType::Le],
vec![(0.0, 5.0), (0.0, 5.0)],
);
let mut fixed = 0usize;
let res = step5_bounds_tightening(&mut st, &mut fixed, None);
assert_eq!(res, Err(PresolveStatus::Infeasible));
}
#[test]
fn step5_eq_tightens_finite_ub() {
let mut st = make_state(
vec![0.0, 0.0],
&[0, 0],
&[0, 1],
&[1.0, 1.0],
1,
2,
vec![3.0],
vec![ConstraintType::Eq],
vec![(0.0, 10.0), (0.0, 10.0)],
);
let mut fixed = 0usize;
step5_bounds_tightening(&mut st, &mut fixed, None).unwrap();
assert!((st.bounds[0].1 - 3.0).abs() < 1e-10);
assert!((st.bounds[1].1 - 3.0).abs() < 1e-10);
assert!(count_bounds_tightened(&st) >= 2);
}
#[test]
fn step6_basic_eliminates_one_var() {
let mut st = make_state(
vec![1.0, 1.0],
&[0, 0],
&[0, 1],
&[1.0, 1.0],
1,
2,
vec![5.0],
vec![ConstraintType::Eq],
vec![(0.0, 10.0), (0.0, 10.0)],
);
let mut subst = 0usize;
step6_doubleton_equation(&mut st, &mut subst, None).unwrap();
assert_eq!(subst, 1);
assert!(st.removed_cols[0]);
assert!(st.removed_rows[0]);
assert_eq!(count_linear_subst(&st), 1);
}
#[test]
fn step6_prefers_free_pivot() {
let mut st = make_state(
vec![1.0, 1.0],
&[0, 0],
&[0, 1],
&[2.0, 3.0],
1,
2,
vec![6.0],
vec![ConstraintType::Eq],
vec![(0.0, 10.0), (f64::NEG_INFINITY, f64::INFINITY)],
);
let mut subst = 0usize;
step6_doubleton_equation(&mut st, &mut subst, None).unwrap();
assert_eq!(subst, 1);
assert!(
st.removed_cols[1],
"free col y should be pivot, not bounded x"
);
assert!(!st.removed_cols[0]);
}
#[test]
fn step6_infeasible_when_doubleton_impossible() {
let mut st = make_state(
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 mut subst = 0usize;
let res = step6_doubleton_equation(&mut st, &mut subst, None);
assert_eq!(res, Err(PresolveStatus::Infeasible));
}
#[test]
fn step7_eliminates_free_var_with_eq_row() {
let mut st = make_state(
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 mut subst = 0usize;
step7_free_var_substitution(&mut st, &mut subst, None).unwrap();
assert_eq!(subst, 1);
assert!(st.removed_cols[2]);
assert!(st.removed_rows[0]);
assert_eq!(count_linear_subst(&st), 1);
}
#[test]
fn step7_skips_free_var_when_no_eq_row() {
let mut st = make_state(
vec![1.0, 1.0, 1.0],
&[0, 0, 0],
&[0, 1, 2],
&[1.0, 1.0, 1.0],
1,
3,
vec![5.0],
vec![ConstraintType::Le],
vec![(0.0, 10.0), (0.0, 10.0), (f64::NEG_INFINITY, f64::INFINITY)],
);
let mut subst = 0usize;
step7_free_var_substitution(&mut st, &mut subst, None).unwrap();
assert_eq!(subst, 0, "no Eq row → free var stays");
assert!(!st.removed_cols[2]);
}
#[test]
fn step7_picks_largest_magnitude_pivot() {
let mut st = make_state(
vec![0.0, 0.0, 0.0],
&[0, 0, 1, 1],
&[0, 2, 1, 2],
&[1.0, 1.0, 1.0, 3.0],
2,
3,
vec![4.0, 12.0],
vec![ConstraintType::Eq, ConstraintType::Eq],
vec![(0.0, 10.0), (0.0, 10.0), (f64::NEG_INFINITY, f64::INFINITY)],
);
let mut subst = 0usize;
step7_free_var_substitution(&mut st, &mut subst, None).unwrap();
assert_eq!(subst, 1);
let pivot_mag = st.postsolve_stack.iter().find_map(|s| match s {
PostsolveStep::LinearSubstitution {
pivot, orig_col, ..
} if *orig_col == 2 => Some(pivot.abs()),
_ => None,
});
assert!(pivot_mag.is_some(), "free col z should be eliminated");
assert!(
(pivot_mag.unwrap() - 3.0).abs() < 1e-10,
"should pick |3| over |1|"
);
}
#[test]
fn step7_respects_expired_deadline() {
let mut st_live = make_state(
vec![0.0, 0.0],
&[0, 0],
&[0, 1],
&[2.0, 1.0],
1,
2,
vec![4.0],
vec![ConstraintType::Eq],
vec![(f64::NEG_INFINITY, f64::INFINITY), (0.0, 10.0)],
);
let mut subst_live = 0usize;
step7_free_var_substitution(
&mut st_live,
&mut subst_live,
Some(std::time::Instant::now() + std::time::Duration::from_millis(200)),
)
.unwrap();
assert_eq!(subst_live, 1, "live deadline should allow substitution");
let mut st_expired = make_state(
vec![0.0, 0.0],
&[0, 0],
&[0, 1],
&[2.0, 1.0],
1,
2,
vec![4.0],
vec![ConstraintType::Eq],
vec![(f64::NEG_INFINITY, f64::INFINITY), (0.0, 10.0)],
);
let mut subst_expired = 0usize;
step7_free_var_substitution(
&mut st_expired,
&mut subst_expired,
Some(std::time::Instant::now() - std::time::Duration::from_millis(1)),
)
.unwrap();
assert_eq!(subst_expired, 0);
assert!(
!st_expired.removed_cols[0] && !st_expired.removed_rows[0],
"expired deadline must short-circuit before elimination",
);
}
#[test]
fn step8_eliminates_free_singleton_eq() {
let mut st = make_state(
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 mut subst = 0usize;
step8_free_singleton_col(&mut st, &mut subst, None).unwrap();
assert_eq!(subst, 1);
assert!(st.removed_cols[2]);
assert!(st.removed_rows[1]);
}
#[test]
fn step8_skips_free_singleton_in_le_row() {
let mut st = make_state(
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::Le],
vec![(0.0, 10.0), (0.0, 10.0), (f64::NEG_INFINITY, f64::INFINITY)],
);
let mut subst = 0usize;
step8_free_singleton_col(&mut st, &mut subst, None).unwrap();
assert_eq!(subst, 0);
assert!(!st.removed_cols[2]);
}
#[test]
fn step8_skips_non_free_singleton() {
let mut st = make_state(
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), (0.0, 5.0)],
);
let mut subst = 0usize;
step8_free_singleton_col(&mut st, &mut subst, None).unwrap();
assert_eq!(subst, 0);
assert!(!st.removed_cols[2]);
}
#[test]
fn fill_in_budget_allows_dense_pivot_with_few_targets() {
let st = make_state(
vec![0.0; 3],
&[0, 0, 0, 1],
&[0, 1, 2, 0],
&[1.0, 1.0, 1.0, 1.0],
2,
3,
vec![2.0, 5.0],
vec![ConstraintType::Eq, ConstraintType::Le],
vec![(0.0, 10.0); 3],
);
assert!(
!fill_in_exceeds_budget(&st, 0, 0),
"low fill-in must not be skipped"
);
}
#[test]
fn fill_in_budget_blocks_high_fill() {
let n_extra = 7usize; let n_rows_extra = 7usize; let n_cols = 1 + n_extra + n_rows_extra;
let mut rows = Vec::new();
let mut cols = Vec::new();
let mut vals = Vec::new();
for j in 0..=n_extra {
rows.push(0);
cols.push(j);
vals.push(1.0);
}
for r in 1..=n_rows_extra {
rows.push(r);
cols.push(0);
vals.push(1.0);
rows.push(r);
cols.push(n_extra + r);
vals.push(1.0);
}
let m = 1 + n_rows_extra;
let mut cts = vec![ConstraintType::Eq];
cts.extend(std::iter::repeat_n(ConstraintType::Le, n_rows_extra));
let mut b = vec![8.0];
b.extend((1..=n_rows_extra).map(|k| k as f64));
let bounds = vec![(0.0, 10.0); n_cols];
let st = make_state(
vec![0.0; n_cols],
&rows,
&cols,
&vals,
m,
n_cols,
b,
cts,
bounds,
);
assert!(
fill_in_exceeds_budget(&st, 0, 0),
"dense disjoint fill should exceed budget"
);
}
#[test]
fn step1_fixed_negative_value_updates_b_and_offset() {
let mut st = make_state(
vec![2.0, 0.0],
&[0, 1, 1],
&[0, 0, 1],
&[1.0, 4.0, 1.0],
2,
2,
vec![10.0, 20.0],
vec![ConstraintType::Le, ConstraintType::Le],
vec![(-3.0, -3.0), (0.0, 10.0)],
);
step1_fixed_variable(&mut st, None).unwrap();
assert!(st.removed_cols[0], "fixed col must be removed");
assert!((st.b[0] - 13.0).abs() < 1e-12, "b0 expected 13, got {}", st.b[0]);
assert!((st.b[1] - 32.0).abs() < 1e-12, "b1 expected 32, got {}", st.b[1]);
assert!(
(st.obj_offset - (-6.0)).abs() < 1e-12,
"offset expected -6, got {}",
st.obj_offset
);
}
#[test]
fn step1_detects_lb_gt_ub() {
let mut st = make_state(
vec![1.0],
&[],
&[],
&[],
0,
1,
vec![],
vec![],
vec![(0.0, 1.0)],
);
st.bounds[0] = (3.0, 2.0);
assert_eq!(
step1_fixed_variable(&mut st, None),
Err(PresolveStatus::Infeasible)
);
}
#[test]
fn step2_singleton_negative_coeff_solves_negative_value() {
let mut st = make_state(
vec![1.0, 0.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![(-5.0, 0.0), (0.0, 10.0)],
);
step2_singleton_row(&mut st, None).unwrap();
assert!(st.removed_cols[0] && st.removed_rows[0]);
assert!((st.b[1] - 13.0).abs() < 1e-12, "b1 expected 13, got {}", st.b[1]);
assert!(
(st.obj_offset - (-3.0)).abs() < 1e-12,
"offset expected -3, got {}",
st.obj_offset
);
}
#[test]
fn step2_skips_singleton_le_row() {
let mut st = make_state(
vec![1.0],
&[0],
&[0],
&[2.0],
1,
1,
vec![6.0],
vec![ConstraintType::Le],
vec![(0.0, 10.0)],
);
step2_singleton_row(&mut st, None).unwrap();
assert!(!st.removed_rows[0], "Le singleton row must remain");
assert!(!st.removed_cols[0]);
}
#[test]
fn step2_singleton_infeasible_out_of_bounds() {
let mut st = make_state(
vec![1.0],
&[0],
&[0],
&[2.0],
1,
1,
vec![6.0],
vec![ConstraintType::Eq],
vec![(0.0, 1.0)],
);
assert_eq!(
step2_singleton_row(&mut st, None),
Err(PresolveStatus::Infeasible)
);
}
fn empty_second_row_state(ct1: ConstraintType, b1: f64) -> PresolveState {
make_state(
vec![1.0],
&[0],
&[0],
&[1.0],
2,
1,
vec![5.0, b1],
vec![ConstraintType::Le, ct1],
vec![(0.0, f64::INFINITY)],
)
}
#[test]
fn step3a_empty_eq_row_nonzero_rhs_infeasible() {
let mut st = empty_second_row_state(ConstraintType::Eq, 5.0);
assert_eq!(
step3a_empty_row(&mut st, None),
Err(PresolveStatus::Infeasible)
);
}
#[test]
fn step3a_empty_eq_row_zero_rhs_feasible() {
let mut st = empty_second_row_state(ConstraintType::Eq, 0.0);
step3a_empty_row(&mut st, None).unwrap();
assert!(st.removed_rows[1], "empty 0==0 row must be removed");
}
#[test]
fn step3a_empty_ge_row_positive_rhs_infeasible() {
let mut st = empty_second_row_state(ConstraintType::Ge, 5.0);
assert_eq!(
step3a_empty_row(&mut st, None),
Err(PresolveStatus::Infeasible)
);
}
#[test]
fn step3a_empty_ge_row_nonpositive_rhs_feasible() {
let mut st = empty_second_row_state(ConstraintType::Ge, -2.0);
step3a_empty_row(&mut st, None).unwrap();
assert!(st.removed_rows[1]);
}
fn empty_second_col_state(c1: f64, bounds1: (f64, f64)) -> PresolveState {
make_state(
vec![0.0, c1],
&[0],
&[0],
&[1.0],
1,
2,
vec![5.0],
vec![ConstraintType::Le],
vec![(0.0, 10.0), bounds1],
)
}
#[test]
fn step3b_empty_col_positive_cost_no_lower_bound_unbounded() {
let mut st = empty_second_col_state(1.0, (f64::NEG_INFINITY, 10.0));
assert_eq!(
step3b_empty_column(&mut st, None),
Err(PresolveStatus::Unbounded)
);
}
#[test]
fn step3b_empty_col_negative_cost_fixes_at_upper_bound() {
let mut st = empty_second_col_state(-2.0, (0.0, 5.0));
step3b_empty_column(&mut st, None).unwrap();
assert!(st.removed_cols[1]);
assert!(
(st.obj_offset - (-10.0)).abs() < 1e-12,
"offset expected -10, got {}",
st.obj_offset
);
}
#[test]
fn step3b_empty_col_zero_cost_finite_lb_fixes_at_lower_bound() {
let mut st = empty_second_col_state(0.0, (3.0, 10.0));
step3b_empty_column(&mut st, None).unwrap();
assert!(st.removed_cols[1]);
assert!(st.obj_offset.abs() < 1e-12, "zero-cost col adds no offset");
let value = st.postsolve_stack.iter().find_map(|s| match s {
PostsolveStep::EmptyColumn { orig_col: 1, value } => Some(*value),
_ => None,
});
assert!(
value.is_some_and(|v| (v - 3.0).abs() < 1e-12),
"empty col value must default to finite lb=3"
);
}
#[test]
fn step3b_empty_col_zero_cost_free_fixes_at_zero() {
let mut st = empty_second_col_state(0.0, (f64::NEG_INFINITY, f64::INFINITY));
step3b_empty_column(&mut st, None).unwrap();
assert!(st.removed_cols[1]);
let value = st.postsolve_stack.iter().find_map(|s| match s {
PostsolveStep::EmptyColumn { orig_col: 1, value } => Some(*value),
_ => None,
});
assert!(
value.is_some_and(|v| v.abs() < 1e-12),
"free zero-cost col value must default to 0"
);
}
#[test]
fn step4_ge_constraint_redundant_when_activity_floor_dominates() {
let mut st = make_state(
vec![0.0, 0.0],
&[0, 0],
&[0, 1],
&[1.0, 1.0],
1,
2,
vec![1.0],
vec![ConstraintType::Ge],
vec![(1.0, 5.0), (1.0, 5.0)],
);
step4_redundant_constraint(&mut st, None).unwrap();
assert!(st.removed_rows[0], "Ge with activity floor >= rhs must be redundant");
}
#[test]
fn step4_ge_constraint_not_redundant_when_floor_below_rhs() {
let mut st = make_state(
vec![0.0, 0.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)],
);
step4_redundant_constraint(&mut st, None).unwrap();
assert!(!st.removed_rows[0]);
}
#[test]
fn step4_eq_constraint_redundant_when_activity_pins_rhs() {
let mut st = make_state(
vec![0.0, 0.0],
&[0, 0],
&[0, 1],
&[1.0, 1.0],
1,
2,
vec![4.0],
vec![ConstraintType::Eq],
vec![(2.0, 2.0), (2.0, 2.0)],
);
step4_redundant_constraint(&mut st, None).unwrap();
assert!(st.removed_rows[0], "Eq pinned to rhs by fixed vars must be redundant");
}
#[test]
fn step5_tightening_to_point_increments_new_fixed() {
let mut st = make_state(
vec![0.0],
&[0],
&[0],
&[1.0],
1,
1,
vec![0.0],
vec![ConstraintType::Le],
vec![(0.0, 10.0)],
);
let mut fixed = 0usize;
step5_bounds_tightening(&mut st, &mut fixed, None).unwrap();
assert_eq!(fixed, 1, "tightening that pins lb==ub must count as a new fix");
let (lb, ub) = st.bounds[0];
assert!(lb.abs() < 1e-12 && ub.abs() < 1e-12, "bounds collapsed to [0,0]");
}
#[test]
fn step6_opposite_sign_coeffs_tightens_other_bound() {
let mut st = make_state(
vec![1.0, 1.0],
&[0, 0],
&[0, 1],
&[1.0, -1.0],
1,
2,
vec![2.0],
vec![ConstraintType::Eq],
vec![(0.0, 10.0), (0.0, 10.0)],
);
let mut subst = 0usize;
step6_doubleton_equation(&mut st, &mut subst, None).unwrap();
assert_eq!(subst, 1);
assert!(st.removed_cols[0] && st.removed_rows[0], "pivot x0 eliminated");
let (lb1, ub1) = st.bounds[1];
assert!(lb1.abs() < 1e-12, "x1 lb stays 0, got {lb1}");
assert!((ub1 - 8.0).abs() < 1e-12, "x1 ub tightened to 8, got {ub1}");
assert!(count_linear_subst(&st) >= 1);
assert!(count_bounds_tightened(&st) >= 1, "ratio<0 must tighten other bound");
}