use super::cutting_planes::{CuttingPlaneConfig, CuttingPlaneGenerator};
use super::dual_simplex::{DualSimplexResult, DualSimplexSolver as DualSimplex};
#[allow(unused_imports)]
use crate::prelude::*;
use core::cmp::Ordering;
use num_rational::BigRational;
use num_traits::{Signed, ToPrimitive, Zero};
pub type VarId = usize;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VarType {
Continuous,
Integer,
Binary,
}
#[derive(Debug, Clone)]
struct BranchNode {
id: usize,
lp_bound: BigRational,
bounds: FxHashMap<VarId, (BigRational, BigRational)>,
depth: usize,
}
impl PartialEq for BranchNode {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for BranchNode {}
impl PartialOrd for BranchNode {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for BranchNode {
fn cmp(&self, other: &Self) -> Ordering {
self.lp_bound.cmp(&other.lp_bound)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NodeSelection {
BestFirst,
DepthFirst,
BreadthFirst,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BranchingStrategy {
MostFractional,
LeastFractional,
LargestCoeff,
}
#[derive(Debug, Clone)]
pub struct BranchCutConfig {
pub node_selection: NodeSelection,
pub branching_strategy: BranchingStrategy,
pub max_nodes: usize,
pub gap_tolerance: f64,
pub enable_cuts: bool,
pub max_cuts_per_node: usize,
pub cut_config: CuttingPlaneConfig,
}
impl Default for BranchCutConfig {
fn default() -> Self {
Self {
node_selection: NodeSelection::BestFirst,
branching_strategy: BranchingStrategy::MostFractional,
max_nodes: 100_000,
gap_tolerance: 1e-6,
enable_cuts: true,
max_cuts_per_node: 10,
cut_config: CuttingPlaneConfig::default(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct BranchCutStats {
pub nodes_explored: usize,
pub cuts_added: usize,
pub lp_solves: usize,
pub best_integer_value: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BranchCutResult {
Optimal,
Feasible,
Infeasible,
NodeLimit,
}
#[derive(Debug)]
pub struct BranchCutSolver {
config: BranchCutConfig,
var_types: Vec<VarType>,
nodes: BinaryHeap<BranchNode>,
next_node_id: usize,
best_solution: Option<FxHashMap<VarId, BigRational>>,
best_value: Option<BigRational>,
#[allow(dead_code)]
cut_generator: CuttingPlaneGenerator,
stats: BranchCutStats,
}
impl BranchCutSolver {
pub fn new(config: BranchCutConfig, var_types: Vec<VarType>) -> Self {
let integer_vars: crate::prelude::FxHashSet<VarId> = var_types
.iter()
.enumerate()
.filter_map(|(i, &vt)| {
if vt == VarType::Integer || vt == VarType::Binary {
Some(i)
} else {
None
}
})
.collect();
Self {
cut_generator: CuttingPlaneGenerator::new(integer_vars),
config,
var_types,
nodes: BinaryHeap::new(),
next_node_id: 0,
best_solution: None,
best_value: None,
stats: BranchCutStats::default(),
}
}
pub fn solve(&mut self, root_solver: &mut DualSimplex) -> BranchCutResult {
let root_result = root_solver.solve();
self.stats.lp_solves += 1;
match root_result {
DualSimplexResult::Infeasible => return BranchCutResult::Infeasible,
DualSimplexResult::Optimal => {
let solution = root_solver.get_solution();
if self.is_integer_feasible(&solution) {
self.best_solution = Some(solution.clone());
self.best_value = Some(root_solver.get_objective_value());
return BranchCutResult::Optimal;
}
let root_node = BranchNode {
id: self.next_node_id,
lp_bound: root_solver.get_objective_value(),
bounds: FxHashMap::default(),
depth: 0,
};
self.next_node_id += 1;
self.nodes.push(root_node);
}
_ => return BranchCutResult::NodeLimit,
}
while let Some(node) = self.nodes.pop() {
self.stats.nodes_explored += 1;
if self.stats.nodes_explored >= self.config.max_nodes {
return BranchCutResult::NodeLimit;
}
if self.can_prune(&node) {
continue;
}
let mut node_solver = root_solver.clone();
for (&var_id, (lb, ub)) in &node.bounds {
let n_vars = root_solver.num_vars();
let mut lb_row = vec![BigRational::zero(); n_vars];
if var_id < n_vars {
lb_row[var_id] = BigRational::from_integer((-1).into());
}
node_solver.add_constraint(lb_row, -lb.clone());
let mut ub_row = vec![BigRational::zero(); n_vars];
if var_id < n_vars {
ub_row[var_id] = BigRational::from_integer(1.into());
}
node_solver.add_constraint(ub_row, ub.clone());
}
let lp_result = node_solver.solve();
self.stats.lp_solves += 1;
match lp_result {
DualSimplexResult::Infeasible => {
continue;
}
DualSimplexResult::Optimal => {
let solution = node_solver.get_solution();
let obj_value = node_solver.get_objective_value();
if let Some(ref best) = self.best_value
&& obj_value >= *best
{
continue;
}
if self.is_integer_feasible(&solution) {
let improve = self.best_value.as_ref().is_none_or(|bv| obj_value < *bv);
if improve {
self.best_value = Some(obj_value.clone());
self.best_solution = Some(solution.clone());
self.stats.best_integer_value =
Some(obj_value.to_f64().unwrap_or(f64::INFINITY));
}
continue; }
let Some((branch_var, branch_val)) = self.select_branch_variable(&solution)
else {
continue;
};
let floor_val = branch_val.floor();
let ceil_val = floor_val.clone() + BigRational::from_integer(1.into());
let mut left_bounds = node.bounds.clone();
{
let entry = left_bounds
.entry(branch_var)
.or_insert_with(|| (BigRational::zero(), floor_val.clone()));
entry.1 = entry.1.clone().min(floor_val);
}
let left_node = BranchNode {
id: self.next_node_id,
lp_bound: obj_value.clone(),
bounds: left_bounds,
depth: node.depth + 1,
};
self.next_node_id += 1;
self.nodes.push(left_node);
let mut right_bounds = node.bounds.clone();
{
let entry = right_bounds
.entry(branch_var)
.or_insert_with(|| (ceil_val.clone(), BigRational::zero()));
entry.0 = entry.0.clone().max(ceil_val);
}
let right_node = BranchNode {
id: self.next_node_id,
lp_bound: obj_value,
bounds: right_bounds,
depth: node.depth + 1,
};
self.next_node_id += 1;
self.nodes.push(right_node);
}
_ => {
continue;
}
}
}
if self.best_solution.is_some() {
BranchCutResult::Feasible
} else {
BranchCutResult::Infeasible
}
}
fn is_integer_feasible(&self, solution: &FxHashMap<VarId, BigRational>) -> bool {
for (var, value) in solution {
if (self.var_types.get(*var) == Some(&VarType::Integer)
|| self.var_types.get(*var) == Some(&VarType::Binary))
&& !self.is_integer_value(value)
{
return false;
}
}
true
}
fn is_integer_value(&self, value: &BigRational) -> bool {
let frac = value - value.floor();
frac.is_zero() || frac < BigRational::new(1.into(), 1000000.into())
}
fn can_prune(&self, node: &BranchNode) -> bool {
if let Some(ref best_value) = self.best_value {
node.lp_bound >= *best_value
} else {
false
}
}
fn select_branch_variable(
&self,
solution: &FxHashMap<VarId, BigRational>,
) -> Option<(VarId, BigRational)> {
let mut candidates = Vec::new();
for (var, value) in solution {
if self.var_types.get(*var) == Some(&VarType::Integer) && !self.is_integer_value(value)
{
let frac = value - value.floor();
candidates.push((*var, value.clone(), frac));
}
}
if candidates.is_empty() {
return None;
}
match self.config.branching_strategy {
BranchingStrategy::MostFractional => {
candidates.sort_by(|a, b| {
let dist_a = (a.2.clone() - BigRational::new(1.into(), 2.into())).abs();
let dist_b = (b.2.clone() - BigRational::new(1.into(), 2.into())).abs();
dist_a.cmp(&dist_b)
});
Some((candidates[0].0, candidates[0].1.clone()))
}
BranchingStrategy::LeastFractional => {
candidates.sort_by(|a, b| a.2.cmp(&b.2));
Some((candidates[0].0, candidates[0].1.clone()))
}
BranchingStrategy::LargestCoeff => {
Some((candidates[0].0, candidates[0].1.clone()))
}
}
}
pub fn stats(&self) -> &BranchCutStats {
&self.stats
}
pub fn best_solution(&self) -> Option<&FxHashMap<VarId, BigRational>> {
self.best_solution.as_ref()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_branch_cut_creation() {
let config = BranchCutConfig::default();
let var_types = vec![VarType::Integer, VarType::Integer];
let solver = BranchCutSolver::new(config, var_types);
assert_eq!(solver.stats().nodes_explored, 0);
}
#[test]
fn test_is_integer_feasible() {
let config = BranchCutConfig::default();
let var_types = vec![VarType::Integer, VarType::Continuous];
let solver = BranchCutSolver::new(config, var_types);
let mut solution = FxHashMap::default();
solution.insert(0, BigRational::from_integer(5.into()));
solution.insert(1, BigRational::new(3.into(), 2.into()));
assert!(solver.is_integer_feasible(&solution));
solution.insert(0, BigRational::new(5.into(), 2.into())); assert!(!solver.is_integer_feasible(&solution));
}
}