use super::{
is_integer_valued,
lp_relaxation::{LpRelaxationSolver, LpResult},
most_fractional_variable, IntegerVariableSet, LinearProgram, MipResult,
};
use crate::error::{OptimizeError, OptimizeResult};
use scirs2_core::ndarray::{s, Array1, Array2};
#[derive(Debug, Clone)]
pub struct CuttingPlaneOptions {
pub max_iter: usize,
pub int_tol: f64,
pub cuts_per_iter: usize,
pub cut_violation_tol: f64,
pub fallback_bb: bool,
pub max_cuts: usize,
}
impl Default for CuttingPlaneOptions {
fn default() -> Self {
CuttingPlaneOptions {
max_iter: 100,
int_tol: 1e-6,
cuts_per_iter: 5,
cut_violation_tol: 1e-8,
fallback_bb: true,
max_cuts: 500,
}
}
}
#[derive(Debug, Clone)]
struct GomoryCut {
a: Vec<f64>,
b: f64,
}
pub struct CuttingPlaneSolver {
pub options: CuttingPlaneOptions,
}
impl CuttingPlaneSolver {
pub fn new() -> Self {
CuttingPlaneSolver {
options: CuttingPlaneOptions::default(),
}
}
pub fn with_options(options: CuttingPlaneOptions) -> Self {
CuttingPlaneSolver { options }
}
fn generate_gomory_cut(
&self,
x: &[f64],
branch_var: usize,
n: usize,
lp: &LinearProgram,
existing_cuts: &[GomoryCut],
) -> Option<GomoryCut> {
let xi = x[branch_var];
let fi = xi - xi.floor();
if fi < self.options.int_tol || fi > 1.0 - self.options.int_tol {
return None;
}
let cx: f64 = lp.c.iter().zip(x.iter()).map(|(&ci, &xi)| ci * xi).sum();
let cut_rhs = -cx.ceil();
let cut_lhs: Vec<f64> = lp.c.iter().map(|&ci| -ci).collect();
let violation: f64 = cut_lhs
.iter()
.zip(x.iter())
.map(|(&ai, &xi)| ai * xi)
.sum::<f64>()
- cut_rhs;
if violation <= self.options.cut_violation_tol {
let mut a = vec![0.0; n];
a[branch_var] = -1.0;
let b = -xi.ceil();
return Some(GomoryCut { a, b });
}
Some(GomoryCut {
a: cut_lhs,
b: cut_rhs,
})
}
fn augment_lp(lp: &LinearProgram, cuts: &[GomoryCut]) -> LinearProgram {
if cuts.is_empty() {
return lp.clone();
}
let n = lp.n_vars();
let n_new_cuts = cuts.len();
let augmented = match (&lp.a_ub, &lp.b_ub) {
(Some(aub), Some(bub)) => {
let m = aub.nrows();
let mut new_a = Array2::zeros((m + n_new_cuts, n));
let mut new_b = Array1::zeros(m + n_new_cuts);
for i in 0..m {
for j in 0..n {
new_a[[i, j]] = aub[[i, j]];
}
new_b[i] = bub[i];
}
for (k, cut) in cuts.iter().enumerate() {
for j in 0..n {
new_a[[m + k, j]] = cut.a[j];
}
new_b[m + k] = cut.b;
}
(new_a, new_b)
}
_ => {
let mut new_a = Array2::zeros((n_new_cuts, n));
let mut new_b = Array1::zeros(n_new_cuts);
for (k, cut) in cuts.iter().enumerate() {
for j in 0..n {
new_a[[k, j]] = cut.a[j];
}
new_b[k] = cut.b;
}
(new_a, new_b)
}
};
LinearProgram {
c: lp.c.clone(),
a_ub: Some(augmented.0),
b_ub: Some(augmented.1),
a_eq: lp.a_eq.clone(),
b_eq: lp.b_eq.clone(),
lower: lp.lower.clone(),
upper: lp.upper.clone(),
}
}
pub fn solve(&self, lp: &LinearProgram, ivs: &IntegerVariableSet) -> OptimizeResult<MipResult> {
let n = lp.n_vars();
if n == 0 {
return Err(OptimizeError::InvalidInput("Empty problem".to_string()));
}
let lb: Vec<f64> = lp.lower.as_ref().map_or(vec![0.0; n], |l| l.to_vec());
let ub: Vec<f64> = lp
.upper
.as_ref()
.map_or(vec![f64::INFINITY; n], |u| u.to_vec());
let mut current_lp = lp.clone();
let mut all_cuts: Vec<GomoryCut> = Vec::new();
let mut lp_solves = 0usize;
let mut total_cuts = 0usize;
for iter in 0..self.options.max_iter {
if total_cuts >= self.options.max_cuts {
break;
}
let lp_result = match LpRelaxationSolver::solve(¤t_lp, &lb, &ub) {
Ok(r) => r,
Err(e) => return Err(e),
};
lp_solves += 1;
if !lp_result.success {
return Ok(MipResult {
x: Array1::zeros(n),
fun: f64::INFINITY,
success: false,
message: "LP relaxation became infeasible after cuts".to_string(),
nodes_explored: iter,
lp_solves,
lower_bound: f64::INFINITY,
});
}
let x: Vec<f64> = lp_result.x.to_vec();
let obj = lp_result.fun;
let all_integer = ivs.kinds.iter().enumerate().all(|(i, k)| {
matches!(k, super::IntegerKind::Continuous)
|| is_integer_valued(x[i], self.options.int_tol)
});
if all_integer {
return Ok(MipResult {
x: Array1::from_vec(x),
fun: obj,
success: true,
message: format!(
"Integer feasible solution found (cuts={}, lp_solves={})",
total_cuts, lp_solves
),
nodes_explored: iter,
lp_solves,
lower_bound: obj,
});
}
let mut new_cuts_added = 0;
let fractional_vars: Vec<usize> = (0..n)
.filter(|&i| ivs.is_integer(i) && !is_integer_valued(x[i], self.options.int_tol))
.collect();
for &var in &fractional_vars {
if new_cuts_added >= self.options.cuts_per_iter {
break;
}
if total_cuts >= self.options.max_cuts {
break;
}
if let Some(cut) = self.generate_gomory_cut(&x, var, n, ¤t_lp, &all_cuts) {
all_cuts.push(cut);
new_cuts_added += 1;
total_cuts += 1;
}
}
if new_cuts_added == 0 {
break;
}
current_lp = Self::augment_lp(lp, &all_cuts);
}
if self.options.fallback_bb {
use super::branch_and_bound::{BranchAndBoundOptions, BranchAndBoundSolver};
let bb_opts = BranchAndBoundOptions {
max_nodes: 10000,
..Default::default()
};
let bb = BranchAndBoundSolver::with_options(bb_opts);
let bb_result = bb.solve(¤t_lp, ivs)?;
return Ok(MipResult {
lp_solves: bb_result.lp_solves + lp_solves,
..bb_result
});
}
Ok(MipResult {
x: Array1::zeros(n),
fun: f64::INFINITY,
success: false,
message: "No integer feasible solution found in cutting plane phase".to_string(),
nodes_explored: self.options.max_iter,
lp_solves,
lower_bound: f64::NEG_INFINITY,
})
}
}
impl Default for CuttingPlaneSolver {
fn default() -> Self {
CuttingPlaneSolver::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use scirs2_core::ndarray::{array, Array2};
#[test]
fn test_cutting_plane_simple_integer() {
let c = array![1.0, 1.0];
let mut lp = LinearProgram::new(c);
lp.a_ub = Some(Array2::from_shape_vec((1, 2), vec![-1.0, -1.0]).expect("shape"));
lp.b_ub = Some(array![-3.5]);
lp.lower = Some(array![0.0, 0.0]);
lp.upper = Some(array![10.0, 10.0]);
let ivs = IntegerVariableSet::all_integer(2);
let solver = CuttingPlaneSolver::new();
let result = solver.solve(&lp, &ivs).expect("solve failed");
assert!(result.success);
assert_abs_diff_eq!(result.fun, 4.0, epsilon = 1e-3);
}
#[test]
fn test_cutting_plane_already_integer() {
let c = array![1.0, 2.0];
let mut lp = LinearProgram::new(c);
lp.lower = Some(array![2.0, 3.0]);
lp.upper = Some(array![5.0, 6.0]);
let ivs = IntegerVariableSet::all_integer(2);
let solver = CuttingPlaneSolver::new();
let result = solver.solve(&lp, &ivs).expect("solve failed");
assert!(result.success);
assert_abs_diff_eq!(result.fun, 8.0, epsilon = 1e-3);
}
#[test]
fn test_cutting_plane_no_fallback() {
let opts = CuttingPlaneOptions {
fallback_bb: false,
max_iter: 50,
..Default::default()
};
let c = array![1.0, 1.0];
let mut lp = LinearProgram::new(c);
lp.lower = Some(array![0.0, 0.0]);
lp.upper = Some(array![5.0, 5.0]);
let ivs = IntegerVariableSet::all_integer(2);
let solver = CuttingPlaneSolver::with_options(opts);
let _result = solver.solve(&lp, &ivs).expect("solve failed");
}
}