use crate::options::SolverOptions;
use crate::problem::{ConstraintType, SolveRoute, SolveStatus};
use crate::qp::problem::QpProblem;
use crate::sparse::CscMatrix;
fn micro_q_qp(qval: f64) -> QpProblem {
let q = CscMatrix::from_triplets(&[0], &[0], &[qval], 1, 1).unwrap();
QpProblem::new(
q,
vec![-1.0],
CscMatrix::new(0, 1),
vec![],
vec![(0.0, f64::INFINITY)],
vec![],
)
.unwrap()
}
#[test]
fn micro_q_psd_qp_is_bounded_and_routes_to_ipm() {
let opts = SolverOptions::default(); for qval in [1e-12, 5e-13, 1e-13, 1e-14] {
let p = micro_q_qp(qval);
assert!(
!p.is_zero_q(),
"Q={qval:e} is structurally non-zero; is_zero_q must be false"
);
let r = crate::qp::solve_qp_with(&p, &opts);
assert_ne!(
r.status,
SolveStatus::Unbounded,
"bounded micro-Q (Q={qval:e}) must NOT be Unbounded; got obj={}",
r.objective
);
assert_eq!(
r.stats.route,
SolveRoute::QpIpm,
"micro-Q must route to IPM (QpIpm), not the LP path, for Q={qval:e}"
);
let analytic_obj = -1.0 / (2.0 * qval);
assert!(
r.objective.is_finite(),
"objective must be finite for bounded Q={qval:e}; got {}",
r.objective
);
let rel = (r.objective - analytic_obj).abs() / analytic_obj.abs();
assert!(
rel < 1e-3,
"Q={qval:e}: obj {} must match analytic {analytic_obj} (rel={rel:e})",
r.objective
);
}
}
#[test]
fn structural_zero_q_keeps_lp_route_and_unbounded() {
let p = QpProblem::new(
CscMatrix::new(1, 1), vec![-1.0],
CscMatrix::new(0, 1),
vec![],
vec![(0.0, f64::INFINITY)],
vec![],
)
.unwrap();
assert!(p.is_zero_q(), "all-zero Q must be is_zero_q==true");
let r = crate::qp::solve_qp_with(&p, &SolverOptions::default());
assert_eq!(
r.stats.route,
SolveRoute::LpForwardedFromQp,
"structural-zero Q must forward to the LP path"
);
assert_eq!(
r.status,
SolveStatus::Unbounded,
"genuine LP (Q=0, c=-1, x≥0) is unbounded and must remain so"
);
}
#[test]
fn explicit_zero_q_value_is_structural_zero() {
let q_mixed = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[0.0_f64, 1.0], 2, 2).unwrap();
let p_mixed = QpProblem::new(
q_mixed,
vec![-1.0, -1.0],
CscMatrix::new(0, 2),
vec![],
vec![(0.0, f64::INFINITY), (0.0, f64::INFINITY)],
vec![],
)
.unwrap();
assert!(
!p_mixed.is_zero_q(),
"Q with a genuine 1.0 entry must not be is_zero_q"
);
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
let p_lp = QpProblem::new(
CscMatrix::new(1, 1),
vec![1.0],
a,
vec![2.0],
vec![(0.0, f64::INFINITY)],
vec![ConstraintType::Ge],
)
.unwrap();
assert!(p_lp.is_zero_q());
let r = crate::qp::solve_qp_with(&p_lp, &SolverOptions::default());
assert_eq!(r.status, SolveStatus::Optimal);
assert!((r.objective - 2.0).abs() < 1e-9, "min x s.t. x≥2 → obj=2");
}
#[test]
fn micro_q_finite_bound_not_pinned_to_bound() {
let q = CscMatrix::from_triplets(&[0], &[0], &[1e-13], 1, 1).unwrap();
let analytic_obj = -5e12; let ub = 2e13;
let empty_col = QpProblem::new(
q.clone(),
vec![-1.0],
CscMatrix::new(0, 1),
vec![],
vec![(0.0, ub)],
vec![],
)
.unwrap();
let singleton_le = QpProblem::new(
q,
vec![-1.0],
CscMatrix::from_triplets(&[0], &[0], &[-1.0], 1, 1).unwrap(),
vec![0.0],
vec![(0.0, ub)],
vec![ConstraintType::Le],
)
.unwrap();
for (label, p) in [("step11 empty-col", empty_col), ("step3 singleton-Le", singleton_le)] {
let r = crate::qp::solve_qp_with(&p, &SolverOptions::default());
assert_ne!(r.status, SolveStatus::Unbounded, "{label}: must not be Unbounded");
assert!(
r.objective < 0.5 * analytic_obj,
"{label}: obj {} must be near analytic {analytic_obj} (interior optimum), \
not ~0 (pinned to ub={ub})",
r.objective
);
assert!(
r.solution[0] < 0.9 * ub,
"{label}: x={} must be interior (≈1e13), not pinned at ub={ub}",
r.solution[0]
);
}
}