use super::node::BBNode;
use crate::options::QpWarmStart;
pub(crate) const MIN_BRANCH_BOX_WIDTH: f64 = 1e-6;
pub(crate) fn select_branching_variable(node: &BBNode, x: &[f64]) -> Option<usize> {
let n = node.var_bounds.len();
debug_assert_eq!(n, x.len(), "x length must match node bounds");
let mut best: Option<(usize, f64, f64)> = None; for j in 0..n {
let (lb, ub) = node.var_bounds[j];
if !lb.is_finite() || !ub.is_finite() {
continue;
}
let width = ub - lb;
if width <= MIN_BRANCH_BOX_WIDTH {
continue;
}
let mid = 0.5 * (lb + ub);
let xj = x[j].clamp(lb, ub);
let dev = (xj - mid).abs() / width;
let better = match best {
None => true,
Some((_, bw, bd)) => {
width > bw || (width == bw && dev > bd)
}
};
if better {
best = Some((j, width, dev));
}
}
best.map(|(j, _, _)| j)
}
pub(crate) fn split_node(
parent: &BBNode,
j: usize,
split_at: f64,
parent_warm: Option<QpWarmStart>,
) -> (BBNode, BBNode) {
let (lb, ub) = parent.var_bounds[j];
let mid = 0.5 * (lb + ub);
let s = if split_at > lb + MIN_BRANCH_BOX_WIDTH && split_at < ub - MIN_BRANCH_BOX_WIDTH {
split_at
} else {
mid
};
let mut left = parent.var_bounds.clone();
left[j].1 = s;
let mut right = parent.var_bounds.clone();
right[j].0 = s;
(
parent.child(left, parent.lower_bound, parent_warm.clone()),
parent.child(right, parent.lower_bound, parent_warm),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn picks_widest_box_primary_then_dev_tiebreak() {
let n = BBNode::root(vec![(0.0, 10.0), (-1.0, 1.0), (0.0, 100.0)], 0.0);
let x = vec![9.0, 0.9, 50.0];
let j = select_branching_variable(&n, &x).expect("should branch");
assert_eq!(j, 2, "widest box (var2 width=100) chosen primary");
}
#[test]
fn dev_breaks_ties_among_equal_widths() {
let n = BBNode::root(vec![(-1.0, 1.0), (-1.0, 1.0), (-1.0, 1.0)], 0.0);
let x = vec![0.9, 0.0, 0.8];
let j = select_branching_variable(&n, &x).expect("should branch");
assert_eq!(j, 0, "dev tiebreak picks var0 with highest |x-mid|/width");
}
#[test]
fn skips_infinite_bounds() {
let n = BBNode::root(vec![(f64::NEG_INFINITY, f64::INFINITY), (0.0, 1.0)], 0.0);
let x = vec![0.0, 0.9];
let j = select_branching_variable(&n, &x).expect("should branch");
assert_eq!(j, 1, "infinite-bound var skipped");
}
#[test]
fn returns_none_when_all_too_narrow() {
let n = BBNode::root(vec![(0.0, 1e-10), (5.0, 5.0)], 0.0);
let x = vec![0.0, 5.0];
assert!(select_branching_variable(&n, &x).is_none());
}
#[test]
fn split_at_interior_uses_provided_point() {
let p = BBNode::root(vec![(0.0, 10.0)], -1.0);
let (l, r) = split_node(&p, 0, 3.0, None);
assert_eq!(l.var_bounds[0], (0.0, 3.0));
assert_eq!(r.var_bounds[0], (3.0, 10.0));
assert_eq!(l.depth, 1);
assert_eq!(r.depth, 1);
}
#[test]
fn split_at_boundary_falls_back_to_midpoint() {
let p = BBNode::root(vec![(0.0, 10.0)], -1.0);
let (l, r) = split_node(&p, 0, 0.0, None);
assert_eq!(l.var_bounds[0], (0.0, 5.0));
assert_eq!(r.var_bounds[0], (5.0, 10.0));
}
#[test]
fn widest_box_dominates_even_when_other_var_has_high_dev() {
let n = BBNode::root(vec![(-1.0, 0.0), (-1.0, 1.0)], 0.0);
let x = vec![-1.0, 0.0];
let j = select_branching_variable(&n, &x).expect("must branch");
assert_eq!(j, 1, "widest var (1) must beat narrower high-dev var (0)");
}
#[test]
fn split_propagates_warm_to_both_children() {
let p = BBNode::root(vec![(0.0, 4.0)], 0.0);
let warm = QpWarmStart {
x: vec![2.0],
y: vec![],
mu: 1e-6,
};
let (l, r) = split_node(&p, 0, 2.0, Some(warm));
assert!(l.warm.is_some());
assert!(r.warm.is_some());
}
}