use crate::{
kkt::{DualVariables, KktError},
matrix::{dot, norm_inf},
problem::QpProblem,
};
const DIVISION_GUARD: f64 = 1.0e-12;
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Certificate {
Primal(PrimalCertificate),
Dual(DualCertificate),
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PrimalCertificate {
pub equality_dual: Vec<f64>,
pub inequality_dual: Vec<f64>,
pub bound_dual: Vec<f64>,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DualCertificate {
pub direction: Vec<f64>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PrimalCertificateResiduals {
pub stationarity: f64,
pub support_gap: f64,
pub cone_violation: f64,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DualCertificateResiduals {
pub curvature: f64,
pub objective_gap: f64,
pub recession_violation: f64,
}
pub fn check_primal_certificate(
problem: &QpProblem,
certificate: &PrimalCertificate,
) -> Result<PrimalCertificateResiduals, KktError> {
let n = problem.quadratic.dimension();
for (field, actual, expected) in [
(
"certificate.equality_dual",
certificate.equality_dual.len(),
problem.equalities.len(),
),
(
"certificate.inequality_dual",
certificate.inequality_dual.len(),
problem.inequalities.len(),
),
("certificate.bound_dual", certificate.bound_dual.len(), n),
] {
if actual != expected {
return Err(KktError::Dimension {
field,
expected,
actual,
});
}
}
let mut combination = certificate.bound_dual.clone();
problem
.equalities
.matrix
.transpose_mul_add(&certificate.equality_dual, &mut combination);
problem
.inequalities
.matrix
.transpose_mul_add(&certificate.inequality_dual, &mut combination);
let stationarity = norm_inf(&combination);
let mut cone_violation = 0.0_f64;
let mut support_gap = dot(&problem.equalities.rhs, &certificate.equality_dual);
for (multiplier, rhs) in certificate
.inequality_dual
.iter()
.zip(&problem.inequalities.rhs)
{
cone_violation = cone_violation.max(-multiplier);
support_gap += rhs * multiplier.max(0.0);
}
for (index, multiplier) in certificate.bound_dual.iter().enumerate() {
let toward_upper = multiplier.max(0.0);
let toward_lower = multiplier.min(0.0);
let upper = problem.upper_bounds[index];
let lower = problem.lower_bounds[index];
if upper.is_finite() {
support_gap += upper * toward_upper;
} else {
cone_violation = cone_violation.max(toward_upper);
}
if lower.is_finite() {
support_gap += lower * toward_lower;
} else {
cone_violation = cone_violation.max(-toward_lower);
}
}
Ok(PrimalCertificateResiduals {
stationarity,
support_gap,
cone_violation,
})
}
pub fn check_dual_certificate(
problem: &QpProblem,
certificate: &DualCertificate,
) -> Result<DualCertificateResiduals, KktError> {
let n = problem.quadratic.dimension();
if certificate.direction.len() != n {
return Err(KktError::Dimension {
field: "certificate.direction",
expected: n,
actual: certificate.direction.len(),
});
}
let curvature = norm_inf(&problem.quadratic.apply(&certificate.direction));
let l1_slope = problem.l1.as_ref().map_or(0.0, |term| {
term.costs
.iter()
.zip(&certificate.direction)
.map(|(cost, value)| cost * value.abs())
.sum()
});
let objective_gap = dot(&problem.linear, &certificate.direction) + l1_slope;
let mut recession_violation =
norm_inf(&problem.equalities.matrix.mul_vec(&certificate.direction));
for value in problem.inequalities.matrix.mul_vec(&certificate.direction) {
recession_violation = recession_violation.max(value);
}
for (index, value) in certificate.direction.iter().enumerate() {
if problem.upper_bounds[index].is_finite() {
recession_violation = recession_violation.max(*value);
}
if problem.lower_bounds[index].is_finite() {
recession_violation = recession_violation.max(-*value);
}
}
Ok(DualCertificateResiduals {
curvature,
objective_gap,
recession_violation,
})
}
pub(crate) fn detect_primal_infeasibility(
problem: &QpProblem,
delta_dual: &DualVariables,
tolerance: f64,
) -> Option<PrimalCertificate> {
let magnitude = norm_inf(&delta_dual.equalities)
.max(norm_inf(&delta_dual.inequalities))
.max(norm_inf(&delta_dual.bounds));
if !magnitude.is_finite() || magnitude <= DIVISION_GUARD {
return None;
}
let equality_dual: Vec<f64> = delta_dual
.equalities
.iter()
.map(|value| value / magnitude)
.collect();
let mut inequality_dual: Vec<f64> = delta_dual
.inequalities
.iter()
.map(|value| value / magnitude)
.collect();
let mut bound_dual: Vec<f64> = delta_dual
.bounds
.iter()
.map(|value| value / magnitude)
.collect();
for value in &mut inequality_dual {
if *value < -tolerance {
return None;
}
*value = value.max(0.0);
}
for (index, value) in bound_dual.iter_mut().enumerate() {
if !problem.upper_bounds[index].is_finite() {
if *value > tolerance {
return None;
}
*value = value.min(0.0);
}
if !problem.lower_bounds[index].is_finite() {
if *value < -tolerance {
return None;
}
*value = value.max(0.0);
}
}
let certificate = PrimalCertificate {
equality_dual,
inequality_dual,
bound_dual,
};
let residuals = check_primal_certificate(problem, &certificate).ok()?;
(residuals.stationarity <= tolerance && residuals.support_gap <= -tolerance)
.then_some(certificate)
}
pub(crate) fn detect_dual_infeasibility(
problem: &QpProblem,
delta_x: &[f64],
tolerance: f64,
) -> Option<DualCertificate> {
let magnitude = norm_inf(delta_x);
if !magnitude.is_finite() || magnitude <= DIVISION_GUARD {
return None;
}
let certificate = DualCertificate {
direction: delta_x.iter().map(|value| value / magnitude).collect(),
};
let residuals = check_dual_certificate(problem, &certificate).ok()?;
(residuals.curvature <= tolerance
&& residuals.recession_violation <= tolerance
&& residuals.objective_gap <= -tolerance)
.then_some(certificate)
}
#[cfg(test)]
mod tests {
use super::{
check_dual_certificate, check_primal_certificate, detect_dual_infeasibility,
detect_primal_infeasibility, PrimalCertificate,
};
use crate::{
kkt::DualVariables,
problem::{FactorCovariance, FactorQuad, LinearConstraints, QpProblem},
Matrix,
};
fn budget_versus_boxes() -> QpProblem {
let n = 4;
QpProblem {
quadratic: FactorQuad {
factors: Matrix::zeros(n, 1),
omega: FactorCovariance::Diagonal(vec![1.0]),
diagonal: vec![1.0; n],
},
linear: vec![0.0; n],
l1: None,
equalities: LinearConstraints {
matrix: Matrix::new(1, n, vec![1.0; n]).unwrap(),
rhs: vec![1.0],
},
inequalities: LinearConstraints::empty(n),
lower_bounds: vec![0.0; n],
upper_bounds: vec![0.2; n],
}
}
#[test]
fn exact_farkas_certificate_audits_clean() {
let problem = budget_versus_boxes();
let certificate = PrimalCertificate {
equality_dual: vec![-1.0],
inequality_dual: Vec::new(),
bound_dual: vec![1.0; 4],
};
let residuals = check_primal_certificate(&problem, &certificate).unwrap();
assert!(residuals.stationarity <= 1.0e-12);
assert!(residuals.cone_violation <= 1.0e-12);
assert!((residuals.support_gap - (-0.2)).abs() <= 1.0e-12);
}
#[test]
fn detection_accepts_the_farkas_direction_and_rejects_noise() {
let problem = budget_versus_boxes();
let farkas = DualVariables {
equalities: vec![-2.0],
inequalities: Vec::new(),
bounds: vec![2.0; 4],
l1: Vec::new(),
};
let certificate = detect_primal_infeasibility(&problem, &farkas, 1.0e-5).unwrap();
let residuals = check_primal_certificate(&problem, &certificate).unwrap();
assert!(residuals.support_gap <= -1.0e-5);
let noise = DualVariables {
equalities: vec![1.0],
inequalities: Vec::new(),
bounds: vec![1.0; 4],
l1: Vec::new(),
};
assert!(detect_primal_infeasibility(&problem, &noise, 1.0e-5).is_none());
let silence = DualVariables {
equalities: vec![0.0],
inequalities: Vec::new(),
bounds: vec![0.0; 4],
l1: Vec::new(),
};
assert!(detect_primal_infeasibility(&problem, &silence, 1.0e-5).is_none());
}
#[test]
fn dual_certificate_requires_descent_and_recession() {
let problem = QpProblem {
quadratic: FactorQuad {
factors: Matrix::zeros(2, 1),
omega: FactorCovariance::Diagonal(vec![1.0]),
diagonal: vec![0.0, 1.0],
},
linear: vec![1.0, 0.0],
l1: None,
equalities: LinearConstraints::empty(2),
inequalities: LinearConstraints::empty(2),
lower_bounds: vec![f64::NEG_INFINITY, 0.0],
upper_bounds: vec![f64::INFINITY, 1.0],
};
let descent = detect_dual_infeasibility(&problem, &[-3.0, 0.0], 1.0e-5).unwrap();
let residuals = check_dual_certificate(&problem, &descent).unwrap();
assert!(residuals.curvature <= 1.0e-12);
assert!(residuals.objective_gap <= -1.0);
assert!(residuals.recession_violation <= 1.0e-12);
assert!(detect_dual_infeasibility(&problem, &[3.0, 0.0], 1.0e-5).is_none());
assert!(detect_dual_infeasibility(&problem, &[-3.0, -1.0], 1.0e-5).is_none());
}
}