use super::node::VarBounds;
use crate::options::MipBranching;
pub(crate) fn fractionality(v: f64) -> f64 {
(v - v.round()).abs()
}
pub(crate) fn is_integer(v: f64, tol: f64) -> bool {
fractionality(v) <= tol
}
pub(crate) fn is_integer_feasible(x: &[f64], integer_mask: &[bool], tol: f64) -> bool {
debug_assert_eq!(x.len(), integer_mask.len(), "x / mask length mismatch");
integer_mask
.iter()
.zip(x.iter())
.all(|(&is_int, &v)| !is_int || is_integer(v, tol))
}
pub(crate) fn select_branching_variable(
x: &[f64],
integer_mask: &[bool],
tol: f64,
strategy: MipBranching,
) -> Option<usize> {
debug_assert_eq!(x.len(), integer_mask.len(), "x / mask length mismatch");
match strategy {
MipBranching::MostFractional => select_most_fractional(x, integer_mask, tol),
}
}
fn select_most_fractional(x: &[f64], integer_mask: &[bool], tol: f64) -> Option<usize> {
let mut best: Option<(usize, f64)> = None;
for (j, &is_int) in integer_mask.iter().enumerate() {
if !is_int {
continue;
}
let frac = fractionality(x[j]);
if frac <= tol {
continue;
}
let better = match best {
None => true,
Some((_, bf)) => frac > bf,
};
if better {
best = Some((j, frac));
}
}
best.map(|(j, _)| j)
}
pub(crate) fn branch_bounds(
parent_bounds: &[(f64, f64)],
j: usize,
v: f64,
) -> (VarBounds, VarBounds) {
let mut down = parent_bounds.to_vec();
down[j].1 = v.floor();
let mut up = parent_bounds.to_vec();
up[j].0 = v.ceil();
(down, up)
}
pub(crate) fn widest_splittable_integer(
bounds: &[(f64, f64)],
integer_mask: &[bool],
) -> Option<usize> {
let mut best: Option<(usize, f64)> = None;
for (j, &is_int) in integer_mask.iter().enumerate() {
if !is_int {
continue;
}
let (lb, ub) = bounds[j];
if !lb.is_finite() || !ub.is_finite() {
continue;
}
let width = ub - lb;
if width < 1.0 {
continue; }
if best.is_none_or(|(_, bw)| width > bw) {
best = Some((j, width));
}
}
best.map(|(j, _)| j)
}
pub(crate) fn split_integer_box(
bounds: &[(f64, f64)],
j: usize,
) -> (VarBounds, VarBounds) {
let (lb, ub) = bounds[j];
let mid = (0.5 * (lb + ub)).floor().max(lb).min(ub - 1.0);
let mut down = bounds.to_vec();
down[j].1 = mid;
let mut up = bounds.to_vec();
up[j].0 = mid + 1.0;
(down, up)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fractionality_measures_distance_to_nearest_integer() {
assert!((fractionality(2.0) - 0.0).abs() < 1e-15);
assert!((fractionality(2.5) - 0.5).abs() < 1e-15);
assert!((fractionality(2.3) - 0.3).abs() < 1e-12);
assert!((fractionality(2.7) - 0.3).abs() < 1e-12);
assert!((fractionality(-1.4) - 0.4).abs() < 1e-12);
}
#[test]
fn is_integer_respects_tolerance() {
assert!(is_integer(3.0000001, 1e-6));
assert!(!is_integer(3.1, 1e-6));
assert!(is_integer(-2.9999999, 1e-6));
}
#[test]
fn integer_feasible_ignores_continuous_components() {
let x = vec![1.5, 0.3];
assert!(!is_integer_feasible(&x, &[true, false], 1e-6));
let x2 = vec![1.0, 0.3];
assert!(is_integer_feasible(&x2, &[true, false], 1e-6));
}
#[test]
fn select_picks_most_fractional_then_lowest_index() {
let x = vec![1.2, 1.5, 2.5];
let j = select_branching_variable(&x, &[true, true, true], 1e-6, MipBranching::MostFractional)
.unwrap();
assert_eq!(j, 1);
}
#[test]
fn select_skips_continuous_and_integral_vars() {
let x = vec![0.5, 3.0, 2.4];
let j = select_branching_variable(&x, &[false, true, true], 1e-6, MipBranching::MostFractional)
.unwrap();
assert_eq!(j, 2);
}
#[test]
fn select_returns_none_when_all_integral() {
let x = vec![1.0, 2.0, 3.0];
assert!(
select_branching_variable(&x, &[true, true, true], 1e-6, MipBranching::MostFractional)
.is_none()
);
}
#[test]
fn branch_bounds_floor_and_ceil() {
let parent = vec![(0.0, 5.0), (0.0, 5.0)];
let (down, up) = branch_bounds(&parent, 0, 2.4);
assert_eq!(down[0], (0.0, 2.0)); assert_eq!(up[0], (3.0, 5.0)); assert_eq!(down[1], (0.0, 5.0));
assert_eq!(up[1], (0.0, 5.0));
}
#[test]
fn branch_bounds_can_produce_empty_box_for_pruning() {
let parent = vec![(3.0, 5.0)];
let (down, _up) = branch_bounds(&parent, 0, 3.5);
assert_eq!(down[0], (3.0, 3.0));
}
#[test]
fn widest_splittable_integer_picks_widest_nonsingleton() {
let bounds = vec![(0.0, 1.0), (3.0, 3.0), (0.0, 4.0)];
assert_eq!(widest_splittable_integer(&bounds, &[true, true, true]), Some(2));
}
#[test]
fn widest_splittable_integer_skips_continuous_and_singletons() {
let bounds = vec![(0.0, 10.0), (2.0, 2.0)];
assert!(widest_splittable_integer(&bounds, &[false, true]).is_none());
}
#[test]
fn split_integer_box_yields_two_nonempty_ranges() {
let bounds = vec![(0.0, 4.0)];
let (down, up) = split_integer_box(&bounds, 0);
assert_eq!(down[0], (0.0, 2.0)); assert_eq!(up[0], (3.0, 4.0));
}
#[test]
fn split_integer_box_binary_splits_to_singletons() {
let bounds = vec![(0.0, 1.0)];
let (down, up) = split_integer_box(&bounds, 0);
assert_eq!(down[0], (0.0, 0.0));
assert_eq!(up[0], (1.0, 1.0));
}
}