use super::params::{PSEUDOCOST_INIT_EPS, SCORE_EPS};
use crate::solver::Solver;
use crate::VarDomain;
fn fractionality(val: f64) -> f64 {
(val - val.round()).abs()
}
fn is_int_domain(d: &VarDomain) -> bool {
matches!(d, VarDomain::Integer | VarDomain::Boolean)
}
pub(crate) fn is_integral(solver: &Solver, domains: &[VarDomain], int_tol: f64) -> bool {
domains
.iter()
.enumerate()
.filter(|(_, d)| is_int_domain(d))
.all(|(v, _)| fractionality(*solver.get_value(v)) <= int_tol)
}
#[derive(Clone, Debug)]
pub(crate) struct PseudoCosts {
up_sum: Vec<f64>,
up_n: Vec<u32>,
down_sum: Vec<f64>,
down_n: Vec<u32>,
init: Vec<f64>,
}
impl PseudoCosts {
pub(crate) fn new(obj_coeffs: &[f64], num_vars: usize) -> Self {
let init = (0..num_vars)
.map(|v| obj_coeffs.get(v).copied().unwrap_or(0.0).abs() + PSEUDOCOST_INIT_EPS)
.collect();
Self {
up_sum: vec![0.0; num_vars],
up_n: vec![0; num_vars],
down_sum: vec![0.0; num_vars],
down_n: vec![0; num_vars],
init,
}
}
pub(crate) fn record(&mut self, var: usize, up: bool, degradation_per_unit: f64) {
if up {
self.up_sum[var] += degradation_per_unit;
self.up_n[var] += 1;
} else {
self.down_sum[var] += degradation_per_unit;
self.down_n[var] += 1;
}
}
pub(crate) fn estimate(&self, var: usize, up: bool) -> f64 {
let (sum, n) = if up {
(self.up_sum[var], self.up_n[var])
} else {
(self.down_sum[var], self.down_n[var])
};
if n > 0 {
sum / n as f64
} else {
self.init[var]
}
}
pub(crate) fn observation_count(&self) -> u64 {
self.up_n.iter().map(|&n| u64::from(n)).sum::<u64>()
+ self.down_n.iter().map(|&n| u64::from(n)).sum::<u64>()
}
}
pub(crate) fn choose_branch_var(
solver: &Solver,
domains: &[VarDomain],
int_tol: f64,
pc: &PseudoCosts,
) -> Option<usize> {
let mut best: Option<(usize, f64)> = None;
for (v, d) in domains.iter().enumerate() {
if !is_int_domain(d) {
continue;
}
let (lo, hi) = solver.get_var_bounds(v);
if hi - lo < 0.5 {
continue;
}
let val = *solver.get_value(v);
if fractionality(val) <= int_tol {
continue;
}
let f_down = val - val.floor();
let f_up = 1.0 - f_down;
let score = (pc.estimate(v, false) * f_down).max(SCORE_EPS)
* (pc.estimate(v, true) * f_up).max(SCORE_EPS);
if best.is_none_or(|(_, s)| score > s) {
best = Some((v, score));
}
}
best.map(|(v, _)| v)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::solver::Solver;
use crate::{ComparisonOp, VarDomain};
fn to_sparse(values: &[f64]) -> crate::CsVec {
let mut indices = vec![];
let mut data = vec![];
for (i, &v) in values.iter().enumerate() {
if v != 0.0 {
indices.push(i);
data.push(v);
}
}
crate::CsVec::new(values.len(), indices, data)
}
#[test]
fn most_fractional_var_is_chosen() {
let mut solver = Solver::try_new(
&[-1.0, -1.0],
&[0.0, 0.0],
&[1.9, 10.0],
&[(to_sparse(&[1.0, 2.0]), ComparisonOp::Le, 3.2)],
&[VarDomain::Integer, VarDomain::Integer],
None,
)
.unwrap();
solver.initial_solve().unwrap();
assert!(!is_integral(
&solver,
solver.orig_var_domains.clone().as_slice(),
1e-6
));
let pc = PseudoCosts::new(&[-1.0, -1.0], 2);
assert_eq!(
choose_branch_var(
&solver,
solver.orig_var_domains.clone().as_slice(),
1e-6,
&pc
),
Some(1)
);
}
#[test]
fn integral_solution_yields_no_branch_var() {
let mut solver = Solver::try_new(
&[1.0],
&[0.0],
&[10.0],
&[(to_sparse(&[2.0]), ComparisonOp::Ge, 4.0)],
&[VarDomain::Integer],
None,
)
.unwrap();
solver.initial_solve().unwrap();
assert!(is_integral(
&solver,
solver.orig_var_domains.clone().as_slice(),
1e-6
));
let pc = PseudoCosts::new(&[1.0], 1);
assert_eq!(
choose_branch_var(
&solver,
solver.orig_var_domains.clone().as_slice(),
1e-6,
&pc
),
None
);
}
#[test]
fn pseudocosts_average_and_fall_back_to_init() {
let mut pc = PseudoCosts::new(&[3.0, 0.0], 2);
assert!((pc.estimate(0, true) - 3.0).abs() < 1e-3);
pc.record(0, true, 10.0);
pc.record(0, true, 20.0);
assert!((pc.estimate(0, true) - 15.0).abs() < 1e-9); assert!((pc.estimate(0, false) - 3.0).abs() < 1e-3); }
#[test]
fn pseudocost_selection_prefers_high_degradation_var() {
let mut solver = Solver::try_new(
&[-1.0, -1.0],
&[0.0, 0.0],
&[1.5, 10.0],
&[(to_sparse(&[1.0, 2.0]), ComparisonOp::Le, 3.0)],
&[VarDomain::Integer, VarDomain::Integer],
None,
)
.unwrap();
solver.initial_solve().unwrap();
let mut pc = PseudoCosts::new(&[-1.0, -1.0], 2);
pc.record(1, true, 100.0);
pc.record(1, false, 100.0);
let domains = solver.orig_var_domains.clone();
assert_eq!(choose_branch_var(&solver, &domains, 1e-6, &pc), Some(1));
}
}