1use crate::sqp::bfgs::DampedBfgs;
20use crate::sqp::filter::{filter_line_search, SqpFilter};
21use crate::sqp::iterates::SqpIterates;
22use crate::sqp::lbfgs::LBfgs;
23use crate::sqp::line_search::l1_merit_line_search;
24use crate::sqp::options::{SqpGlobalization, SqpHessianSource, SqpOptions};
25use crate::sqp::problem::SqpProblemSpec;
26use crate::sqp::qp_assembly::{SqpQpData, Triplet};
27use crate::sqp::result::{SqpError, SqpResult, SqpStatus};
28use pounce_common::types::{Number, NLP_LOWER_BOUND_INF, NLP_UPPER_BOUND_INF};
29use pounce_qp::{HessianInertia, ParametricActiveSetSolver, QpOptions, QpSolver, QpStatus};
30
31pub struct SqpAlgorithm {
33 qp_solver: ParametricActiveSetSolver,
34 qp_opts: QpOptions,
35 opts: SqpOptions,
36 iterates: Option<SqpIterates>,
37 filter: SqpFilter,
41}
42
43impl SqpAlgorithm {
44 pub fn new(qp_solver: ParametricActiveSetSolver, opts: SqpOptions) -> Self {
45 Self {
46 qp_solver,
47 qp_opts: QpOptions::default(),
48 opts,
49 iterates: None,
50 filter: SqpFilter::new(),
51 }
52 }
53
54 pub fn with_qp_options(mut self, qp_opts: QpOptions) -> Self {
60 self.qp_opts = qp_opts;
61 self
62 }
63
64 pub fn options(&self) -> &SqpOptions {
65 &self.opts
66 }
67
68 pub fn iterates(&self) -> Option<&SqpIterates> {
69 self.iterates.as_ref()
70 }
71
72 pub fn optimize<N: SqpProblemSpec>(&mut self, nlp: &mut N) -> Result<SqpResult, SqpError> {
75 self.optimize_with_warm_start(nlp, None)
76 }
77
78 pub fn optimize_with_warm_start<N: SqpProblemSpec>(
92 &mut self,
93 nlp: &mut N,
94 warm: Option<SqpIterates>,
95 ) -> Result<SqpResult, SqpError> {
96 let n = nlp.n();
97 let m = nlp.m();
98 let (xl, xu) = nlp.variable_bounds();
99 let (bl_c, bu_c) = nlp.constraint_bounds();
100 if xl.len() != n || xu.len() != n {
101 return Err(SqpError::DimensionMismatch(format!(
102 "variable_bounds length must be n = {n}"
103 )));
104 }
105 if bl_c.len() != m || bu_c.len() != m {
106 return Err(SqpError::DimensionMismatch(format!(
107 "constraint_bounds length must be m = {m}"
108 )));
109 }
110
111 let mut iter = match warm {
112 Some(w) => {
113 if w.x.len() != n {
114 return Err(SqpError::DimensionMismatch(format!(
115 "warm.x length {} must equal n = {n}",
116 w.x.len()
117 )));
118 }
119 if w.lambda_g.len() != m {
120 return Err(SqpError::DimensionMismatch(format!(
121 "warm.lambda_g length {} must equal m = {m}",
122 w.lambda_g.len()
123 )));
124 }
125 if w.lambda_x.len() != n {
126 return Err(SqpError::DimensionMismatch(format!(
127 "warm.lambda_x length {} must equal n = {n}",
128 w.lambda_x.len()
129 )));
130 }
131 if let Some(ws) = w.working.as_ref() {
132 ws.validate_dims(n, m).map_err(SqpError::QpFailure)?;
133 }
134 w
135 }
136 None => {
137 let mut cold = SqpIterates::cold(n, m);
138 let x_init = nlp.x_init();
139 if x_init.len() != n {
140 return Err(SqpError::DimensionMismatch(format!(
141 "x_init length must be n = {n}"
142 )));
143 }
144 cold.x = x_init;
145 cold
146 }
147 };
148
149 let mut n_qp_solves: u32 = 0;
150 let mut final_stationarity = 0.0;
151 let mut final_constr_viol = 0.0;
152 let mut nu = self.opts.l1_penalty;
156 self.filter = SqpFilter::new();
158 let mut f_cached: Option<Number> = None;
162 let mut c_cached: Option<Vec<Number>> = None;
163
164 let mut bfgs: Option<DampedBfgs> =
169 if matches!(self.opts.hessian, SqpHessianSource::DampedBfgs) {
170 Some(DampedBfgs::new(n))
171 } else {
172 None
173 };
174 let mut lbfgs: Option<LBfgs> = if matches!(self.opts.hessian, SqpHessianSource::Lbfgs) {
175 Some(LBfgs::new(n, self.opts.lbfgs_max_history.max(1) as usize))
176 } else {
177 None
178 };
179
180 for outer in 0..self.opts.max_iter {
181 let grad_f = nlp.eval_grad_f(&iter.x);
182 let c_vals = c_cached.take().unwrap_or_else(|| nlp.eval_c(&iter.x));
183 let f_curr = f_cached.take().unwrap_or_else(|| nlp.eval_f(&iter.x));
184 let jac_c = nlp.eval_jac_c(&iter.x);
185 let hess_lag = match self.opts.hessian {
186 SqpHessianSource::Exact => nlp.eval_hess_lag(&iter.x, &iter.lambda_g),
187 SqpHessianSource::DampedBfgs => {
188 let bfgs = bfgs.as_mut().expect("DampedBfgs state initialized above");
189 let grad_lag = compute_grad_lag(&grad_f, &jac_c, &iter.lambda_g, n);
193 bfgs.update(&iter.x, &grad_lag);
194 bfgs.as_triplet()
195 }
196 SqpHessianSource::Lbfgs => {
197 let lb = lbfgs.as_mut().expect("LBfgs state initialized above");
198 let grad_lag = compute_grad_lag(&grad_f, &jac_c, &iter.lambda_g, n);
199 lb.update(&iter.x, &grad_lag);
200 lb.as_triplet()
201 }
202 };
203
204 let kkt = check_kkt(
206 n, m, &iter, &grad_f, &c_vals, &bl_c, &bu_c, &xl, &xu, &jac_c,
207 );
208 final_stationarity = kkt.stationarity;
209 final_constr_viol = kkt.constr_viol;
210
211 #[cfg(test)]
212 if self.opts.print_level >= 1 {
213 tracing::debug!(target: "pounce::sqp",
214 "[sqp k={outer:3}] x={:?} f={:.4e} ‖c‖={:.2e} stat={:.2e} ν={:.2e}",
215 iter.x.iter().map(|v| format!("{v:.3}")).collect::<Vec<_>>(),
216 f_curr,
217 kkt.constr_viol,
218 kkt.stationarity,
219 nu,
220 );
221 }
222
223 if kkt.stationarity <= self.opts.dual_inf_tol
224 && kkt.constr_viol <= self.opts.constr_viol_tol
225 {
226 self.iterates = Some(iter.clone());
227 return Ok(SqpResult {
228 x: iter.x,
229 lambda_g: iter.lambda_g,
230 lambda_x: iter.lambda_x,
231 obj: f_curr,
232 status: SqpStatus::Optimal,
233 n_iter: outer,
234 n_qp_solves,
235 final_stationarity,
236 final_constr_viol,
237 working_set: iter.working,
238 });
239 }
240
241 let qp_data = SqpQpData::build(
242 &iter.x,
243 &grad_f,
244 &c_vals,
245 &bl_c,
246 &bu_c,
247 &xl,
248 &xu,
249 jac_c,
250 hess_lag,
251 self.hessian_inertia(),
252 );
253 let qp = qp_data.as_qp();
254
255 let sol = if let Some(prev_w) = iter.working.as_ref() {
264 self.qp_solver
265 .solve_with_working_set(&qp, prev_w, &self.qp_opts)?
266 } else {
267 self.qp_solver.solve(&qp, None, &self.qp_opts)?
268 };
269 n_qp_solves += 1;
270
271 match sol.status {
272 QpStatus::Optimal => {}
273 QpStatus::Infeasible => {
274 let obj = nlp.eval_f(&iter.x);
275 self.iterates = Some(iter.clone());
276 return Ok(SqpResult {
277 x: iter.x,
278 lambda_g: iter.lambda_g,
279 lambda_x: iter.lambda_x,
280 obj,
281 status: SqpStatus::InfeasibleSubproblem,
282 n_iter: outer,
283 n_qp_solves,
284 final_stationarity,
285 final_constr_viol,
286 working_set: iter.working,
287 });
288 }
289 other => {
290 return Err(SqpError::QpFailure(
291 pounce_qp::QpError::LinearSolverFailure(format!(
292 "QP subproblem returned status {other}"
293 )),
294 ));
295 }
296 }
297
298 #[cfg(test)]
299 if self.opts.print_level >= 1 {
300 let p_inf = sol.x.iter().map(|v| v.abs()).fold(0.0_f64, f64::max);
301 tracing::debug!(target: "pounce::sqp",
302 " qp: ‖p‖_inf={:.3e} ‖λ_g_qp‖_inf={:.3e}",
303 p_inf,
304 sol.lambda_g.iter().map(|v| v.abs()).fold(0.0_f64, f64::max)
305 );
306 }
307 let ls = match self.opts.globalization {
313 SqpGlobalization::L1Elastic => l1_merit_line_search(
314 nlp,
315 &iter.x,
316 &sol.x,
317 &sol.lambda_g,
318 &grad_f,
319 f_curr,
320 &c_vals,
321 &bl_c,
322 &bu_c,
323 &xl,
324 &xu,
325 nu,
326 &self.opts,
327 ),
328 SqpGlobalization::Filter => filter_line_search(
329 nlp,
330 &mut self.filter,
331 &iter.x,
332 &sol.x,
333 f_curr,
334 &c_vals,
335 &bl_c,
336 &bu_c,
337 &xl,
338 &xu,
339 nu,
340 &self.opts,
341 ),
342 };
343 #[cfg(test)]
344 if self.opts.print_level >= 1 {
345 tracing::debug!(target: "pounce::sqp",
346 " ls: α={:.3e} ν={:.3e} ok={} f_new={:.3e}",
347 ls.alpha, ls.nu, ls.success, ls.f_new
348 );
349 }
350 if !ls.success {
351 self.iterates = Some(iter.clone());
352 return Ok(SqpResult {
353 x: iter.x,
354 lambda_g: iter.lambda_g,
355 lambda_x: iter.lambda_x,
356 obj: f_curr,
357 status: SqpStatus::LineSearchFailed,
358 n_iter: outer,
359 n_qp_solves,
360 final_stationarity,
361 final_constr_viol,
362 working_set: Some(sol.working),
363 });
364 }
365 iter.x = ls.x_new;
366 for (l, &lq) in iter.lambda_g.iter_mut().zip(sol.lambda_g.iter()) {
367 *l = (1.0 - ls.alpha) * *l + ls.alpha * lq;
368 }
369 for (l, &lq) in iter.lambda_x.iter_mut().zip(sol.lambda_x.iter()) {
370 *l = (1.0 - ls.alpha) * *l + ls.alpha * lq;
371 }
372 iter.working = Some(sol.working);
373 nu = ls.nu;
374 f_cached = Some(ls.f_new);
375 c_cached = Some(ls.c_new);
376 }
377
378 let obj = nlp.eval_f(&iter.x);
379 self.iterates = Some(iter.clone());
380 Ok(SqpResult {
381 x: iter.x,
382 lambda_g: iter.lambda_g,
383 lambda_x: iter.lambda_x,
384 obj,
385 status: SqpStatus::MaxIter,
386 n_iter: self.opts.max_iter,
387 n_qp_solves,
388 final_stationarity,
389 final_constr_viol,
390 working_set: iter.working,
391 })
392 }
393
394 fn hessian_inertia(&self) -> HessianInertia {
395 match self.opts.hessian {
396 crate::sqp::SqpHessianSource::Exact => HessianInertia::Indefinite,
399 crate::sqp::SqpHessianSource::DampedBfgs => HessianInertia::Psd,
401 crate::sqp::SqpHessianSource::Lbfgs => HessianInertia::Psd,
402 }
403 }
404}
405
406#[derive(Debug, Clone, Copy)]
407struct KktError {
408 pub stationarity: Number,
409 pub constr_viol: Number,
410}
411
412fn compute_grad_lag(
415 grad_f: &[Number],
416 jac_c: &Triplet,
417 lambda_g: &[Number],
418 n: usize,
419) -> Vec<Number> {
420 let mut out = grad_f.to_vec();
421 debug_assert_eq!(out.len(), n);
422 for k in 0..jac_c.irow.len() {
423 let row_i = (jac_c.irow[k] - 1) as usize;
424 let col_j = (jac_c.jcol[k] - 1) as usize;
425 out[col_j] += jac_c.vals[k] * lambda_g[row_i];
426 }
427 out
428}
429
430fn check_kkt(
431 n: usize,
432 m: usize,
433 iter: &SqpIterates,
434 grad_f: &[Number],
435 c_vals: &[Number],
436 bl_c: &[Number],
437 bu_c: &[Number],
438 xl: &[Number],
439 xu: &[Number],
440 jac_c: &crate::sqp::qp_assembly::Triplet,
441) -> KktError {
442 let mut viol = 0.0_f64;
445 for i in 0..m {
446 let lo = if bl_c[i] > NLP_LOWER_BOUND_INF {
447 (bl_c[i] - c_vals[i]).max(0.0)
448 } else {
449 0.0
450 };
451 let hi = if bu_c[i] < NLP_UPPER_BOUND_INF {
452 (c_vals[i] - bu_c[i]).max(0.0)
453 } else {
454 0.0
455 };
456 viol = viol.max(lo).max(hi);
457 }
458 for i in 0..n {
459 let lo = if xl[i] > NLP_LOWER_BOUND_INF {
460 (xl[i] - iter.x[i]).max(0.0)
461 } else {
462 0.0
463 };
464 let hi = if xu[i] < NLP_UPPER_BOUND_INF {
465 (iter.x[i] - xu[i]).max(0.0)
466 } else {
467 0.0
468 };
469 viol = viol.max(lo).max(hi);
470 }
471
472 let mut stat = vec![0.0; n];
479 for (s, &g) in stat.iter_mut().zip(grad_f.iter()) {
480 *s = g;
481 }
482 for k in 0..jac_c.irow.len() {
484 let i = (jac_c.irow[k] - 1) as usize; let j = (jac_c.jcol[k] - 1) as usize; stat[j] += jac_c.vals[k] * iter.lambda_g[i];
487 }
488 for (s, &lx) in stat.iter_mut().zip(iter.lambda_x.iter()) {
490 *s -= lx;
491 }
492 let stat_max = stat.iter().map(|s| s.abs()).fold(0.0_f64, f64::max);
493
494 KktError {
495 stationarity: stat_max,
496 constr_viol: viol,
497 }
498}