use crate::sqp::bfgs::DampedBfgs;
use crate::sqp::filter::{filter_line_search, SqpFilter};
use crate::sqp::iterates::SqpIterates;
use crate::sqp::lbfgs::LBfgs;
use crate::sqp::line_search::l1_merit_line_search;
use crate::sqp::options::{SqpGlobalization, SqpHessianSource, SqpOptions};
use crate::sqp::problem::SqpProblemSpec;
use crate::sqp::qp_assembly::{SqpQpData, Triplet};
use crate::sqp::result::{SqpError, SqpResult, SqpStatus};
use pounce_common::types::{Number, NLP_LOWER_BOUND_INF, NLP_UPPER_BOUND_INF};
use pounce_qp::{HessianInertia, ParametricActiveSetSolver, QpOptions, QpSolver, QpStatus};
pub struct SqpAlgorithm {
qp_solver: ParametricActiveSetSolver,
qp_opts: QpOptions,
opts: SqpOptions,
iterates: Option<SqpIterates>,
filter: SqpFilter,
}
impl SqpAlgorithm {
pub fn new(qp_solver: ParametricActiveSetSolver, opts: SqpOptions) -> Self {
Self {
qp_solver,
qp_opts: QpOptions::default(),
opts,
iterates: None,
filter: SqpFilter::new(),
}
}
pub fn with_qp_options(mut self, qp_opts: QpOptions) -> Self {
self.qp_opts = qp_opts;
self
}
pub fn options(&self) -> &SqpOptions {
&self.opts
}
pub fn iterates(&self) -> Option<&SqpIterates> {
self.iterates.as_ref()
}
pub fn optimize<N: SqpProblemSpec>(&mut self, nlp: &mut N) -> Result<SqpResult, SqpError> {
self.optimize_with_warm_start(nlp, None)
}
pub fn optimize_with_warm_start<N: SqpProblemSpec>(
&mut self,
nlp: &mut N,
warm: Option<SqpIterates>,
) -> Result<SqpResult, SqpError> {
let n = nlp.n();
let m = nlp.m();
let (xl, xu) = nlp.variable_bounds();
let (bl_c, bu_c) = nlp.constraint_bounds();
if xl.len() != n || xu.len() != n {
return Err(SqpError::DimensionMismatch(format!(
"variable_bounds length must be n = {n}"
)));
}
if bl_c.len() != m || bu_c.len() != m {
return Err(SqpError::DimensionMismatch(format!(
"constraint_bounds length must be m = {m}"
)));
}
let mut iter = match warm {
Some(w) => {
if w.x.len() != n {
return Err(SqpError::DimensionMismatch(format!(
"warm.x length {} must equal n = {n}",
w.x.len()
)));
}
if w.lambda_g.len() != m {
return Err(SqpError::DimensionMismatch(format!(
"warm.lambda_g length {} must equal m = {m}",
w.lambda_g.len()
)));
}
if w.lambda_x.len() != n {
return Err(SqpError::DimensionMismatch(format!(
"warm.lambda_x length {} must equal n = {n}",
w.lambda_x.len()
)));
}
if let Some(ws) = w.working.as_ref() {
ws.validate_dims(n, m).map_err(SqpError::QpFailure)?;
}
w
}
None => {
let mut cold = SqpIterates::cold(n, m);
let x_init = nlp.x_init();
if x_init.len() != n {
return Err(SqpError::DimensionMismatch(format!(
"x_init length must be n = {n}"
)));
}
cold.x = x_init;
cold
}
};
let mut n_qp_solves: u32 = 0;
let mut final_stationarity = 0.0;
let mut final_constr_viol = 0.0;
let mut nu = self.opts.l1_penalty;
self.filter = SqpFilter::new();
let mut f_cached: Option<Number> = None;
let mut c_cached: Option<Vec<Number>> = None;
let mut bfgs: Option<DampedBfgs> =
if matches!(self.opts.hessian, SqpHessianSource::DampedBfgs) {
Some(DampedBfgs::new(n))
} else {
None
};
let mut lbfgs: Option<LBfgs> = if matches!(self.opts.hessian, SqpHessianSource::Lbfgs) {
Some(LBfgs::new(n, self.opts.lbfgs_max_history.max(1) as usize))
} else {
None
};
for outer in 0..self.opts.max_iter {
let grad_f = nlp.eval_grad_f(&iter.x);
let c_vals = c_cached.take().unwrap_or_else(|| nlp.eval_c(&iter.x));
let f_curr = f_cached.take().unwrap_or_else(|| nlp.eval_f(&iter.x));
let jac_c = nlp.eval_jac_c(&iter.x);
let hess_lag = match self.opts.hessian {
SqpHessianSource::Exact => nlp.eval_hess_lag(&iter.x, &iter.lambda_g),
SqpHessianSource::DampedBfgs => {
let bfgs = bfgs.as_mut().expect("DampedBfgs state initialized above");
let grad_lag = compute_grad_lag(&grad_f, &jac_c, &iter.lambda_g, n);
bfgs.update(&iter.x, &grad_lag);
bfgs.as_triplet()
}
SqpHessianSource::Lbfgs => {
let lb = lbfgs.as_mut().expect("LBfgs state initialized above");
let grad_lag = compute_grad_lag(&grad_f, &jac_c, &iter.lambda_g, n);
lb.update(&iter.x, &grad_lag);
lb.as_triplet()
}
};
let kkt = check_kkt(
n, m, &iter, &grad_f, &c_vals, &bl_c, &bu_c, &xl, &xu, &jac_c,
);
final_stationarity = kkt.stationarity;
final_constr_viol = kkt.constr_viol;
#[cfg(test)]
if self.opts.print_level >= 1 {
tracing::debug!(target: "pounce::sqp",
"[sqp k={outer:3}] x={:?} f={:.4e} ‖c‖={:.2e} stat={:.2e} ν={:.2e}",
iter.x.iter().map(|v| format!("{v:.3}")).collect::<Vec<_>>(),
f_curr,
kkt.constr_viol,
kkt.stationarity,
nu,
);
}
if kkt.stationarity <= self.opts.dual_inf_tol
&& kkt.constr_viol <= self.opts.constr_viol_tol
{
self.iterates = Some(iter.clone());
return Ok(SqpResult {
x: iter.x,
lambda_g: iter.lambda_g,
lambda_x: iter.lambda_x,
obj: f_curr,
status: SqpStatus::Optimal,
n_iter: outer,
n_qp_solves,
final_stationarity,
final_constr_viol,
working_set: iter.working,
});
}
let qp_data = SqpQpData::build(
&iter.x,
&grad_f,
&c_vals,
&bl_c,
&bu_c,
&xl,
&xu,
jac_c,
hess_lag,
self.hessian_inertia(),
);
let qp = qp_data.as_qp();
let sol = if let Some(prev_w) = iter.working.as_ref() {
self.qp_solver
.solve_with_working_set(&qp, prev_w, &self.qp_opts)?
} else {
self.qp_solver.solve(&qp, None, &self.qp_opts)?
};
n_qp_solves += 1;
match sol.status {
QpStatus::Optimal => {}
QpStatus::Infeasible => {
let obj = nlp.eval_f(&iter.x);
self.iterates = Some(iter.clone());
return Ok(SqpResult {
x: iter.x,
lambda_g: iter.lambda_g,
lambda_x: iter.lambda_x,
obj,
status: SqpStatus::InfeasibleSubproblem,
n_iter: outer,
n_qp_solves,
final_stationarity,
final_constr_viol,
working_set: iter.working,
});
}
other => {
return Err(SqpError::QpFailure(
pounce_qp::QpError::LinearSolverFailure(format!(
"QP subproblem returned status {other}"
)),
));
}
}
#[cfg(test)]
if self.opts.print_level >= 1 {
let p_inf = sol.x.iter().map(|v| v.abs()).fold(0.0_f64, f64::max);
tracing::debug!(target: "pounce::sqp",
" qp: ‖p‖_inf={:.3e} ‖λ_g_qp‖_inf={:.3e}",
p_inf,
sol.lambda_g.iter().map(|v| v.abs()).fold(0.0_f64, f64::max)
);
}
let ls = match self.opts.globalization {
SqpGlobalization::L1Elastic => l1_merit_line_search(
nlp,
&iter.x,
&sol.x,
&sol.lambda_g,
&grad_f,
f_curr,
&c_vals,
&bl_c,
&bu_c,
&xl,
&xu,
nu,
&self.opts,
),
SqpGlobalization::Filter => filter_line_search(
nlp,
&mut self.filter,
&iter.x,
&sol.x,
f_curr,
&c_vals,
&bl_c,
&bu_c,
&xl,
&xu,
nu,
&self.opts,
),
};
#[cfg(test)]
if self.opts.print_level >= 1 {
tracing::debug!(target: "pounce::sqp",
" ls: α={:.3e} ν={:.3e} ok={} f_new={:.3e}",
ls.alpha, ls.nu, ls.success, ls.f_new
);
}
if !ls.success {
self.iterates = Some(iter.clone());
return Ok(SqpResult {
x: iter.x,
lambda_g: iter.lambda_g,
lambda_x: iter.lambda_x,
obj: f_curr,
status: SqpStatus::LineSearchFailed,
n_iter: outer,
n_qp_solves,
final_stationarity,
final_constr_viol,
working_set: Some(sol.working),
});
}
iter.x = ls.x_new;
for (l, &lq) in iter.lambda_g.iter_mut().zip(sol.lambda_g.iter()) {
*l = (1.0 - ls.alpha) * *l + ls.alpha * lq;
}
for (l, &lq) in iter.lambda_x.iter_mut().zip(sol.lambda_x.iter()) {
*l = (1.0 - ls.alpha) * *l + ls.alpha * lq;
}
iter.working = Some(sol.working);
nu = ls.nu;
f_cached = Some(ls.f_new);
c_cached = Some(ls.c_new);
}
let obj = nlp.eval_f(&iter.x);
self.iterates = Some(iter.clone());
Ok(SqpResult {
x: iter.x,
lambda_g: iter.lambda_g,
lambda_x: iter.lambda_x,
obj,
status: SqpStatus::MaxIter,
n_iter: self.opts.max_iter,
n_qp_solves,
final_stationarity,
final_constr_viol,
working_set: iter.working,
})
}
fn hessian_inertia(&self) -> HessianInertia {
match self.opts.hessian {
crate::sqp::SqpHessianSource::Exact => HessianInertia::Indefinite,
crate::sqp::SqpHessianSource::DampedBfgs => HessianInertia::Psd,
crate::sqp::SqpHessianSource::Lbfgs => HessianInertia::Psd,
}
}
}
#[derive(Debug, Clone, Copy)]
struct KktError {
pub stationarity: Number,
pub constr_viol: Number,
}
fn compute_grad_lag(
grad_f: &[Number],
jac_c: &Triplet,
lambda_g: &[Number],
n: usize,
) -> Vec<Number> {
let mut out = grad_f.to_vec();
debug_assert_eq!(out.len(), n);
for k in 0..jac_c.irow.len() {
let row_i = (jac_c.irow[k] - 1) as usize;
let col_j = (jac_c.jcol[k] - 1) as usize;
out[col_j] += jac_c.vals[k] * lambda_g[row_i];
}
out
}
fn check_kkt(
n: usize,
m: usize,
iter: &SqpIterates,
grad_f: &[Number],
c_vals: &[Number],
bl_c: &[Number],
bu_c: &[Number],
xl: &[Number],
xu: &[Number],
jac_c: &crate::sqp::qp_assembly::Triplet,
) -> KktError {
let mut viol = 0.0_f64;
for i in 0..m {
let lo = if bl_c[i] > NLP_LOWER_BOUND_INF {
(bl_c[i] - c_vals[i]).max(0.0)
} else {
0.0
};
let hi = if bu_c[i] < NLP_UPPER_BOUND_INF {
(c_vals[i] - bu_c[i]).max(0.0)
} else {
0.0
};
viol = viol.max(lo).max(hi);
}
for i in 0..n {
let lo = if xl[i] > NLP_LOWER_BOUND_INF {
(xl[i] - iter.x[i]).max(0.0)
} else {
0.0
};
let hi = if xu[i] < NLP_UPPER_BOUND_INF {
(iter.x[i] - xu[i]).max(0.0)
} else {
0.0
};
viol = viol.max(lo).max(hi);
}
let mut stat = vec![0.0; n];
for (s, &g) in stat.iter_mut().zip(grad_f.iter()) {
*s = g;
}
for k in 0..jac_c.irow.len() {
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];
}
for (s, &lx) in stat.iter_mut().zip(iter.lambda_x.iter()) {
*s -= lx;
}
let stat_max = stat.iter().map(|s| s.abs()).fold(0.0_f64, f64::max);
KktError {
stationarity: stat_max,
constr_viol: viol,
}
}