use super::*;
use crate::options::{MipConfig, SolverOptions};
use crate::problem::ConstraintType;
use crate::sparse::CscMatrix;
fn lp(
c: Vec<f64>,
rows: &[usize],
cols: &[usize],
vals: &[f64],
m: usize,
b: Vec<f64>,
ct: Vec<ConstraintType>,
bounds: Vec<(f64, f64)>,
) -> LpProblem {
let n = c.len();
let a = if m == 0 {
CscMatrix::new(0, n)
} else {
CscMatrix::from_triplets(rows, cols, vals, m, n).unwrap()
};
LpProblem::new_general(c, a, b, ct, bounds, None).unwrap()
}
fn cuts_cfg(rounds: usize) -> MipConfig {
MipConfig {
cuts: true,
max_cut_rounds: rounds,
..MipConfig::default()
}
}
fn enumerate_int_box(bounds: &[(f64, f64)]) -> Vec<Vec<f64>> {
let mut pts = vec![vec![]];
for &(lo, hi) in bounds {
let lo = lo.ceil() as i64;
let hi = hi.floor() as i64;
let mut next = Vec::new();
for p in &pts {
for v in lo..=hi {
let mut q = p.clone();
q.push(v as f64);
next.push(q);
}
}
pts = next;
}
pts
}
fn feasible_orig(p: &LpProblem, x: &[f64]) -> bool {
let tol = 1e-7;
for (j, &(lo, hi)) in p.bounds.iter().enumerate() {
if x[j] < lo - tol || x[j] > hi + tol {
return false;
}
}
let ax = p.a.mat_vec_mul(x).unwrap();
for i in 0..p.num_constraints {
let ok = match p.constraint_types[i] {
ConstraintType::Le => ax[i] <= p.b[i] + tol,
ConstraintType::Ge => ax[i] >= p.b[i] - tol,
ConstraintType::Eq => (ax[i] - p.b[i]).abs() <= tol,
};
if !ok {
return false;
}
}
true
}
fn lp_root(p: &LpProblem) -> crate::problem::SolverResult {
super::solve_cut_lp(p, &SolverOptions::default(), None)
}
fn p_box_le() -> MilpProblem {
let l = lp(
vec![-1.0, -1.0],
&[0, 0],
&[0, 1],
&[2.0, 2.0],
1,
vec![3.0],
vec![ConstraintType::Le],
vec![(0.0, 1.0), (0.0, 1.0)],
);
MilpProblem::new(l, vec![0, 1]).unwrap()
}
fn p_box_ge() -> MilpProblem {
let l = lp(
vec![1.0, 1.0],
&[0, 0],
&[0, 1],
&[2.0, 2.0],
1,
vec![3.0],
vec![ConstraintType::Ge],
vec![(0.0, 3.0), (0.0, 3.0)],
);
MilpProblem::new(l, vec![0, 1]).unwrap()
}
fn p_two_le() -> MilpProblem {
let l = lp(
vec![-1.0, -2.0],
&[0, 1, 1],
&[1, 0, 1],
&[2.0, 1.0, 1.0],
2,
vec![3.0, 3.0],
vec![ConstraintType::Le, ConstraintType::Le],
vec![(0.0, 3.0), (0.0, 3.0)],
);
MilpProblem::new(l, vec![0, 1]).unwrap()
}
fn p_lb_only() -> MilpProblem {
let l = lp(
vec![-1.0, -1.0, -1.0],
&[0, 0, 0],
&[0, 1, 2],
&[3.0, 2.0, 4.0],
1,
vec![7.0],
vec![ConstraintType::Le],
vec![(0.0, 3.0), (0.0, 3.0), (0.0, 3.0)],
);
MilpProblem::new(l, vec![0, 1, 2]).unwrap()
}
fn p_lb_only_inf() -> MilpProblem {
let l = lp(
vec![-1.0, -1.0, -1.0],
&[0, 0, 0],
&[0, 1, 2],
&[3.0, 2.0, 4.0],
1,
vec![7.0],
vec![ConstraintType::Le],
vec![(0.0, f64::INFINITY), (0.0, f64::INFINITY), (0.0, f64::INFINITY)],
);
MilpProblem::new(l, vec![0, 1, 2]).unwrap()
}
fn all_problems() -> Vec<(&'static str, MilpProblem)> {
vec![
("box_le", p_box_le()),
("box_ge", p_box_ge()),
("two_le", p_two_le()),
("lb_only", p_lb_only()),
]
}
#[test]
fn cut_validity_brute_force() {
for (name, milp) in all_problems() {
for rounds in [1usize, 5] {
let out = add_root_cuts(&milp, &SolverOptions::default(), &cuts_cfg(rounds));
let m_old = milp.lp.num_constraints;
let m_new = out.lp.num_constraints;
assert!(
m_new >= m_old,
"{name}: cuts must not drop rows ({m_old}->{m_new})"
);
let pts = enumerate_int_box(&milp.lp.bounds);
for x in &pts {
if !feasible_orig(&milp.lp, x) {
continue;
}
let ax = out.lp.a.mat_vec_mul(x).unwrap();
for i in m_old..m_new {
assert_eq!(out.lp.constraint_types[i], ConstraintType::Ge);
assert!(
ax[i] >= out.lp.b[i] - 1e-6,
"{name} round={rounds}: INVALID CUT — integer point {x:?} \
removed by cut row {i}: {} < {}",
ax[i],
out.lp.b[i]
);
}
}
}
}
}
#[test]
fn cut_validity_ub_only_var() {
let l = lp(
vec![-1.0],
&[0],
&[0],
&[2.0],
1,
vec![3.0],
vec![ConstraintType::Le],
vec![(f64::NEG_INFINITY, 2.0)],
);
let milp = MilpProblem::new(l, vec![0]).unwrap();
let out = add_root_cuts(&milp, &SolverOptions::default(), &cuts_cfg(3));
let m_old = milp.lp.num_constraints;
let m_new = out.lp.num_constraints;
assert!(m_new > m_old, "a GMI cut must be generated for the UbOnly source");
for xi in -8..=2 {
let x = vec![xi as f64];
if !feasible_orig(&milp.lp, &x) {
continue;
}
let ax = out.lp.a.mat_vec_mul(&x).unwrap();
for i in m_old..m_new {
assert!(
ax[i] >= out.lp.b[i] - 1e-6,
"INVALID CUT (UbOnly): integer x={xi} removed by cut row {i}: {} < {}",
ax[i],
out.lp.b[i]
);
}
}
}
#[test]
fn cuts_are_generated_and_cut_lp_optimum() {
for (name, milp) in all_problems() {
let root = lp_root(&milp.lp);
assert_eq!(root.status, SolveStatus::Optimal, "{name}: root must solve");
let x_star = &root.solution;
let out = add_root_cuts(&milp, &SolverOptions::default(), &cuts_cfg(1));
let m_old = milp.lp.num_constraints;
let m_new = out.lp.num_constraints;
assert!(
m_new > m_old,
"{name}: at least one GMI cut must be generated (root LP is fractional)"
);
let ax = out.lp.a.mat_vec_mul(x_star).unwrap();
let any_violated = (m_old..m_new).any(|i| ax[i] < out.lp.b[i] - 1e-6);
assert!(
any_violated,
"{name}: a generated cut must violate the fractional LP optimum {x_star:?}"
);
}
}
#[test]
fn cuts_tighten_lp_bound_without_crossing_integer_optimum() {
for (name, milp) in all_problems() {
let root = lp_root(&milp.lp);
let out = add_root_cuts(&milp, &SolverOptions::default(), &cuts_cfg(5));
let cut_root = lp_root(&out.lp);
assert_eq!(cut_root.status, SolveStatus::Optimal, "{name}");
assert!(
cut_root.objective >= root.objective - 1e-6,
"{name}: cut LP bound {} must not be looser than root {}",
cut_root.objective,
root.objective
);
let int_opt = brute_force_min(&milp);
if let Some(opt) = int_opt {
assert!(
cut_root.objective <= opt + 1e-6,
"{name}: cut LP bound {} must stay <= integer optimum {}",
cut_root.objective,
opt
);
}
}
}
fn brute_force_min(milp: &MilpProblem) -> Option<f64> {
let mut best: Option<f64> = None;
for x in enumerate_int_box(&milp.lp.bounds) {
if feasible_orig(&milp.lp, &x) {
let obj: f64 = milp.lp.c.iter().zip(&x).map(|(c, xi)| c * xi).sum();
best = Some(best.map_or(obj, |b| b.min(obj)));
}
}
best
}
#[test]
fn cuts_preserve_optimum() {
use crate::solve_milp;
let opts = SolverOptions {
timeout_secs: Some(10.0),
..Default::default()
};
for (name, milp) in all_problems() {
let off = solve_milp(&milp, &opts, &MipConfig::default());
let on = solve_milp(&milp, &opts, &cuts_cfg(0));
assert_eq!(off.status, SolveStatus::Optimal, "{name}: off optimal");
assert_eq!(on.status, SolveStatus::Optimal, "{name}: on optimal");
assert!(
(off.objective - on.objective).abs() < 1e-6,
"{name}: cuts changed the optimum: off={} on={}",
off.objective,
on.objective
);
let bf = brute_force_min(&milp).expect("feasible integer optimum");
assert!(
(on.objective - bf).abs() < 1e-6,
"{name}: cuts-ON optimum {} != brute force {}",
on.objective,
bf
);
}
}
#[test]
fn cuts_on_root_lp_bound_valid_and_optimum_correct() {
use crate::solve_milp_with_stats;
let opts = SolverOptions {
timeout_secs: Some(10.0),
..Default::default()
};
for (name, milp) in all_problems() {
let bf = brute_force_min(&milp);
let (res, stats) =
solve_milp_with_stats(&milp, &opts, &cuts_cfg(3));
assert_eq!(
res.status,
SolveStatus::Optimal,
"{name}: cuts-on solve must reach Optimal"
);
assert!(
stats.root_lp_bound.is_finite(),
"{name}: root_lp_bound must be finite"
);
if let Some(opt) = bf {
assert!(
(res.objective - opt).abs() < 1e-6,
"{name}: cuts-on objective {} must equal brute-force {} \
(Bug 1: FP false incumbent corrupts B&B pruning)",
res.objective,
opt
);
}
}
}
#[test]
fn tableau_row_matches_dense() {
let l = p_box_le().lp;
let root = lp_root(&l);
assert_eq!(root.status, SolveStatus::Optimal);
let basis = &root.warm_start_basis.as_ref().unwrap().basis;
let sf = build_standard_form(&l);
assert_eq!(basis.len(), sf.m);
let mut lu = LuBasis::new_timed(&sf.a, basis, 0, None).unwrap();
let m = sf.m;
let n = sf.n_total;
let dense_a = csc_to_dense(&sf.a, m, n);
let b_inv = dense_basis_inverse(&dense_a, basis);
for i in 0..m {
let mut rho = vec![0.0; m];
rho[i] = 1.0;
lu.btran_dense(&mut rho);
for j in 0..n {
let via_btran = column_dot(&sf.a, j, &rho);
let mut direct = 0.0;
for k in 0..m {
direct += b_inv[i][k] * dense_a[k][j];
}
assert!(
(via_btran - direct).abs() < 1e-7,
"tableau ({i},{j}): btran {via_btran} != dense {direct}"
);
}
}
}
fn enumerate_int_window(n_vars: usize, lo: i64, hi: i64) -> Vec<Vec<f64>> {
let mut pts = vec![vec![]];
for _ in 0..n_vars {
let mut next = Vec::new();
for p in &pts {
for v in lo..=hi {
let mut q = p.clone();
q.push(v as f64);
next.push(q);
}
}
pts = next;
}
pts
}
fn assert_cuts_valid_nonvacuous(
milp: &MilpProblem,
int_pts: &[Vec<f64>],
name: &str,
rounds: usize,
) -> usize {
let out = add_root_cuts(milp, &SolverOptions::default(), &cuts_cfg(rounds));
let m_old = milp.lp.num_constraints;
let m_new = out.lp.num_constraints;
assert!(
m_new > m_old,
"{name}: no cuts generated (LP relaxation must be fractional for non-vacuous check)"
);
for x in int_pts {
if !feasible_orig(&milp.lp, x) {
continue;
}
let ax = out.lp.a.mat_vec_mul(x).unwrap();
for i in m_old..m_new {
assert_eq!(out.lp.constraint_types[i], ConstraintType::Ge);
assert!(
ax[i] >= out.lp.b[i] - 1e-6,
"{name}: INVALID CUT — integer point {x:?} removed by cut row {i}: \
lhs={} < rhs={}",
ax[i],
out.lp.b[i]
);
}
}
m_new - m_old
}
#[test]
fn cut_validity_negative_lb() {
let l = lp(
vec![-1.0, -1.0],
&[0, 0],
&[0, 1],
&[2.0, 2.0],
1,
vec![3.0],
vec![ConstraintType::Le],
vec![(-1.0, 2.0), (-1.0, 2.0)],
);
let milp = MilpProblem::new(l, vec![0, 1]).unwrap();
let pts = enumerate_int_box(&milp.lp.bounds);
let n = assert_cuts_valid_nonvacuous(&milp, &pts, "neg_lb", 5);
assert!(n > 0, "negative-lb path must generate ≥1 cut");
}
#[test]
fn cut_validity_negative_rhs_row_negation() {
let l = lp(
vec![1.0, 1.0],
&[0, 0],
&[0, 1],
&[-2.0, -2.0],
1,
vec![-3.0],
vec![ConstraintType::Le],
vec![(0.0, 2.0), (0.0, 2.0)],
);
let milp = MilpProblem::new(l, vec![0, 1]).unwrap();
let pts = enumerate_int_box(&milp.lp.bounds);
let n = assert_cuts_valid_nonvacuous(&milp, &pts, "neg_rhs_row_negation", 5);
assert!(n > 0, "row-negation path must generate ≥1 cut");
}
#[test]
fn cut_validity_mixed_le_ge() {
let l = lp(
vec![-1.0, -1.0],
&[0, 0, 1, 1],
&[0, 1, 0, 1],
&[2.0, 2.0, 1.0, 1.0],
2,
vec![3.0, 0.0],
vec![ConstraintType::Le, ConstraintType::Ge],
vec![(0.0, 2.0), (0.0, 2.0)],
);
let milp = MilpProblem::new(l, vec![0, 1]).unwrap();
let pts = enumerate_int_box(&milp.lp.bounds);
let n = assert_cuts_valid_nonvacuous(&milp, &pts, "mixed_le_ge", 5);
assert!(n > 0, "mixed Le/Ge path must generate ≥1 cut");
}
#[test]
fn cut_validity_true_lb_only_inf_ub() {
let milp = p_lb_only_inf();
let pts = enumerate_int_window(3, 0, 4);
let n = assert_cuts_valid_nonvacuous(&milp, &pts, "true_lb_only_inf", 5);
assert!(n > 0, "true lb-only (ub=∞) path must generate ≥1 cut");
}
#[test]
fn cut_validity_multi_var_ubonly_eq_row() {
let l = lp(
vec![-1.0, -2.0],
&[0, 0, 1, 1],
&[0, 1, 0, 1],
&[1.0, 1.0, 2.0, 4.0],
2,
vec![2.0, 7.0],
vec![ConstraintType::Eq, ConstraintType::Le],
vec![(f64::NEG_INFINITY, 3.0), (f64::NEG_INFINITY, 3.0)],
);
let milp = MilpProblem::new(l, vec![0, 1]).unwrap();
let pts = enumerate_int_window(2, -2, 4);
let n = assert_cuts_valid_nonvacuous(&milp, &pts, "ubonly_eq_row", 5);
assert!(n > 0, "UbOnly + Eq row path must generate ≥1 cut");
}
#[test]
fn cut_validity_fuzz_lcg() {
fn lcg(s: &mut u64) -> u64 {
*s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
*s
}
fn lcg_f(s: &mut u64, lo: f64, hi: f64) -> f64 {
lo + ((lcg(s) >> 11) as f64 / (1u64 << 53) as f64) * (hi - lo)
}
fn is_frac(v: f64) -> bool {
let f = v - v.floor();
f > 1e-4 && f < 1.0 - 1e-4
}
let mut rng: u64 = 0xdead_beef_cafe_babe;
let mut total = 0usize;
let mut with_cuts = 0usize;
for _ in 0..600 {
if total >= 160 {
break;
}
let lb0 = lcg_f(&mut rng, -2.0, 0.9).round();
let lb1 = lcg_f(&mut rng, -2.0, 0.9).round();
let ub0 = lcg_f(&mut rng, 2.0, 4.0).round();
let ub1 = lcg_f(&mut rng, 2.0, 4.0).round();
if lb0 >= ub0 || lb1 >= ub1 {
continue;
}
let a00 = lcg_f(&mut rng, 1.0, 5.0).round();
let a01 = lcg_f(&mut rng, 1.0, 5.0).round();
let is_le = lcg(&mut rng).is_multiple_of(2);
let min_ax = a00 * lb0 + a01 * lb1;
let max_ax = a00 * ub0 + a01 * ub1;
let range = max_ax - min_ax;
if range < 2.0 {
continue;
}
let mid = (min_ax + max_ax) / 2.0;
let rhs = mid.floor() + 0.5;
let ct = if is_le { ConstraintType::Le } else { ConstraintType::Ge };
let actual_rhs = rhs;
if actual_rhs <= min_ax || actual_rhs >= max_ax {
continue;
}
let l = lp(
vec![-1.0, -1.0],
&[0, 0],
&[0, 1],
&[a00, a01],
1,
vec![actual_rhs],
vec![ct],
vec![(lb0, ub0), (lb1, ub1)],
);
let milp = match MilpProblem::new(l, vec![0, 1]) {
Ok(m) => m,
Err(_) => continue,
};
let root = lp_root(&milp.lp);
if root.status != SolveStatus::Optimal {
continue;
}
let fractional_lp = root.solution.iter().any(|&v| is_frac(v));
if !fractional_lp {
continue;
}
total += 1;
let out = add_root_cuts(&milp, &SolverOptions::default(), &cuts_cfg(3));
let m_old = milp.lp.num_constraints;
let m_new = out.lp.num_constraints;
if m_new > m_old {
with_cuts += 1;
}
let pts = enumerate_int_box(&milp.lp.bounds);
for x in &pts {
if !feasible_orig(&milp.lp, x) {
continue;
}
let ax = out.lp.a.mat_vec_mul(x).unwrap();
for i in m_old..m_new {
assert!(
ax[i] >= out.lp.b[i] - 1e-6,
"fuzz INVALID CUT — a=[{a00},{a01}] rhs={actual_rhs} ct={ct:?} \
int-pt {x:?} violates cut row {i}: lhs={} < rhs={}",
ax[i],
out.lp.b[i]
);
}
}
}
assert!(total >= 100, "fuzz: need ≥100 fractional-LP problems, got {total}");
assert!(
with_cuts > 0,
"fuzz: at least one fractional-LP problem must generate cuts (got 0/{total})"
);
}
fn csc_to_dense(a: &CscMatrix, m: usize, n: usize) -> Vec<Vec<f64>> {
let mut d = vec![vec![0.0; n]; m];
for j in 0..n {
let (rs, vs) = a.get_column(j).unwrap();
for (&r, &v) in rs.iter().zip(vs) {
d[r][j] = v;
}
}
d
}
fn dense_basis_inverse(dense_a: &[Vec<f64>], basis: &[usize]) -> Vec<Vec<f64>> {
let m = basis.len();
let mut aug = vec![vec![0.0; 2 * m]; m];
for (r, row) in aug.iter_mut().enumerate() {
for (c, &col) in basis.iter().enumerate() {
row[c] = dense_a[r][col];
}
row[m + r] = 1.0;
}
for c in 0..m {
let mut piv = c;
for r in (c + 1)..m {
if aug[r][c].abs() > aug[piv][c].abs() {
piv = r;
}
}
aug.swap(c, piv);
let d = aug[c][c];
assert!(d.abs() > 1e-12, "singular basis in oracle");
for v in aug[c].iter_mut() {
*v /= d;
}
for r in 0..m {
if r != c {
let f = aug[r][c];
for k in 0..2 * m {
aug[r][k] -= f * aug[c][k];
}
}
}
}
aug.iter().map(|row| row[m..].to_vec()).collect()
}