use crate::problem::certificate::{NotProven, OptimalCertificate};
use crate::problem::{LpProblem, SolveStatus, SolverResult};
use crate::qp::ipm_solver::kkt::{
bound_violation, complementarity_componentwise_rel, complementarity_residual_rel,
kkt_residual_rel, primal_residual_rel,
};
use crate::qp::ipm_solver::outcome::ProblemView;
use crate::qp::kkt_resid::dual_sign_violation;
use crate::sparse::CscMatrix;
pub fn prove_optimal<'a>(
view: &ProblemView<'a>,
x: &[f64],
y: &[f64],
z: &[f64],
duality_gap_rel: f64,
tol: f64,
) -> Result<OptimalCertificate, NotProven> {
let num_vars = view.bounds.len();
let num_constraints = view.a.nrows;
let n_lb = view
.bounds
.iter()
.filter(|&&(lb, _)| lb.is_finite())
.count();
let n_ub = view
.bounds
.iter()
.filter(|&&(_, ub)| ub.is_finite())
.count();
let dim_valid = view.q.nrows == num_vars
&& view.q.ncols == num_vars
&& view.a.ncols == num_vars
&& view.b.len() == num_constraints
&& view.c.len() == num_vars
&& view.constraint_types.len() == num_constraints
&& x.len() == num_vars
&& y.len() == num_constraints
&& z.len() == n_lb + n_ub;
if !dim_valid {
return Err(NotProven {
stationarity_rel: f64::NAN,
primal_residual_rel: f64::NAN,
bound_violation: f64::NAN,
complementarity_rel: f64::NAN,
dual_sign_violation: f64::NAN,
duality_gap_rel: f64::NAN,
tol,
failing_conditions: vec!["input_dimensions"],
});
}
if !duality_gap_rel.is_finite() || duality_gap_rel < 0.0 {
return Err(NotProven {
stationarity_rel: f64::NAN,
primal_residual_rel: f64::NAN,
bound_violation: f64::NAN,
complementarity_rel: f64::NAN,
dual_sign_violation: f64::NAN,
duality_gap_rel,
tol,
failing_conditions: vec!["duality_gap"],
});
}
if !tol.is_finite() || tol <= 0.0 {
return Err(NotProven {
stationarity_rel: f64::NAN,
primal_residual_rel: f64::NAN,
bound_violation: f64::NAN,
complementarity_rel: f64::NAN,
dual_sign_violation: f64::NAN,
duality_gap_rel,
tol,
failing_conditions: vec!["invalid_tolerance"],
});
}
let stat = kkt_residual_rel(view, x, y, z);
let pres = primal_residual_rel(view, x);
let bviol = bound_violation(view.bounds, x);
let comp = complementarity_residual_rel(view, x, y, z)
.max(complementarity_componentwise_rel(view, x, y, z));
let dsign = dual_sign_violation(view.constraint_types, y, view.bounds, z);
let gap = duality_gap_rel;
let mut failing: Vec<&'static str> = Vec::new();
for (name, val) in [
("stationarity", stat),
("primal_feasibility", pres),
("bound_feasibility", bviol),
("complementarity", comp),
("dual_sign", dsign),
("duality_gap", gap),
] {
#[allow(clippy::neg_cmp_op_on_partial_ord)]
if !(val <= tol) {
failing.push(name);
}
}
if failing.is_empty() {
Ok(OptimalCertificate::new(stat, pres, dsign, gap, tol))
} else {
Err(NotProven {
stationarity_rel: stat,
primal_residual_rel: pres,
bound_violation: bviol,
complementarity_rel: comp,
dual_sign_violation: dsign,
duality_gap_rel: gap,
tol,
failing_conditions: failing,
})
}
}
pub(crate) const LP_CERT_TOL: f64 = 1e-4;
pub(crate) fn prove_optimal_lp(
problem: &LpProblem,
result: &SolverResult,
tol: f64,
) -> Result<OptimalCertificate, NotProven> {
let n = problem.num_vars;
let q_zero = CscMatrix::new(n, n);
let view = ProblemView {
q: &q_zero,
a: &problem.a,
c: &problem.c,
b: &problem.b,
bounds: &problem.bounds,
constraint_types: &problem.constraint_types,
eliminated_cols: &[],
};
let (y_prove, z) = if result.reduced_costs.is_empty() {
(result.dual_solution.clone(), result.bound_duals.clone())
} else {
let y_prove: Vec<f64> = result.dual_solution.iter().map(|&v| -v).collect();
let rc = &result.reduced_costs;
let mut z_lb = Vec::new();
let mut z_ub = Vec::new();
for (j, &(lb, ub)) in problem.bounds.iter().enumerate() {
let rc_j = rc.get(j).copied().unwrap_or(0.0);
if lb.is_finite() {
z_lb.push(rc_j.max(0.0));
}
if ub.is_finite() {
z_ub.push((-rc_j).max(0.0));
}
}
z_lb.extend(z_ub);
(y_prove, z_lb)
};
let gap_rel = lp_duality_gap_rel(problem, &result.solution, &y_prove, &z);
prove_optimal(&view, &result.solution, &y_prove, &z, gap_rel, tol)
}
fn lp_duality_gap_rel(problem: &LpProblem, x: &[f64], y_prove: &[f64], z: &[f64]) -> f64 {
let primal_obj: f64 = problem.c.iter().zip(x.iter()).map(|(&c, &xj)| c * xj).sum();
let by: f64 = problem
.b
.iter()
.zip(y_prove.iter())
.map(|(&b, &y)| b * y)
.sum();
let n_lb = problem
.bounds
.iter()
.filter(|&&(lb, _)| lb.is_finite())
.count();
let mut bnd_term = 0.0_f64;
let mut lb_idx = 0_usize;
let mut ub_idx = n_lb;
for &(lb, ub) in problem.bounds.iter() {
if lb.is_finite() && lb_idx < z.len() {
bnd_term += lb * z[lb_idx];
lb_idx += 1;
}
if ub.is_finite() && ub_idx < z.len() {
bnd_term -= ub * z[ub_idx];
ub_idx += 1;
}
}
let dual_obj = -by + bnd_term;
let gap_abs = (primal_obj - dual_obj).abs();
let denom = primal_obj.abs().max(dual_obj.abs()).max(1.0);
if gap_abs.is_finite() {
gap_abs / denom
} else {
f64::INFINITY
}
}
pub(crate) fn guard_lp_optimal(result: SolverResult, problem: &LpProblem) -> SolverResult {
if result.status != SolveStatus::Optimal || result.solution.is_empty() {
return result;
}
match prove_optimal_lp(problem, &result, LP_CERT_TOL) {
Ok(_) => result,
Err(_) => SolverResult {
status: SolveStatus::SuboptimalSolution,
..result
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::problem::ConstraintType;
use crate::qp::ipm_solver::outcome::ProblemView;
use crate::sparse::CscMatrix;
fn trivial_view<'a>(
q: &'a CscMatrix,
a: &'a CscMatrix,
c: &'a [f64],
b: &'a [f64],
bounds: &'a [(f64, f64)],
ct: &'a [ConstraintType],
) -> ProblemView<'a> {
ProblemView {
q,
a,
c,
b,
bounds,
constraint_types: ct,
eliminated_cols: &[],
}
}
#[test]
fn prove_optimal_exact_kkt_passes() {
let q = CscMatrix::new(1, 1);
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0_f64], 1, 1).unwrap();
let c = vec![1.0_f64];
let b = vec![1.0_f64];
let bounds = vec![(0.0_f64, f64::INFINITY)];
let ct = vec![ConstraintType::Ge];
let view = trivial_view(&q, &a, &c, &b, &bounds, &ct);
let x = vec![1.0_f64]; let y = vec![-1.0_f64]; let z = vec![0.0_f64]; let gap = 0.0_f64;
let result = prove_optimal(&view, &x, &y, &z, gap, 1e-6);
assert!(result.is_ok(), "exact KKT must pass: {:?}", result.err());
let cert = result.unwrap();
assert!(cert.stationarity_rel() < 1e-6);
assert!(cert.primal_residual_rel() < 1e-6);
assert!(cert.dual_sign_violation() < 1e-6);
assert!(cert.duality_gap_rel() < 1e-6);
}
#[test]
fn prove_optimal_corrupted_x_fails() {
let q = CscMatrix::new(1, 1);
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0_f64], 1, 1).unwrap();
let c = vec![1.0_f64];
let b = vec![1.0_f64];
let bounds = vec![(0.0_f64, f64::INFINITY)];
let ct = vec![ConstraintType::Ge];
let view = trivial_view(&q, &a, &c, &b, &bounds, &ct);
let x_bad = vec![2.0_f64]; let y = vec![-1.0_f64];
let z = vec![0.0_f64]; let gap = 1.0_f64;
let result = prove_optimal(&view, &x_bad, &y, &z, gap, 1e-6);
assert!(result.is_err(), "corrupted iterate must fail prove_optimal");
let err = result.unwrap_err();
assert!(!err.failing_conditions.is_empty());
assert!(err.failing_conditions.contains(&"duality_gap"));
}
#[test]
fn prove_optimal_wrong_sign_dual_fails_dual_sign_check() {
let q = CscMatrix::new(1, 1);
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0_f64], 1, 1).unwrap();
let c = vec![-1.0_f64]; let b = vec![1.0_f64];
let bounds = vec![(0.0_f64, f64::INFINITY)];
let ct = vec![ConstraintType::Le];
let view = trivial_view(&q, &a, &c, &b, &bounds, &ct);
let x = vec![1.0_f64];
let y_wrong = vec![-1.0_f64]; let z = vec![0.0_f64]; let gap = 0.0_f64;
let result = prove_optimal(&view, &x, &y_wrong, &z, gap, 1e-6);
assert!(result.is_err(), "wrong-sign dual must fail");
let err = result.unwrap_err();
assert!(
err.failing_conditions.contains(&"dual_sign"),
"dual_sign must be in failing_conditions: {:?}",
err.failing_conditions
);
assert!(err.dual_sign_violation > 0.0);
}
#[test]
fn prove_optimal_stationarity_fails() {
let q = CscMatrix::new(1, 1);
let a = CscMatrix::new(0, 1);
let c = vec![1.0_f64];
let b: Vec<f64> = vec![];
let bounds = vec![(0.0_f64, f64::INFINITY)];
let ct: Vec<ConstraintType> = vec![];
let view = trivial_view(&q, &a, &c, &b, &bounds, &ct);
let x = vec![0.0_f64];
let y: Vec<f64> = vec![];
let z_bad = vec![0.0_f64]; let gap = 0.0_f64;
let result = prove_optimal(&view, &x, &y, &z_bad, gap, 1e-6);
assert!(result.is_err(), "stationarity violation must fail");
let err = result.unwrap_err();
assert!(
err.failing_conditions.contains(&"stationarity"),
"stationarity must fail: {:?}",
err.failing_conditions
);
}
#[test]
fn prove_optimal_tol_semantics() {
let q = CscMatrix::new(1, 1);
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0_f64], 1, 1).unwrap();
let c = vec![1.0_f64];
let b = vec![1.0_f64];
let bounds = vec![(0.0_f64, f64::INFINITY)];
let ct = vec![ConstraintType::Ge];
let view = trivial_view(&q, &a, &c, &b, &bounds, &ct);
let x = vec![1.0_f64];
let y = vec![-1.0_f64];
let z = vec![0.0_f64];
assert!(prove_optimal(&view, &x, &y, &z, 0.0, 1e-10).is_ok());
assert!(prove_optimal(&view, &x, &y, &z, 0.0, 1e-14).is_ok());
let result_gap_fail = prove_optimal(&view, &x, &y, &z, 1e-5, 1e-6);
assert!(result_gap_fail.is_err());
assert!(result_gap_fail
.unwrap_err()
.failing_conditions
.contains(&"duality_gap"));
}
#[test]
fn prove_optimal_empty_problem() {
let q = CscMatrix::new(0, 0);
let a = CscMatrix::new(0, 0);
let c: Vec<f64> = vec![];
let b: Vec<f64> = vec![];
let bounds: Vec<(f64, f64)> = vec![];
let ct: Vec<ConstraintType> = vec![];
let view = trivial_view(&q, &a, &c, &b, &bounds, &ct);
let result = prove_optimal(&view, &[], &[], &[], 0.0, 1e-6);
assert!(
result.is_ok(),
"empty problem must pass: {:?}",
result.err()
);
}
#[test]
fn prove_optimal_nan_in_each_condition_is_rejected() {
let q = CscMatrix::new(1, 1);
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0_f64], 1, 1).unwrap();
let c = vec![1.0_f64];
let b = vec![1.0_f64];
let bounds = vec![(0.0_f64, f64::INFINITY)];
let ct = vec![ConstraintType::Ge];
{
let view = trivial_view(&q, &a, &c, &b, &bounds, &ct);
let x = vec![1.0_f64];
let y = vec![-1.0_f64];
let z = vec![0.0_f64]; let result = prove_optimal(&view, &x, &y, &z, f64::NAN, 1e-6);
assert!(result.is_err(), "NaN gap must be rejected");
let err = result.unwrap_err();
assert!(
err.failing_conditions.contains(&"duality_gap"),
"NaN gap: duality_gap must be in failing_conditions, got {:?}",
err.failing_conditions
);
}
{
let view = trivial_view(&q, &a, &c, &b, &bounds, &ct);
let x = vec![1.0_f64];
let y = vec![-1.0_f64];
let z = vec![0.0_f64]; let result = prove_optimal(&view, &x, &y, &z, f64::INFINITY, 1e-6);
assert!(result.is_err(), "+Inf gap must be rejected");
let err = result.unwrap_err();
assert!(
err.failing_conditions.contains(&"duality_gap"),
"+Inf gap: duality_gap must be in failing_conditions, got {:?}",
err.failing_conditions
);
}
{
let view = trivial_view(&q, &a, &c, &b, &bounds, &ct);
let x_nan = vec![f64::NAN];
let y = vec![-1.0_f64];
let z = vec![0.0_f64]; let result = prove_optimal(&view, &x_nan, &y, &z, 0.0, 1e-6);
assert!(result.is_err(), "NaN x must be rejected");
let err = result.unwrap_err();
assert!(
!err.failing_conditions.is_empty(),
"NaN x: at least one condition must fail, got {:?}",
err.failing_conditions
);
}
{
let view = trivial_view(&q, &a, &c, &b, &bounds, &ct);
let x_nan = vec![f64::NAN];
let y_nan = vec![f64::NAN];
let z = vec![0.0_f64]; let result = prove_optimal(&view, &x_nan, &y_nan, &z, f64::NAN, 1e-6);
assert!(result.is_err(), "NaN x/y/gap must be rejected");
let err = result.unwrap_err();
assert!(
!err.failing_conditions.is_empty(),
"NaN inputs: at least one condition must fail, got {:?}",
err.failing_conditions
);
assert!(
err.failing_conditions.contains(&"duality_gap"),
"NaN gap: duality_gap must always be in failing_conditions, got {:?}",
err.failing_conditions
);
}
}
#[test]
fn prove_optimal_finite_below_tol_still_passes() {
let q = CscMatrix::new(1, 1);
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0_f64], 1, 1).unwrap();
let c = vec![1.0_f64];
let b = vec![1.0_f64];
let bounds = vec![(0.0_f64, f64::INFINITY)];
let ct = vec![ConstraintType::Ge];
let view = trivial_view(&q, &a, &c, &b, &bounds, &ct);
let x = vec![1.0_f64];
let y = vec![-1.0_f64];
let z = vec![0.0_f64];
let result = prove_optimal(&view, &x, &y, &z, 0.0, 1e-6);
assert!(
result.is_ok(),
"exact finite KKT must still pass after NaN fix: {:?}",
result.err()
);
}
#[test]
fn prove_optimal_dimension_mismatch_returns_err_not_panic() {
let q1 = CscMatrix::new(1, 1);
let a2x1 = CscMatrix::from_triplets(&[0usize, 1], &[0, 0], &[1.0_f64, 1.0], 2, 1).unwrap();
let c1 = vec![0.0_f64];
let b2 = vec![1.0_f64, 1.0];
let bounds_lb = vec![(0.0_f64, f64::INFINITY)]; let bounds_free = vec![(f64::NEG_INFINITY, f64::INFINITY)]; let ct2 = vec![ConstraintType::Le, ConstraintType::Le];
{
let view = trivial_view(&q1, &a2x1, &c1, &b2, &bounds_free, &ct2);
let result = prove_optimal(&view, &[0.0], &[0.5], &[], 0.0, 1e-6);
assert!(result.is_err(), "y too short: expected Err");
let err = result.unwrap_err();
assert_eq!(
err.failing_conditions,
vec!["input_dimensions"],
"y too short: {:?}",
err.failing_conditions
);
assert!(
err.stationarity_rel.is_nan(),
"residuals must be NaN on dim error"
);
}
{
let view = trivial_view(&q1, &a2x1, &c1, &b2, &bounds_free, &ct2);
let result = prove_optimal(&view, &[0.0], &[0.5, 0.5, 0.5], &[], 0.0, 1e-6);
assert!(result.is_err(), "y too long: expected Err");
let err = result.unwrap_err();
assert_eq!(
err.failing_conditions,
vec!["input_dimensions"],
"y too long: {:?}",
err.failing_conditions
);
}
{
let a1x1 = CscMatrix::from_triplets(&[0usize], &[0], &[1.0_f64], 1, 1).unwrap();
let b1 = vec![1.0_f64];
let ct1 = vec![ConstraintType::Ge];
let view = trivial_view(&q1, &a1x1, &c1, &b1, &bounds_lb, &ct1);
let result = prove_optimal(&view, &[], &[-1.0], &[0.0], 0.0, 1e-6);
assert!(result.is_err(), "x empty (too short): expected Err");
let err = result.unwrap_err();
assert_eq!(
err.failing_conditions,
vec!["input_dimensions"],
"x too short: {:?}",
err.failing_conditions
);
}
{
let a1x1 = CscMatrix::from_triplets(&[0usize], &[0], &[1.0_f64], 1, 1).unwrap();
let b1 = vec![1.0_f64];
let ct1 = vec![ConstraintType::Ge];
let view = trivial_view(&q1, &a1x1, &c1, &b1, &bounds_lb, &ct1);
let result = prove_optimal(&view, &[1.0, 2.0], &[-1.0], &[0.0], 0.0, 1e-6);
assert!(result.is_err(), "x too long: expected Err");
let err = result.unwrap_err();
assert_eq!(
err.failing_conditions,
vec!["input_dimensions"],
"x too long: {:?}",
err.failing_conditions
);
}
{
let a1x1 = CscMatrix::from_triplets(&[0usize], &[0], &[1.0_f64], 1, 1).unwrap();
let b1 = vec![1.0_f64];
let ct1 = vec![ConstraintType::Ge];
let view = trivial_view(&q1, &a1x1, &c1, &b1, &bounds_lb, &ct1);
let result = prove_optimal(&view, &[1.0], &[-1.0], &[], 0.0, 1e-6);
assert!(
result.is_err(),
"z too short (empty for lb-only): expected Err"
);
let err = result.unwrap_err();
assert_eq!(
err.failing_conditions,
vec!["input_dimensions"],
"z too short: {:?}",
err.failing_conditions
);
}
{
let a1x1 = CscMatrix::from_triplets(&[0usize], &[0], &[1.0_f64], 1, 1).unwrap();
let b1 = vec![1.0_f64];
let ct1 = vec![ConstraintType::Ge];
let view = trivial_view(&q1, &a1x1, &c1, &b1, &bounds_lb, &ct1);
let result = prove_optimal(&view, &[1.0], &[-1.0], &[0.0, 0.0], 0.0, 1e-6);
assert!(result.is_err(), "z too long: expected Err");
let err = result.unwrap_err();
assert_eq!(
err.failing_conditions,
vec!["input_dimensions"],
"z too long: {:?}",
err.failing_conditions
);
}
{
let a1x1 = CscMatrix::from_triplets(&[0usize], &[0], &[1.0_f64], 1, 1).unwrap();
let b1 = vec![1.0_f64];
let ct1 = vec![ConstraintType::Ge];
let c_wrong = vec![1.0_f64, 2.0]; let view = ProblemView {
q: &q1,
a: &a1x1,
c: &c_wrong,
b: &b1,
bounds: &bounds_lb,
constraint_types: &ct1,
eliminated_cols: &[],
};
let result = prove_optimal(&view, &[1.0], &[-1.0], &[0.0], 0.0, 1e-6);
assert!(result.is_err(), "view c wrong length: expected Err");
let err = result.unwrap_err();
assert_eq!(
err.failing_conditions,
vec!["input_dimensions"],
"view c wrong: {:?}",
err.failing_conditions
);
}
{
let a1x1 = CscMatrix::from_triplets(&[0usize], &[0], &[1.0_f64], 1, 1).unwrap();
let c_min_x = vec![1.0_f64];
let b1 = vec![1.0_f64];
let ct_ge = vec![ConstraintType::Ge];
let view = trivial_view(&q1, &a1x1, &c_min_x, &b1, &bounds_lb, &ct_ge);
let result = prove_optimal(&view, &[1.0], &[-1.0], &[0.0], 0.0, 1e-6);
assert!(
result.is_ok(),
"correct dims + exact KKT must pass: {:?}",
result.err()
);
}
}
#[test]
fn prove_optimal_z_length_table_box_constraint() {
let q = CscMatrix::new(1, 1);
let a = CscMatrix::new(0, 1); let c = vec![0.0_f64];
let bounds = vec![(1.0_f64, 2.0_f64)]; let ct: Vec<ConstraintType> = vec![];
for (z_bad, label) in [
(vec![], "z empty"),
(vec![0.0_f64], "z len 1 (too short)"),
(vec![0.0_f64, 0.0, 0.0], "z len 3 (too long)"),
] {
let view = trivial_view(&q, &a, &c, &[], &bounds, &ct);
let result = prove_optimal(&view, &[1.5], &[], &z_bad, 0.0, 1e-6);
assert!(result.is_err(), "{label}: expected Err");
let err = result.unwrap_err();
assert_eq!(
err.failing_conditions,
vec!["input_dimensions"],
"{label}: {:?}",
err.failing_conditions
);
}
{
let view = trivial_view(&q, &a, &c, &[], &bounds, &ct);
let result = prove_optimal(&view, &[1.5], &[], &[0.0, 0.0], 0.0, 1e-6);
assert!(
result.is_ok(),
"correct z=[0,0] with x inside bounds must pass: {:?}",
result.err()
);
}
}
fn make_le_lp() -> LpProblem {
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0_f64], 1, 1).unwrap();
LpProblem::new_general(
vec![-1.0_f64],
a,
vec![1.0_f64],
vec![ConstraintType::Le],
vec![(0.0_f64, f64::INFINITY)],
None,
)
.unwrap()
}
fn correct_simplex_result() -> SolverResult {
SolverResult {
status: SolveStatus::Optimal,
objective: -1.0,
solution: vec![1.0_f64],
dual_solution: vec![-1.0_f64],
reduced_costs: vec![0.0_f64],
slack: vec![0.0_f64],
..Default::default()
}
}
#[test]
fn prove_optimal_lp_correct_simplex_path_passes() {
let lp = make_le_lp();
let result = correct_simplex_result();
let cert = prove_optimal_lp(&lp, &result, 1e-6);
assert!(
cert.is_ok(),
"correct simplex result must pass: {:?}",
cert.err()
);
}
#[test]
fn prove_optimal_lp_wrong_sign_dual_fails() {
let lp = make_le_lp();
let mut result = correct_simplex_result();
result.dual_solution = vec![1.0_f64]; let cert = prove_optimal_lp(&lp, &result, 1e-6);
assert!(cert.is_err(), "wrong-sign dual must fail prove_optimal_lp");
let err = cert.unwrap_err();
assert!(
err.failing_conditions.contains(&"dual_sign")
|| err.failing_conditions.contains(&"stationarity"),
"dual_sign or stationarity must fail: {:?}",
err.failing_conditions,
);
}
#[test]
fn prove_optimal_lp_active_lower_bound_cert() {
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0_f64], 1, 1).unwrap();
let lp = LpProblem::new_general(
vec![1.0_f64],
a,
vec![2.0_f64],
vec![ConstraintType::Ge],
vec![(0.0_f64, f64::INFINITY)],
None,
)
.unwrap();
let result = SolverResult {
status: SolveStatus::Optimal,
objective: 2.0,
solution: vec![2.0_f64],
dual_solution: vec![1.0_f64], reduced_costs: vec![0.0_f64],
slack: vec![0.0_f64],
..Default::default()
};
let cert = prove_optimal_lp(&lp, &result, 1e-6);
assert!(
cert.is_ok(),
"active Ge constraint must produce valid cert: {:?}",
cert.err()
);
}
#[test]
fn guard_lp_optimal_passes_correct_result() {
let lp = make_le_lp();
let result = correct_simplex_result();
let guarded = guard_lp_optimal(result, &lp);
assert_eq!(
guarded.status,
SolveStatus::Optimal,
"correct KKT result must remain Optimal"
);
}
#[test]
fn guard_lp_optimal_demotes_wrong_sign_dual() {
let lp = make_le_lp();
let mut result = correct_simplex_result();
result.dual_solution = vec![1.0_f64]; let guarded = guard_lp_optimal(result, &lp);
assert_eq!(
guarded.status,
SolveStatus::SuboptimalSolution,
"wrong-sign dual must be demoted to SuboptimalSolution"
);
}
#[test]
fn guard_lp_optimal_passthrough_non_optimal_statuses() {
let lp = make_le_lp();
for status in [
SolveStatus::Infeasible,
SolveStatus::Timeout,
SolveStatus::NumericalError,
] {
let r = SolverResult {
status: status.clone(),
..Default::default()
};
let out = guard_lp_optimal(r, &lp);
assert_eq!(out.status, status);
}
}
#[test]
fn lp_cert_tol_equals_feas_rel_tol() {
let frt = crate::tolerances::feas_rel_tol();
assert_eq!(
LP_CERT_TOL, frt,
"LP_CERT_TOL ({LP_CERT_TOL}) must equal feas_rel_tol() ({frt}); \
update one to match the other (see LP_CERT_TOL docstring for derivation)"
);
}
#[test]
fn prove_optimal_lp_ipm_all_free_passes() {
let a = CscMatrix::from_triplets(
&[0, 1, 0, 1],
&[0, 0, 1, 1],
&[1.0_f64, 1.0, -1.0, 1.0],
2,
2,
)
.unwrap();
let lp = LpProblem::new_general(
vec![1.0_f64, -1.0],
a,
vec![2.0_f64, 4.0],
vec![ConstraintType::Eq, ConstraintType::Eq],
vec![(f64::NEG_INFINITY, f64::INFINITY); 2],
None,
)
.unwrap();
let ipm_result = SolverResult {
status: SolveStatus::Optimal,
objective: 2.0,
solution: vec![3.0_f64, 1.0],
dual_solution: vec![-1.0_f64, 0.0], bound_duals: vec![], reduced_costs: vec![], ..Default::default()
};
let cert = prove_optimal_lp(&lp, &ipm_result, LP_CERT_TOL);
assert!(
cert.is_ok(),
"IPM all-free LP must pass prove_optimal_lp: {:?}",
cert.err()
);
let guarded = guard_lp_optimal(ipm_result, &lp);
assert_eq!(
guarded.status,
SolveStatus::Optimal,
"guard_lp_optimal must not demote correct IPM all-free LP result"
);
}
#[test]
fn prove_optimal_lp_path_bound_cross_table() {
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0_f64], 1, 1).unwrap();
let run = |bounds: Vec<(f64, f64)>,
dual_sol: Vec<f64>,
bound_duals: Vec<f64>,
reduced_costs: Vec<f64>,
label: &str| {
let lp = LpProblem::new_general(
vec![1.0_f64],
a.clone(),
vec![1.0_f64],
vec![ConstraintType::Eq],
bounds,
None,
)
.unwrap();
let r = SolverResult {
status: SolveStatus::Optimal,
objective: 1.0,
solution: vec![1.0_f64],
dual_solution: dual_sol,
bound_duals,
reduced_costs,
..Default::default()
};
let cert = prove_optimal_lp(&lp, &r, LP_CERT_TOL);
assert!(
cert.is_ok(),
"case `{label}` must pass prove_optimal_lp: {:?}",
cert.err()
);
let guarded = guard_lp_optimal(r, &lp);
assert_eq!(
guarded.status,
SolveStatus::Optimal,
"case `{label}` must remain Optimal after guard"
);
};
run(
vec![(0.0, f64::INFINITY)],
vec![1.0_f64],
vec![],
vec![0.0_f64],
"simplex/lb0",
);
run(
vec![(f64::NEG_INFINITY, f64::INFINITY)],
vec![1.0_f64],
vec![],
vec![0.0_f64],
"simplex/free",
);
run(
vec![(0.0, f64::INFINITY)],
vec![-1.0_f64],
vec![0.0_f64],
vec![],
"ipm/lb0",
);
run(
vec![(f64::NEG_INFINITY, f64::INFINITY)],
vec![-1.0_f64],
vec![],
vec![],
"ipm/free",
);
}
#[test]
fn prove_optimal_negative_gap_bug_reproduction() {
let q = CscMatrix::new(1, 1);
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0_f64], 1, 1).unwrap();
let c = vec![1.0_f64];
let b = vec![1.0_f64];
let bounds = vec![(0.0_f64, f64::INFINITY)];
let ct = vec![ConstraintType::Ge];
let view = trivial_view(&q, &a, &c, &b, &bounds, &ct);
let x = vec![1.0_f64];
let y = vec![-1.0_f64];
let z = vec![0.0_f64];
let r_neg = prove_optimal(&view, &x, &y, &z, -1e-3, 1e-6);
assert!(r_neg.is_err(), "gap=-1e-3 must be rejected");
let r_neginf = prove_optimal(&view, &x, &y, &z, f64::NEG_INFINITY, 1e-6);
assert!(r_neginf.is_err(), "gap=-inf must be rejected");
}
#[test]
fn prove_optimal_invalid_gap_and_tol_rejected() {
let q = CscMatrix::new(1, 1);
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0_f64], 1, 1).unwrap();
let c = vec![1.0_f64];
let b = vec![1.0_f64];
let bounds = vec![(0.0_f64, f64::INFINITY)];
let ct = vec![ConstraintType::Ge];
let view = trivial_view(&q, &a, &c, &b, &bounds, &ct);
let x = vec![1.0_f64];
let y = vec![-1.0_f64];
let z = vec![0.0_f64];
for (gap, label) in [
(-1e-3_f64, "gap = -1e-3"),
(f64::NEG_INFINITY, "gap = -inf"),
(f64::NAN, "gap = NaN"),
(f64::INFINITY, "gap = +inf"),
] {
let r = prove_optimal(&view, &x, &y, &z, gap, 1e-6);
assert!(r.is_err(), "{label}: expected Err");
let err = r.unwrap_err();
assert!(
err.failing_conditions.contains(&"duality_gap"),
"{label}: 'duality_gap' must be in failing_conditions, got {:?}",
err.failing_conditions
);
}
for (tol, label) in [
(0.0_f64, "tol = 0"),
(-1.0_f64, "tol = -1"),
(f64::NAN, "tol = NaN"),
(f64::INFINITY, "tol = +inf"),
] {
let r = prove_optimal(&view, &x, &y, &z, 0.0, tol);
assert!(r.is_err(), "{label}: expected Err");
let err = r.unwrap_err();
assert!(
err.failing_conditions.contains(&"invalid_tolerance"),
"{label}: 'invalid_tolerance' must be in failing_conditions, got {:?}",
err.failing_conditions
);
}
for (gap, tol, label) in [
(0.0_f64, 1e-6_f64, "gap=0 tol=1e-6"),
(1e-7_f64, 1e-6_f64, "gap=1e-7 tol=1e-6"),
] {
let r = prove_optimal(&view, &x, &y, &z, gap, tol);
assert!(r.is_ok(), "{label}: expected Ok, got {:?}", r.err());
}
}
#[test]
fn guard_lp_optimal_no_false_demote_for_residuals_below_lp_cert_tol() {
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0_f64], 1, 1).unwrap();
let lp = LpProblem::new_general(
vec![1.0_f64],
a,
vec![1.0_f64],
vec![ConstraintType::Ge],
vec![(0.0_f64, f64::INFINITY)],
None,
)
.unwrap();
let y_perturbed = 1.0 + 5e-5; let result = SolverResult {
status: SolveStatus::Optimal,
objective: 1.0,
solution: vec![1.0_f64],
dual_solution: vec![y_perturbed], reduced_costs: vec![0.0_f64],
slack: vec![0.0_f64],
..Default::default()
};
let r = prove_optimal_lp(&lp, &result, LP_CERT_TOL);
assert!(
r.is_ok(),
"residuals < LP_CERT_TOL={LP_CERT_TOL} must not demote: {:?}",
r.err()
);
let r_strict = prove_optimal_lp(&lp, &result, 1e-6);
assert!(
r_strict.is_err(),
"residuals should exceed 1e-6, proving the test targets the (1e-6, LP_CERT_TOL) window"
);
let guarded = guard_lp_optimal(result, &lp);
assert_eq!(
guarded.status,
SolveStatus::Optimal,
"guard must not demote an LP result whose residuals are within LP_CERT_TOL"
);
}
}