use crate::options::SolverOptions;
use crate::problem::{ConstraintType, LpProblem, SolveStatus};
use crate::qp::kkt_resid::f64_impl;
use crate::qp::QpProblem;
use crate::simplex::solve_with;
use crate::sparse::CscMatrix;
pub const EPS_KKT: f64 = 1e-6;
pub const EPS_OBJ_REL: f64 = 1e-6;
pub const MINI_TIMEOUT_SECS: f64 = 5.0;
const BOUND_TOL: f64 = 1e-6;
pub fn dfeas_rel_bound(
c: &[f64],
bounds: &[(f64, f64)],
x: &[f64],
rc: &[f64],
) -> f64 {
let n = c.len().min(rc.len()).min(x.len());
let mut max_rel = 0.0_f64;
for j in 0..n {
let (lb, ub) = bounds[j];
let fixed = lb.is_finite() && ub.is_finite() && (ub - lb).abs() < BOUND_TOL;
if fixed {
continue;
}
let at_lb = lb.is_finite() && (x[j] - lb).abs() < BOUND_TOL;
let at_ub = ub.is_finite() && (x[j] - ub).abs() < BOUND_TOL;
let r = rc[j];
let viol = if at_lb && !at_ub {
f64::max(0.0, -r)
} else if at_ub && !at_lb {
f64::max(0.0, r)
} else if !at_lb && !at_ub {
r.abs()
} else {
0.0
};
let scale = 1.0 + r.abs() + c[j].abs();
max_rel = max_rel.max(viol / scale);
}
max_rel
}
pub fn pfeas_abs(a: &CscMatrix, b: &[f64], cts: &[ConstraintType], x: &[f64]) -> f64 {
let ax = f64_impl::ax(a, x);
f64_impl::constraint_violations(&ax, b, cts)
.into_iter()
.fold(0.0_f64, f64::max)
}
pub fn pfeas_abs_bounds(bounds: &[(f64, f64)], x: &[f64]) -> f64 {
let mut max_v = 0.0_f64;
for j in 0..x.len() {
let (lb, ub) = bounds[j];
if lb.is_finite() && x[j] < lb {
max_v = max_v.max(lb - x[j]);
}
if ub.is_finite() && x[j] > ub {
max_v = max_v.max(x[j] - ub);
}
}
max_v
}
pub fn assert_solver_invariants_lp(result: &crate::problem::SolverResult, lp: &LpProblem) {
use crate::qp::certificate::{prove_optimal_lp, LP_CERT_TOL};
if result.status != crate::problem::SolveStatus::Optimal {
return;
}
if !lp.c.is_empty() {
assert!(
!result.solution.is_empty(),
"Optimal result must have non-empty solution"
);
}
let pf = pfeas_abs(&lp.a, &lp.b, &lp.constraint_types, &result.solution);
let b_inf = lp.b.iter().fold(0.0_f64, |a, &v| a.max(v.abs()));
let pf_norm = pf / (1.0 + b_inf);
assert!(
pf_norm < LP_CERT_TOL,
"Optimal result has excessive primal violation: pfeas={:.3e} normalized={:.3e} > {:.3e}",
pf,
pf_norm,
LP_CERT_TOL
);
let bv = pfeas_abs_bounds(&lp.bounds, &result.solution);
assert!(
bv < LP_CERT_TOL,
"Optimal result has bound violation={:.3e} > {:.3e}",
bv,
LP_CERT_TOL
);
match prove_optimal_lp(lp, result, LP_CERT_TOL) {
Ok(_) => {}
Err(not_proven) => {
panic!(
"Optimal LP result failed KKT: failing={:?} stat={:.3e} pres={:.3e} \
bviol={:.3e} comp={:.3e} dsign={:.3e} gap={:.3e}",
not_proven.failing_conditions,
not_proven.stationarity_rel,
not_proven.primal_residual_rel,
not_proven.bound_violation,
not_proven.complementarity_rel,
not_proven.dual_sign_violation,
not_proven.duality_gap_rel,
);
}
}
}
pub fn assert_kkt_optimal(lp: &LpProblem, expected_obj: f64, label: &'static str) {
let opts = SolverOptions { presolve: true, timeout_secs: Some(MINI_TIMEOUT_SECS), ..Default::default() };
assert_kkt_optimal_with(lp, expected_obj, label, &opts);
}
pub fn assert_kkt_optimal_with(
lp: &LpProblem,
expected_obj: f64,
label: &'static str,
opts: &SolverOptions,
) {
let r = solve_with(lp, opts);
assert_eq!(
r.status,
SolveStatus::Optimal,
"[{}] expected Optimal, got {:?} (obj={:.6e})",
label,
r.status,
r.objective
);
let bv = pfeas_abs_bounds(&lp.bounds, &r.solution);
assert!(
bv < EPS_KKT,
"[{}] bound violation={:.3e} > {:.3e} (x={:?})",
label,
bv,
EPS_KKT,
&r.solution
);
let pf = pfeas_abs(&lp.a, &lp.b, &lp.constraint_types, &r.solution);
assert!(
pf < EPS_KKT,
"[{}] pfeas={:.3e} > {:.3e} (x={:?})",
label,
pf,
EPS_KKT,
&r.solution
);
let df = dfeas_rel_bound(&lp.c, &lp.bounds, &r.solution, &r.reduced_costs);
assert!(
df < EPS_KKT,
"[{}] dfeas_rel_bound={:.3e} > {:.3e} | x={:?} rc={:?} y={:?}",
label,
df,
EPS_KKT,
&r.solution,
&r.reduced_costs,
&r.dual_solution
);
let obj_err = (r.objective - expected_obj).abs() / (1.0 + expected_obj.abs());
assert!(
obj_err < EPS_OBJ_REL,
"[{}] obj={:.9e} expected={:.9e} rel_err={:.3e} > {:.3e}",
label,
r.objective,
expected_obj,
obj_err,
EPS_OBJ_REL
);
}
pub fn assert_solver_invariants_qp(
result: &crate::problem::SolverResult,
qp: &QpProblem,
) {
use crate::problem::SolveStatus;
if !matches!(result.status, SolveStatus::Optimal | SolveStatus::LocallyOptimal) {
return;
}
assert!(
!result.solution.is_empty(),
"Optimal/LocallyOptimal QP result must have non-empty solution"
);
let pf = pfeas_abs(
&qp.a,
&qp.b,
&qp.constraint_types,
&result.solution,
);
let b_inf = qp.b.iter().fold(0.0_f64, |a, &v: &f64| a.max(v.abs()));
let pf_norm = pf / (1.0 + b_inf);
assert!(
pf_norm < EPS_KKT,
"QP Optimal result has excessive primal violation: pfeas={:.3e} norm={:.3e} > {:.3e}",
pf,
pf_norm,
EPS_KKT
);
let bv = pfeas_abs_bounds(&qp.bounds, &result.solution);
assert!(
bv < EPS_KKT,
"QP Optimal result has bound violation={:.3e} > {:.3e}",
bv,
EPS_KKT
);
use crate::qp::ipm_solver::kkt::kkt_residual_rel;
use crate::qp::ipm_solver::outcome::ProblemView;
let view = ProblemView::from_problem(qp);
let kkt = kkt_residual_rel(
&view,
&result.solution,
&result.dual_solution,
&result.bound_duals,
);
assert!(
kkt < EPS_KKT,
"QP Optimal result has KKT stationarity residual={:.3e} > {:.3e}",
kkt,
EPS_KKT
);
}
#[cfg(test)]
mod no_op_proof_tests {
use super::*;
use crate::problem::{SolveStatus, SolverResult};
use crate::sparse::CscMatrix;
#[test]
#[should_panic(expected = "primal violation")]
fn assert_solver_invariants_lp_panics_on_primal_violation() {
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
let lp = crate::problem::LpProblem::new_general(
vec![1.0],
a,
vec![5.0],
vec![ConstraintType::Le],
vec![(0.0, f64::INFINITY)],
None,
)
.unwrap();
let corrupt = SolverResult {
status: SolveStatus::Optimal,
solution: vec![1e12],
..Default::default()
};
assert_solver_invariants_lp(&corrupt, &lp);
}
#[test]
#[should_panic(expected = "bound violation")]
fn assert_solver_invariants_lp_panics_on_bound_violation() {
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
let lp = crate::problem::LpProblem::new_general(
vec![1.0],
a,
vec![1000.0],
vec![ConstraintType::Le],
vec![(0.0, 5.0)],
None,
)
.unwrap();
let corrupt = SolverResult {
status: SolveStatus::Optimal,
solution: vec![100.0],
..Default::default()
};
assert_solver_invariants_lp(&corrupt, &lp);
}
#[test]
#[should_panic(expected = "KKT")]
fn assert_solver_invariants_lp_panics_on_stationarity_violation() {
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
let lp = crate::problem::LpProblem::new_general(
vec![1.0],
a,
vec![1.0],
vec![ConstraintType::Ge],
vec![(0.0, f64::INFINITY)],
None,
)
.unwrap();
let corrupt = SolverResult {
status: SolveStatus::Optimal,
solution: vec![1.0],
dual_solution: vec![0.0], reduced_costs: vec![0.0],
..Default::default()
};
assert_solver_invariants_lp(&corrupt, &lp);
}
#[test]
#[should_panic(expected = "KKT")]
fn assert_solver_invariants_lp_panics_on_dual_sign_violation() {
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
let lp = crate::problem::LpProblem::new_general(
vec![-1.0],
a,
vec![1.0],
vec![ConstraintType::Le],
vec![(0.0, f64::INFINITY)],
None,
)
.unwrap();
let corrupt = SolverResult {
status: SolveStatus::Optimal,
solution: vec![1.0],
dual_solution: vec![1.0], reduced_costs: vec![0.0],
..Default::default()
};
assert_solver_invariants_lp(&corrupt, &lp);
}
#[test]
fn assert_solver_invariants_lp_passes_correct_result() {
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
let lp = crate::problem::LpProblem::new_general(
vec![1.0],
a,
vec![1.0],
vec![ConstraintType::Ge],
vec![(0.0, f64::INFINITY)],
None,
)
.unwrap();
let correct = SolverResult {
status: SolveStatus::Optimal,
solution: vec![1.0],
dual_solution: vec![1.0],
reduced_costs: vec![0.0],
..Default::default()
};
assert_solver_invariants_lp(&correct, &lp);
}
#[test]
#[should_panic(expected = "primal violation")]
fn assert_solver_invariants_qp_panics_on_primal_violation() {
use crate::qp::QpProblem;
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
let q = CscMatrix::from_triplets(&[0], &[0], &[2.0], 1, 1).unwrap();
let prob = QpProblem::new(
q,
vec![0.0],
a,
vec![1.0],
vec![(0.0, f64::INFINITY)],
vec![ConstraintType::Eq],
)
.unwrap();
let corrupt = SolverResult {
status: SolveStatus::Optimal,
solution: vec![1e12],
dual_solution: vec![0.0],
bound_duals: vec![0.0],
..Default::default()
};
assert_solver_invariants_qp(&corrupt, &prob);
}
#[test]
#[should_panic(expected = "bound violation")]
fn assert_solver_invariants_qp_panics_on_bound_violation() {
use crate::qp::QpProblem;
let a = CscMatrix::new(0, 1);
let q = CscMatrix::from_triplets(&[0], &[0], &[2.0], 1, 1).unwrap();
let prob = QpProblem::new(
q,
vec![0.0],
a,
vec![],
vec![(0.0, 1.0)],
vec![],
)
.unwrap();
let corrupt = SolverResult {
status: SolveStatus::Optimal,
solution: vec![5.0],
dual_solution: vec![],
bound_duals: vec![0.0, 0.0],
..Default::default()
};
assert_solver_invariants_qp(&corrupt, &prob);
}
#[test]
#[should_panic(expected = "KKT stationarity")]
fn assert_solver_invariants_qp_panics_on_stationarity_violation() {
use crate::qp::QpProblem;
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
let q = CscMatrix::from_triplets(&[0], &[0], &[2.0], 1, 1).unwrap();
let prob = QpProblem::new(
q,
vec![0.0],
a,
vec![1.0],
vec![(0.0, f64::INFINITY)],
vec![ConstraintType::Eq],
)
.unwrap();
let corrupt = SolverResult {
status: SolveStatus::Optimal,
solution: vec![1.0],
dual_solution: vec![0.0], bound_duals: vec![0.0],
..Default::default()
};
assert_solver_invariants_qp(&corrupt, &prob);
}
#[test]
#[should_panic(expected = "KKT stationarity")]
fn assert_solver_invariants_qp_locally_optimal_panics_on_stationarity() {
use crate::qp::QpProblem;
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
let q = CscMatrix::from_triplets(&[0], &[0], &[2.0], 1, 1).unwrap();
let prob = QpProblem::new(
q,
vec![0.0],
a,
vec![1.0],
vec![(0.0, f64::INFINITY)],
vec![ConstraintType::Eq],
)
.unwrap();
let corrupt = SolverResult {
status: SolveStatus::LocallyOptimal,
solution: vec![1.0],
dual_solution: vec![0.0],
bound_duals: vec![0.0],
..Default::default()
};
assert_solver_invariants_qp(&corrupt, &prob);
}
#[test]
fn assert_solver_invariants_qp_passthrough_non_optimal() {
use crate::qp::QpProblem;
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
let q = CscMatrix::from_triplets(&[0], &[0], &[2.0], 1, 1).unwrap();
let prob = QpProblem::new(
q,
vec![0.0],
a,
vec![1.0],
vec![(0.0, f64::INFINITY)],
vec![ConstraintType::Eq],
)
.unwrap();
for status in [SolveStatus::Infeasible, SolveStatus::Timeout, SolveStatus::NumericalError,
SolveStatus::MaxIterations, SolveStatus::SuboptimalSolution] {
let r = SolverResult {
status,
solution: vec![1e12], dual_solution: vec![0.0],
bound_duals: vec![0.0],
..Default::default()
};
assert_solver_invariants_qp(&r, &prob);
}
}
}