use super::{LinearProgram, OpfError, OptStatus, Solution, Solver};
#[derive(Clone, Debug)]
pub struct BnbOptions {
pub integrality_tolerance: f64,
pub relative_gap: f64,
pub max_nodes: usize,
}
impl Default for BnbOptions {
fn default() -> Self {
Self {
integrality_tolerance: 1e-6,
relative_gap: 0.0,
max_nodes: 10_000,
}
}
}
pub struct BranchAndBound {
relaxation: Box<dyn Solver>,
options: BnbOptions,
nodes: usize,
gap: f64,
}
impl BranchAndBound {
pub fn new() -> Self {
Self::with_solver(Box::new(super::ipm::IpmSolver::new()))
}
pub fn with_solver(relaxation: Box<dyn Solver>) -> Self {
Self {
relaxation,
options: BnbOptions::default(),
nodes: 0,
gap: f64::INFINITY,
}
}
pub fn options_mut(&mut self) -> &mut BnbOptions {
&mut self.options
}
pub fn nodes(&self) -> usize {
self.nodes
}
pub fn gap(&self) -> f64 {
self.gap
}
fn is_integral(&self, value: f64) -> bool {
(value - value.round()).abs() <= self.options.integrality_tolerance
}
fn most_fractional(&self, problem: &LinearProgram, primal: &[f64]) -> Option<usize> {
(0..problem.n_vars)
.filter(|&c| problem.is_integral(c))
.filter(|&c| !self.is_integral(primal[c]))
.max_by(|&a, &b| {
let fa = (primal[a] - primal[a].round()).abs();
let fb = (primal[b] - primal[b].round()).abs();
fa.total_cmp(&fb)
})
}
}
impl Default for BranchAndBound {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug)]
struct Node {
bounds: Vec<(usize, f64, f64)>,
}
impl Solver for BranchAndBound {
fn solve(&mut self, problem: &LinearProgram) -> Result<Solution, OpfError> {
problem.validate()?;
self.nodes = 0;
self.gap = f64::INFINITY;
let mut relaxed = problem.clone();
relaxed.col_integral = Vec::new();
if !problem.has_integers() {
return self.relaxation.solve(&relaxed);
}
let mut incumbent: Option<Solution> = None;
let mut best_bound = f64::NEG_INFINITY;
let mut stack = vec![Node { bounds: Vec::new() }];
let mut exhausted = true;
while let Some(node) = stack.pop() {
if self.nodes >= self.options.max_nodes {
exhausted = false;
break;
}
self.nodes += 1;
let mut sub = relaxed.clone();
for &(col, lower, upper) in &node.bounds {
sub.col_lower[col] = sub.col_lower[col].max(lower);
sub.col_upper[col] = sub.col_upper[col].min(upper);
if sub.col_lower[col] > sub.col_upper[col] {
sub.col_lower[col] = f64::NAN;
break;
}
}
if sub.col_lower.iter().any(|x| x.is_nan()) {
continue;
}
let solution = match self.relaxation.solve(&sub) {
Ok(s) => s,
Err(_) => continue,
};
if solution.status != OptStatus::Optimal {
if solution.status == OptStatus::Unbounded {
return Ok(Solution::failed(OptStatus::Unbounded));
}
continue;
}
if node.bounds.is_empty() {
best_bound = solution.objective;
}
if let Some(best) = &incumbent {
if solution.objective >= best.objective - self.options.integrality_tolerance {
continue;
}
}
match self.most_fractional(problem, &solution.primal) {
None => {
let mut found = solution;
for c in 0..problem.n_vars {
if problem.is_integral(c) {
found.primal[c] = found.primal[c].round();
}
}
incumbent = Some(found);
}
Some(col) => {
let value = solution.primal[col];
let floor = value.floor();
let ceil = value.ceil();
let (near, far) = if value - floor <= ceil - value {
((col, f64::NEG_INFINITY, floor), (col, ceil, f64::INFINITY))
} else {
((col, ceil, f64::INFINITY), (col, f64::NEG_INFINITY, floor))
};
for child in [far, near] {
let mut bounds = node.bounds.clone();
bounds.push(child);
stack.push(Node { bounds });
}
}
}
}
let Some(mut best) = incumbent else {
return Ok(Solution::failed(if self.nodes >= self.options.max_nodes {
OptStatus::Other("node limit reached before any integer solution")
} else {
OptStatus::Infeasible
}));
};
self.gap = if exhausted {
0.0
} else if best_bound.is_finite() {
((best.objective - best_bound) / (best.objective.abs() + 1.0)).max(0.0)
} else {
f64::INFINITY
};
if !exhausted && self.gap > self.options.relative_gap {
best.status = OptStatus::Other("node limit reached; solution may not be optimal");
}
best.col_dual = vec![0.0; problem.n_vars];
best.row_dual = vec![0.0; problem.n_rows];
Ok(best)
}
}