use crate::options::{QpWarmStart, SolverOptions};
use crate::problem::{SolveStatus, SolverResult};
use crate::qp::problem::QpProblem;
fn square_interval(a: f64, b: f64) -> (f64, f64) {
let aa = a * a;
let bb = b * b;
let min = if a <= 0.0 && b >= 0.0 {
0.0
} else {
aa.min(bb)
};
let max = aa.max(bb);
(min, max)
}
fn product_interval(a1: f64, b1: f64, a2: f64, b2: f64) -> (f64, f64) {
let c = [a1 * a2, a1 * b2, b1 * a2, b1 * b2];
let mut lo = c[0];
let mut hi = c[0];
for &v in c.iter().skip(1) {
if v < lo {
lo = v;
}
if v > hi {
hi = v;
}
}
(lo, hi)
}
pub(crate) fn interval_quadratic_bounds(problem: &QpProblem, bounds: &[(f64, f64)]) -> (f64, f64) {
let n = problem.num_vars;
debug_assert_eq!(bounds.len(), n, "bounds length mismatch");
let mut lo = problem.obj_offset;
let mut hi = problem.obj_offset;
for i in 0..n {
let (a, b) = bounds[i];
if !a.is_finite() || !b.is_finite() {
return (f64::NEG_INFINITY, f64::INFINITY);
}
let p = problem.c[i] * a;
let q = problem.c[i] * b;
lo += p.min(q);
hi += p.max(q);
}
for col in 0..n {
let (a_c, b_c) = bounds[col];
for k in problem.q.col_ptr[col]..problem.q.col_ptr[col + 1] {
let row = problem.q.row_ind[k];
let v = problem.q.values[k];
let coeff = 0.5 * v;
if row == col {
let (x2_min, x2_max) = square_interval(a_c, b_c);
let t1 = coeff * x2_min;
let t2 = coeff * x2_max;
lo += t1.min(t2);
hi += t1.max(t2);
} else {
let (a_r, b_r) = bounds[row];
let (p_min, p_max) = product_interval(a_r, b_r, a_c, b_c);
let t1 = coeff * p_min;
let t2 = coeff * p_max;
lo += t1.min(t2);
hi += t1.max(t2);
}
}
}
(lo, hi)
}
const WARM_BOUNDARY_REL_MARGIN: f64 = 1e-3;
fn warm_is_safe_for_box(warm: &QpWarmStart, bounds: &[(f64, f64)]) -> bool {
if warm.x.len() != bounds.len() {
return false;
}
for (xi, &(lb, ub)) in warm.x.iter().zip(bounds.iter()) {
if !lb.is_finite() || !ub.is_finite() {
continue;
}
let width = ub - lb;
if width <= 0.0 {
return false;
}
let margin = WARM_BOUNDARY_REL_MARGIN * width;
if *xi < lb + margin || *xi > ub - margin {
return false;
}
}
true
}
pub(crate) fn solve_local_upper_bound(
problem: &QpProblem,
node_bounds: &[(f64, f64)],
base_opts: &SolverOptions,
parent_warm: Option<&QpWarmStart>,
) -> SolverResult {
let mut sub = problem.clone();
sub.bounds = node_bounds.to_vec();
let mut opts = base_opts.clone();
opts.warm_start_qp = parent_warm.and_then(|w| {
if warm_is_safe_for_box(w, node_bounds) {
Some(w.clone())
} else {
None
}
});
opts.multistart = None;
opts.global_optimization = None;
crate::qp::solve_qp_with(&sub, &opts)
}
pub(crate) fn all_bounds_finite(bounds: &[(f64, f64)]) -> bool {
bounds
.iter()
.all(|&(l, u)| l.is_finite() && u.is_finite() && u >= l)
}
pub(crate) fn is_feasible_result(status: &SolveStatus) -> bool {
matches!(
status,
SolveStatus::Optimal
| SolveStatus::LocallyOptimal
| SolveStatus::SuboptimalSolution
| SolveStatus::MaxIterations
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sparse::CscMatrix;
fn build_diag_qp(diag: &[f64], c: &[f64], bounds: Vec<(f64, f64)>) -> QpProblem {
let n = diag.len();
let rows: Vec<usize> = (0..n).collect();
let cols: Vec<usize> = (0..n).collect();
let q = CscMatrix::from_triplets(&rows, &cols, diag, n, n).unwrap();
let a = CscMatrix::from_triplets(&[], &[], &[], 0, n).unwrap();
QpProblem::new_all_le(q, c.to_vec(), a, vec![], bounds).unwrap()
}
#[test]
fn square_interval_zero_crossing_yields_zero_min() {
let (lo, hi) = square_interval(-2.0, 3.0);
assert_eq!(lo, 0.0);
assert_eq!(hi, 9.0);
}
#[test]
fn square_interval_positive_box() {
let (lo, hi) = square_interval(1.0, 4.0);
assert_eq!(lo, 1.0);
assert_eq!(hi, 16.0);
}
#[test]
fn square_interval_negative_box() {
let (lo, hi) = square_interval(-4.0, -1.0);
assert_eq!(lo, 1.0);
assert_eq!(hi, 16.0);
}
#[test]
fn product_interval_general() {
let (lo, hi) = product_interval(-2.0, 1.0, -3.0, 4.0);
assert_eq!(lo, -8.0);
assert_eq!(hi, 6.0);
}
#[test]
fn convex_diag_qp_interval_matches_box_min() {
let p = build_diag_qp(&[2.0], &[0.0], vec![(-2.0, 3.0)]);
let (lo, hi) = interval_quadratic_bounds(&p, &p.bounds);
assert!((lo - 0.0).abs() < 1e-12, "lo={lo}");
assert!((hi - 9.0).abs() < 1e-12, "hi={hi}");
}
#[test]
fn concave_diag_qp_interval_min_at_corner() {
let p = build_diag_qp(&[-2.0], &[0.0], vec![(-2.0, 3.0)]);
let (lo, hi) = interval_quadratic_bounds(&p, &p.bounds);
assert!((lo - (-9.0)).abs() < 1e-12, "lo={lo}");
assert!((hi - 0.0).abs() < 1e-12, "hi={hi}");
}
#[test]
fn linear_objective_contributes() {
let p = build_diag_qp(&[0.0, 0.0], &[2.0, -3.0], vec![(-1.0, 1.0); 2]);
let (lo, hi) = interval_quadratic_bounds(&p, &p.bounds);
assert!((lo - (-5.0)).abs() < 1e-12);
assert!((hi - 5.0).abs() < 1e-12);
}
#[test]
fn bilinear_off_diagonal_handled() {
let q = CscMatrix::from_triplets(&[0, 1], &[1, 0], &[1.0, 1.0], 2, 2).unwrap();
let a = CscMatrix::from_triplets(&[], &[], &[], 0, 2).unwrap();
let p = QpProblem::new_all_le(q, vec![0.0, 0.0], a, vec![], vec![(-1.0, 1.0); 2]).unwrap();
let (lo, hi) = interval_quadratic_bounds(&p, &p.bounds);
assert!((lo - (-1.0)).abs() < 1e-12, "lo={lo}");
assert!((hi - 1.0).abs() < 1e-12, "hi={hi}");
}
#[test]
fn infinite_bound_returns_unbounded_interval() {
let p = build_diag_qp(&[1.0], &[0.0], vec![(f64::NEG_INFINITY, 1.0)]);
let (lo, hi) = interval_quadratic_bounds(&p, &p.bounds);
assert!(lo.is_infinite() && lo < 0.0);
assert!(hi.is_infinite() && hi > 0.0);
}
#[test]
fn warm_safe_when_x_well_inside_box() {
let warm = QpWarmStart {
x: vec![0.5, -0.3],
y: vec![],
mu: 1e-6,
};
assert!(warm_is_safe_for_box(&warm, &[(0.0, 1.0), (-1.0, 1.0)]));
}
#[test]
fn warm_unsafe_when_x_on_boundary() {
let warm = QpWarmStart {
x: vec![0.0001, 0.0],
y: vec![],
mu: 1e-6,
};
assert!(!warm_is_safe_for_box(&warm, &[(0.0, 1.0), (-1.0, 1.0)]));
}
#[test]
fn warm_unsafe_when_x_outside_box_post_branching() {
let warm = QpWarmStart {
x: vec![1.0, 0.0],
y: vec![],
mu: 1e-6,
};
assert!(!warm_is_safe_for_box(&warm, &[(-1.0, 0.0), (-1.0, 1.0)]));
}
#[test]
fn feasibility_classifier_covers_finite_obj_statuses() {
for s in [
SolveStatus::Optimal,
SolveStatus::LocallyOptimal,
SolveStatus::SuboptimalSolution,
SolveStatus::MaxIterations,
] {
assert!(is_feasible_result(&s), "{s:?} should be feasible");
}
for s in [
SolveStatus::Infeasible,
SolveStatus::Unbounded,
SolveStatus::NumericalError,
SolveStatus::Timeout,
SolveStatus::NonConvex("x".into()),
] {
assert!(!is_feasible_result(&s), "{s:?} should NOT be feasible");
}
}
}