#[allow(unused_imports)]
use crate::prelude::*;
use num_bigint::BigInt;
use num_rational::BigRational;
use num_traits::{One, Zero};
#[derive(Debug)]
pub struct CuttingPlaneGenerator {
integer_vars: FxHashSet<VarId>,
stats: CuttingPlaneStats,
}
pub type VarId = usize;
#[derive(Debug, Clone)]
pub struct CuttingPlane {
pub coeffs: Vec<(VarId, BigRational)>,
pub rhs: BigRational,
pub cut_type: CutType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CutType {
Gomory,
GomoryMI,
LiftProject,
Cover,
Clique,
}
#[derive(Debug, Clone)]
pub struct CuttingPlaneConfig {
pub max_cuts: usize,
pub enable_gomory: bool,
pub enable_lift_project: bool,
pub enable_cover: bool,
}
impl Default for CuttingPlaneConfig {
fn default() -> Self {
Self {
max_cuts: 100,
enable_gomory: true,
enable_lift_project: true,
enable_cover: true,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CuttingPlaneStats {
pub cuts_generated: usize,
pub gomory_cuts: usize,
pub lift_project_cuts: usize,
pub cover_cuts: usize,
}
impl CuttingPlaneGenerator {
pub fn new(integer_vars: FxHashSet<VarId>) -> Self {
Self {
integer_vars,
stats: CuttingPlaneStats::default(),
}
}
pub fn generate_gomory_cut(
&mut self,
_basic_var: VarId,
row: &[(VarId, BigRational)],
rhs: &BigRational,
) -> Option<CuttingPlane> {
let frac_rhs = self.fractional_part(rhs);
if frac_rhs.is_zero() {
return None; }
let mut cut_coeffs = Vec::new();
for (var_id, coeff) in row {
let frac_coeff = self.fractional_part(coeff);
if !frac_coeff.is_zero() {
let cut_coeff = if frac_coeff <= frac_rhs {
-frac_coeff
} else {
-(BigRational::one() - frac_coeff)
};
cut_coeffs.push((*var_id, cut_coeff));
}
}
self.stats.gomory_cuts += 1;
self.stats.cuts_generated += 1;
Some(CuttingPlane {
coeffs: cut_coeffs,
rhs: -frac_rhs,
cut_type: CutType::Gomory,
})
}
pub fn generate_gomory_mi_cut(
&mut self,
basic_var: VarId,
row: &[(VarId, BigRational)],
rhs: &BigRational,
) -> Option<CuttingPlane> {
if !self.integer_vars.contains(&basic_var) {
return None; }
let f0 = self.fractional_part(rhs);
if f0.is_zero() {
return None;
}
let mut cut_coeffs = Vec::new();
for (var_id, coeff) in row {
let fj = self.fractional_part(coeff);
let cut_coeff = if self.integer_vars.contains(var_id) {
if fj <= f0 {
-fj
} else {
-(BigRational::one() - fj) * &f0 / (BigRational::one() - &f0)
}
} else {
if coeff >= &BigRational::zero() {
-coeff
} else {
coeff * &f0 / (BigRational::one() - &f0)
}
};
if !cut_coeff.is_zero() {
cut_coeffs.push((*var_id, cut_coeff));
}
}
self.stats.gomory_cuts += 1;
self.stats.cuts_generated += 1;
Some(CuttingPlane {
coeffs: cut_coeffs,
rhs: -f0,
cut_type: CutType::GomoryMI,
})
}
pub fn generate_lift_project_cut(
&mut self,
constraint: &[(VarId, BigRational)],
rhs: &BigRational,
lifting_var: VarId,
) -> Option<CuttingPlane> {
if !self.integer_vars.contains(&lifting_var) {
return None;
}
let mut cut_coeffs = Vec::new();
for (var_id, coeff) in constraint {
if *var_id == lifting_var {
continue;
}
let lifted_coeff = coeff.clone();
cut_coeffs.push((*var_id, lifted_coeff));
}
self.stats.lift_project_cuts += 1;
self.stats.cuts_generated += 1;
Some(CuttingPlane {
coeffs: cut_coeffs,
rhs: rhs.clone(),
cut_type: CutType::LiftProject,
})
}
pub fn generate_cover_cut(
&mut self,
weights: &[(VarId, BigRational)],
capacity: &BigRational,
) -> Option<CuttingPlane> {
let cover = self.find_minimal_cover(weights, capacity)?;
let mut cut_coeffs = Vec::new();
for var_id in &cover {
cut_coeffs.push((*var_id, BigRational::one()));
}
let rhs = BigRational::from_integer(BigInt::from(cover.len() as i64 - 1));
self.stats.cover_cuts += 1;
self.stats.cuts_generated += 1;
Some(CuttingPlane {
coeffs: cut_coeffs,
rhs,
cut_type: CutType::Cover,
})
}
fn find_minimal_cover(
&self,
weights: &[(VarId, BigRational)],
capacity: &BigRational,
) -> Option<Vec<VarId>> {
let mut sorted_weights = weights.to_vec();
sorted_weights.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(core::cmp::Ordering::Equal));
let mut cover = Vec::new();
let mut total_weight = BigRational::zero();
for (var_id, weight) in sorted_weights {
cover.push(var_id);
total_weight = &total_weight + &weight;
if total_weight > *capacity {
return Some(cover);
}
}
None }
fn fractional_part(&self, value: &BigRational) -> BigRational {
let floor_val = self.floor(value);
value - &floor_val
}
fn floor(&self, value: &BigRational) -> BigRational {
let numer = value.numer();
let denom = value.denom();
let quotient = numer / denom;
BigRational::from_integer(quotient)
}
pub fn stats(&self) -> &CuttingPlaneStats {
&self.stats
}
pub fn is_violated(&self, cut: &CuttingPlane, solution: &[(VarId, BigRational)]) -> bool {
let mut lhs = BigRational::zero();
for (var_id, coeff) in &cut.coeffs {
if let Some((_, value)) = solution.iter().find(|(id, _)| id == var_id) {
lhs += coeff * value;
}
}
lhs > cut.rhs
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cutting_plane_generator() {
let mut integer_vars = FxHashSet::default();
integer_vars.insert(0);
integer_vars.insert(1);
let generator = CuttingPlaneGenerator::new(integer_vars);
assert_eq!(generator.stats.cuts_generated, 0);
}
#[test]
fn test_fractional_part() {
let integer_vars = FxHashSet::default();
let generator = CuttingPlaneGenerator::new(integer_vars);
let val = BigRational::new(BigInt::from(7), BigInt::from(3)); let frac = generator.fractional_part(&val);
assert_eq!(frac, BigRational::new(BigInt::from(1), BigInt::from(3)));
}
}