use std::time::Instant;
use crate::linalg::gershgorin::psd_shift_from_gershgorin;
use crate::options::SolverOptions;
use crate::problem::SolverResult;
use crate::qp::problem::QpProblem;
use crate::sparse::CscMatrix;
use super::bound::{all_bounds_finite, is_feasible_result};
pub(crate) fn gershgorin_alpha(q: &CscMatrix) -> f64 {
0.5 * psd_shift_from_gershgorin(q)
}
fn add_scalar_to_diagonal(q: &CscMatrix, value: f64) -> CscMatrix {
let n = q.nrows;
debug_assert_eq!(q.nrows, q.ncols, "Q must be square");
let mut rows = Vec::with_capacity(q.values.len() + n);
let mut cols = Vec::with_capacity(q.values.len() + n);
let mut vals = Vec::with_capacity(q.values.len() + n);
for col in 0..n {
for k in q.col_ptr[col]..q.col_ptr[col + 1] {
rows.push(q.row_ind[k]);
cols.push(col);
vals.push(q.values[k]);
}
rows.push(col);
cols.push(col);
vals.push(value);
}
CscMatrix::from_triplets(&rows, &cols, &vals, n, n).expect("from_triplets diag add")
}
fn build_convex_relaxation(
problem: &QpProblem,
node_bounds: &[(f64, f64)],
alpha: f64,
) -> QpProblem {
let n = problem.num_vars;
debug_assert_eq!(node_bounds.len(), n);
let mut sub = problem.clone();
sub.q = add_scalar_to_diagonal(&problem.q, 2.0 * alpha);
let mut new_offset = problem.obj_offset;
for i in 0..n {
let (l, u) = node_bounds[i];
sub.c[i] = problem.c[i] - alpha * (l + u);
new_offset += alpha * l * u;
}
sub.obj_offset = new_offset;
sub.bounds = node_bounds.to_vec();
sub
}
pub(crate) fn alpha_bb_lower_bound(
problem: &QpProblem,
node_bounds: &[(f64, f64)],
alpha: f64,
base_opts: &SolverOptions,
deadline: Option<Instant>,
) -> Option<f64> {
if alpha <= 0.0 {
return None;
}
if !all_bounds_finite(node_bounds) {
return None;
}
let sub = build_convex_relaxation(problem, node_bounds, alpha);
let mut opts = base_opts.clone();
opts.multistart = None;
opts.global_optimization = None;
opts.warm_start = None;
opts.warm_start_qp = None;
opts.warm_start_lp = None;
opts.deadline = deadline;
let res: SolverResult = crate::qp::solve_qp_with(&sub, &opts);
if !is_feasible_result(&res.status) {
return None;
}
Some(res.objective)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::options::SolverOptions;
use crate::problem::ConstraintType;
use crate::sparse::CscMatrix;
fn build_problem(
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(q, c.to_vec(), a, vec![], bounds, vec![]).unwrap()
}
#[test]
fn gershgorin_alpha_zero_on_psd() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
assert_eq!(gershgorin_alpha(&q), 0.0);
}
#[test]
fn gershgorin_alpha_positive_on_concave_diag() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[-2.0, -2.0], 2, 2).unwrap();
assert!((gershgorin_alpha(&q) - 1.0).abs() < 1e-12);
}
#[test]
fn gershgorin_alpha_covers_off_diag_full_symmetric() {
let q = CscMatrix::from_triplets(&[0, 1], &[1, 0], &[1.0, 1.0], 2, 2).unwrap();
let a = gershgorin_alpha(&q);
assert!(a >= 0.5 - 1e-12, "alpha must be at least 0.5, got {a}");
}
#[test]
fn add_scalar_to_diagonal_inserts_missing_diag() {
let q = CscMatrix::from_triplets(&[], &[], &[], 2, 2).unwrap();
let r = add_scalar_to_diagonal(&q, 3.0);
assert_eq!(r.nnz(), 2);
for col in 0..2 {
let (ri, vi) = r.get_column(col).unwrap();
assert_eq!(ri, &[col]);
assert!((vi[0] - 3.0).abs() < 1e-12);
}
}
#[test]
fn add_scalar_to_diagonal_sums_existing_diag() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[1.0, 2.0], 2, 2).unwrap();
let r = add_scalar_to_diagonal(&q, 5.0);
let (_r0, v0) = r.get_column(0).unwrap();
let (_r1, v1) = r.get_column(1).unwrap();
assert!((v0[0] - 6.0).abs() < 1e-12);
assert!((v1[0] - 7.0).abs() < 1e-12);
}
fn eval_obj(p: &QpProblem, x: &[f64]) -> f64 {
let qx = p.q.mat_vec_mul(x).expect("mat_vec_mul");
let xtqx: f64 = x.iter().zip(qx.iter()).map(|(xi, qxi)| xi * qxi).sum();
let ctx: f64 = x.iter().zip(p.c.iter()).map(|(xi, ci)| xi * ci).sum();
0.5 * xtqx + ctx + p.obj_offset
}
#[test]
fn relaxation_matches_original_at_corners() {
let problem = build_problem(&[-2.0], &[0.0], vec![(-2.0, 2.0)]);
let alpha = gershgorin_alpha(&problem.q);
assert!(alpha > 0.0);
let sub = build_convex_relaxation(&problem, &problem.bounds, alpha);
let f_orig = eval_obj(&problem, &[2.0]);
let l_relax = eval_obj(&sub, &[2.0]);
assert!(
(l_relax - f_orig).abs() < 1e-9,
"L at corner should equal f: f={f_orig}, L={l_relax}"
);
let f_lo = eval_obj(&problem, &[-2.0]);
let l_lo = eval_obj(&sub, &[-2.0]);
assert!((l_lo - f_lo).abs() < 1e-9, "L at lower corner: f={f_lo}, L={l_lo}");
}
#[test]
fn relaxation_underestimates_strictly_in_interior() {
let problem = build_problem(&[-2.0], &[0.0], vec![(-2.0, 2.0)]);
let alpha = gershgorin_alpha(&problem.q);
let sub = build_convex_relaxation(&problem, &problem.bounds, alpha);
let f_mid = eval_obj(&problem, &[0.0]);
let l_mid = eval_obj(&sub, &[0.0]);
assert!(
l_mid <= f_mid + 1e-12,
"L({{0}})={l_mid} must underestimate f({{0}})={f_mid}"
);
assert!(
(l_mid - f_mid - (-4.0)).abs() < 1e-9,
"L(0)-f(0) should be -4, got {}",
l_mid - f_mid
);
}
#[test]
fn alpha_bb_returns_none_for_infinite_box() {
let problem = build_problem(&[-2.0], &[0.0], vec![(f64::NEG_INFINITY, 1.0)]);
let alpha = 1.0;
let opts = SolverOptions::default();
assert!(alpha_bb_lower_bound(&problem, &problem.bounds, alpha, &opts, None).is_none());
}
#[test]
fn alpha_bb_returns_none_for_zero_alpha() {
let problem = build_problem(&[2.0], &[0.0], vec![(-1.0, 1.0)]);
let opts = SolverOptions::default();
assert!(alpha_bb_lower_bound(&problem, &problem.bounds, 0.0, &opts, None).is_none());
}
#[test]
fn alpha_bb_yields_finite_lb_for_concave_box() {
let problem = build_problem(&[-2.0], &[0.0], vec![(-2.0, 2.0)]);
let alpha = gershgorin_alpha(&problem.q);
let opts = SolverOptions::default();
let lb = alpha_bb_lower_bound(&problem, &problem.bounds, alpha, &opts, None)
.expect("convex relaxation must solve on bounded concave 1d");
assert!(lb.is_finite(), "lb must be finite, got {lb}");
assert!(lb <= -4.0 + 1e-6, "lb must be ≤ global -4, got {lb}");
}
#[test]
fn alpha_bb_lb_tightens_as_box_shrinks() {
let problem_wide = build_problem(&[-2.0], &[0.0], vec![(-2.0, 2.0)]);
let alpha = gershgorin_alpha(&problem_wide.q);
let opts = SolverOptions::default();
let lb_wide = alpha_bb_lower_bound(&problem_wide, &[(-2.0, 2.0)], alpha, &opts, None)
.expect("wide");
let lb_narrow = alpha_bb_lower_bound(&problem_wide, &[(0.0, 1.0)], alpha, &opts, None)
.expect("narrow");
assert!(
lb_narrow >= lb_wide - 1e-9,
"narrow lb ({lb_narrow}) should not be worse than wide ({lb_wide})"
);
}
#[test]
fn alpha_bb_with_linear_eq_constraint() {
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[-2.0, -2.0], 2, 2).unwrap();
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
let p = QpProblem::new(
q,
vec![0.0, 0.0],
a,
vec![1.0],
vec![(0.0, 1.0); 2],
vec![ConstraintType::Eq],
)
.unwrap();
let alpha = gershgorin_alpha(&p.q);
let opts = SolverOptions::default();
let lb = alpha_bb_lower_bound(&p, &p.bounds, alpha, &opts, None)
.expect("constrained α-BB must solve");
assert!(lb.is_finite() && lb <= -1.0 + 1e-6,
"lb must be ≤ -1 (global) and finite, got {lb}");
}
}