use crate::presolve::activity::propagate_row_bounds;
use crate::problem::{ConstraintType, LpProblem};
use crate::sparse::CscMatrix;
use crate::tolerances::ZERO_TOL;
pub(crate) fn tighten_bounds_linear(
n: usize,
a: &CscMatrix,
b: &[f64],
constraint_types: &[ConstraintType],
bounds: &[(f64, f64)],
integer_mask: &[bool],
) -> Option<Vec<(f64, f64)>> {
let m = b.len();
let mut rows: Vec<Vec<(usize, f64)>> = vec![Vec::new(); m];
for j in 0..n {
for k in a.col_ptr[j]..a.col_ptr[j + 1] {
let row = a.row_ind[k];
let val = a.values[k];
if val.abs() >= ZERO_TOL {
rows[row].push((j, val));
}
}
}
let mut new_bounds = bounds.to_vec();
for i in 0..m {
let updates = propagate_row_bounds(
&rows[i],
&new_bounds,
constraint_types[i],
b[i],
Some(integer_mask),
)?;
for (j, new_lb, new_ub) in updates {
new_bounds[j] = (new_lb, new_ub);
}
}
Some(new_bounds)
}
pub(crate) fn tighten_integer_bounds(
lp: &LpProblem,
integer_mask: &[bool],
) -> Option<Vec<(f64, f64)>> {
tighten_bounds_linear(
lp.num_vars,
&lp.a,
&lp.b,
&lp.constraint_types,
&lp.bounds,
integer_mask,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::problem::{ConstraintType, LpProblem};
use crate::sparse::CscMatrix;
fn single_var_lp(a_val: f64, b_val: f64, ct: ConstraintType, domain: (f64, f64)) -> LpProblem {
let a = CscMatrix::from_triplets(&[0], &[0], &[a_val], 1, 1).unwrap();
LpProblem::new_general(vec![1.0], a, vec![b_val], vec![ct], vec![domain], None).unwrap()
}
#[test]
fn integer_ub_is_floored_from_le_constraint() {
let lp = single_var_lp(1.0, 3.7, ConstraintType::Le, (0.0, 10.0));
let bounds = tighten_integer_bounds(&lp, &[true]).expect("feasible");
assert!(
(bounds[0].1 - 3.0).abs() < 1e-9,
"integer ub from x ≤ 3.7 must be floor(3.7)=3, got {}",
bounds[0].1
);
}
#[test]
fn integer_lb_is_ceiled_from_ge_constraint() {
let lp = single_var_lp(1.0, 2.3, ConstraintType::Ge, (0.0, 10.0));
let bounds = tighten_integer_bounds(&lp, &[true]).expect("feasible");
assert!(
(bounds[0].0 - 3.0).abs() < 1e-9,
"integer lb from x ≥ 2.3 must be ceil(2.3)=3, got {}",
bounds[0].0
);
}
#[test]
fn infeasibility_detected_by_integer_rounding() {
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![3.7, 3.5],
vec![ConstraintType::Le, ConstraintType::Ge],
vec![(0.0, 10.0)],
None,
)
.unwrap();
let result = tighten_integer_bounds(&lp, &[true]);
assert!(
result.is_none(),
"x ≤ 3.7 ∧ x ≥ 3.5 integer → empty domain → None"
);
}
#[test]
fn continuous_var_not_rounded() {
let lp = single_var_lp(1.0, 3.7, ConstraintType::Le, (0.0, 10.0));
let bounds = tighten_integer_bounds(&lp, &[false]).expect("feasible");
assert!(
(bounds[0].1 - 3.7).abs() < 1e-9,
"continuous var ub must stay 3.7, got {}",
bounds[0].1
);
}
#[test]
fn no_constraints_unchanged() {
let a = CscMatrix::new(0, 2);
let lp =
LpProblem::new_general(vec![1.0, 1.0], a, vec![], vec![], vec![(0.0, 5.0); 2], None)
.unwrap();
let bounds = tighten_integer_bounds(&lp, &[true, true]).expect("feasible");
assert_eq!(bounds, vec![(0.0, 5.0), (0.0, 5.0)]);
}
#[test]
fn two_var_le_tightens_both_ubs() {
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::Le],
vec![(0.0, 10.0), (0.0, 10.0)],
None,
)
.unwrap();
let bounds = tighten_integer_bounds(&lp, &[true, true]).expect("feasible");
assert!(
(bounds[0].1 - 5.0).abs() < 1e-9,
"x ub must be 5, got {}",
bounds[0].1
);
assert!(
(bounds[1].1 - 5.0).abs() < 1e-9,
"y ub must be 5, got {}",
bounds[1].1
);
}
#[test]
fn eq_constraint_fixes_integer_var() {
let lp = single_var_lp(1.0, 4.0, ConstraintType::Eq, (0.0, 10.0));
let bounds = tighten_integer_bounds(&lp, &[true]).expect("feasible");
assert!((bounds[0].0 - 4.0).abs() < 1e-9, "lb={}", bounds[0].0);
assert!((bounds[0].1 - 4.0).abs() < 1e-9, "ub={}", bounds[0].1);
}
#[test]
fn negative_coeff_le_tightens_lb() {
let lp = single_var_lp(-1.0, -2.3, ConstraintType::Le, (0.0, 10.0));
let bounds = tighten_integer_bounds(&lp, &[true]).expect("feasible");
assert!(
(bounds[0].0 - 3.0).abs() < 1e-9,
"integer lb from -x ≤ -2.3 must be ceil(2.3)=3, got {}",
bounds[0].0
);
}
#[test]
fn tolerance_aware_floor_retains_boundary_integer() {
let lp = single_var_lp(0.1, 0.3, ConstraintType::Le, (0.0, 5.0));
let bounds =
tighten_integer_bounds(&lp, &[true]).expect("feasible: x=3 satisfies 0.1*3=0.3");
assert!(
(bounds[0].1 - 3.0).abs() < 1e-9,
"integer ub from 0.1*x<=0.3 must be 3 (not 2 from raw floor), got {}",
bounds[0].1
);
}
#[test]
fn finite_lower_bound_propagates_with_infinite_upper_bound() {
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::Le],
vec![(0.0, 10.0), (0.0, f64::INFINITY)],
None,
)
.unwrap();
let bounds = tighten_integer_bounds(&lp, &[true, false]).expect("feasible");
assert!((bounds[0].1 - 5.0).abs() < 1e-9, "x ub: {}", bounds[0].1);
assert!((bounds[1].1 - 5.0).abs() < 1e-9, "y ub: {}", bounds[1].1);
}
#[test]
fn eq_non_integer_rhs_integer_var_crossed_bounds_is_infeasible() {
let lp = single_var_lp(1.0, 3.5, ConstraintType::Eq, (0.0, 10.0));
let result = tighten_integer_bounds(&lp, &[true]);
assert!(
result.is_none(),
"x=3.5 integer → floor(3.5)=3 < ceil(3.5)=4 → infeasible"
);
}
#[test]
fn infinite_upper_skips_ge_propagation() {
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::Ge],
vec![(0.0, 10.0), (0.0, f64::INFINITY)],
None,
)
.unwrap();
let bounds = tighten_integer_bounds(&lp, &[true, false]).expect("feasible");
assert_eq!(
bounds[0].0, 0.0,
"x lb must stay 0 (Ge propagation skipped, y_ub=∞)"
);
}
}