1use thiserror::Error;
4
5use crate::{matrix::norm_inf, problem::QpProblem};
6
7#[derive(Clone, Debug, Default, PartialEq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct DualVariables {
15 pub equalities: Vec<f64>,
17 pub inequalities: Vec<f64>,
19 pub bounds: Vec<f64>,
21 pub l1: Vec<f64>,
28}
29
30#[derive(Clone, Copy, Debug, Default, PartialEq)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
33pub struct KktResiduals {
34 pub primal: f64,
36 pub dual: f64,
38 pub complementarity: f64,
40}
41
42#[derive(Clone, Debug, Error, PartialEq, Eq)]
44pub enum KktError {
45 #[error("{field} has length {actual}; expected {expected}")]
47 Dimension {
48 field: &'static str,
50 expected: usize,
52 actual: usize,
54 },
55}
56
57pub fn check_kkt(
65 problem: &QpProblem,
66 x: &[f64],
67 dual: &DualVariables,
68) -> Result<KktResiduals, KktError> {
69 let n = problem.quadratic.dimension();
70 let l1_expected = if problem.l1.is_some() { n } else { 0 };
71 for (field, actual, expected) in [
72 ("x", x.len(), n),
73 (
74 "dual.equalities",
75 dual.equalities.len(),
76 problem.equalities.len(),
77 ),
78 (
79 "dual.inequalities",
80 dual.inequalities.len(),
81 problem.inequalities.len(),
82 ),
83 ("dual.bounds", dual.bounds.len(), n),
84 ("dual.l1", dual.l1.len(), l1_expected),
85 ] {
86 if actual != expected {
87 return Err(KktError::Dimension {
88 field,
89 expected,
90 actual,
91 });
92 }
93 }
94
95 let equality_values = problem.equalities.matrix.mul_vec(x);
96 let inequality_values = problem.inequalities.matrix.mul_vec(x);
97 let mut primal = 0.0_f64;
98 for (value, rhs) in equality_values.iter().zip(&problem.equalities.rhs) {
99 primal = primal.max((value - rhs).abs());
100 }
101 for (value, rhs) in inequality_values.iter().zip(&problem.inequalities.rhs) {
102 primal = primal.max((value - rhs).max(0.0));
103 }
104 for (index, value) in x.iter().enumerate() {
105 primal = primal
106 .max((problem.lower_bounds[index] - value).max(0.0))
107 .max((value - problem.upper_bounds[index]).max(0.0));
108 }
109
110 let mut stationarity = problem.quadratic.apply(x);
111 for (value, linear) in stationarity.iter_mut().zip(&problem.linear) {
112 *value += linear;
113 }
114 problem
115 .equalities
116 .matrix
117 .transpose_mul_add(&dual.equalities, &mut stationarity);
118 problem
119 .inequalities
120 .matrix
121 .transpose_mul_add(&dual.inequalities, &mut stationarity);
122 for (value, bound_dual) in stationarity.iter_mut().zip(&dual.bounds) {
123 *value += bound_dual;
124 }
125 for (value, l1_dual) in stationarity.iter_mut().zip(&dual.l1) {
126 *value += l1_dual;
127 }
128
129 let mut cone_violation = 0.0_f64;
130 let mut complementarity = 0.0_f64;
131 for ((value, rhs), multiplier) in inequality_values
132 .iter()
133 .zip(&problem.inequalities.rhs)
134 .zip(&dual.inequalities)
135 {
136 cone_violation = cone_violation.max((-multiplier).max(0.0));
137 complementarity = complementarity.max((multiplier * (value - rhs)).abs());
138 }
139
140 for (index, value) in x.iter().copied().enumerate() {
149 let lower = problem.lower_bounds[index];
150 let upper = problem.upper_bounds[index];
151 let multiplier = dual.bounds[index];
152 let toward_upper = multiplier.max(0.0);
153 let toward_lower = (-multiplier).max(0.0);
154 if upper.is_finite() {
155 complementarity = complementarity.max(toward_upper * (upper - value).abs());
156 } else {
157 cone_violation = cone_violation.max(toward_upper);
158 }
159 if lower.is_finite() {
160 complementarity = complementarity.max(toward_lower * (value - lower).abs());
161 } else {
162 cone_violation = cone_violation.max(toward_lower);
163 }
164 }
165
166 if let Some(l1) = &problem.l1 {
173 for (index, multiplier) in dual.l1.iter().copied().enumerate() {
174 let cost = l1.costs[index];
175 let offset = x[index] - l1.anchor[index];
176 cone_violation = cone_violation.max(multiplier.abs() - cost);
177 complementarity = complementarity
178 .max(((cost - multiplier) * offset.max(0.0)).abs())
179 .max(((cost + multiplier) * (-offset).max(0.0)).abs());
180 }
181 }
182
183 Ok(KktResiduals {
184 primal,
185 dual: norm_inf(&stationarity).max(cone_violation),
186 complementarity,
187 })
188}