use crate::sqp::bfgs::DampedBfgs;
use crate::sqp::filter::{SqpFilter, filter_line_search};
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::{NLP_LOWER_BOUND_INF, NLP_UPPER_BOUND_INF, Number};
use pounce_linalg::triplet::GenTMatrix;
use pounce_qp::{
BoundStatus, ConsStatus, HessianInertia, ParametricActiveSetSolver, QpOptions, QpSolver,
QpStatus, WorkingSet,
};
const SQP_ITER_HEADER: &str = "iter objective inf_pr inf_du ||p|| alpha ws";
struct SqpStepRecord {
p_inf: Number,
alpha: Number,
soc: bool,
ws_changes: u32,
}
fn format_sqp_row(
iter: u32,
obj: Number,
inf_pr: Number,
inf_du: Number,
step: Option<&SqpStepRecord>,
) -> String {
use crate::output::orig::format_e;
match step {
Some(st) => format!(
"{:>4} {:>14} {:>8} {:>8} {:>8} {:>8}{} {:>5}",
iter,
format_e(obj, 7),
format_e(inf_pr, 2),
format_e(inf_du, 2),
format_e(st.p_inf, 2),
format_e(st.alpha, 2),
if st.soc { 'c' } else { ' ' },
st.ws_changes,
),
None => format!(
"{:>4} {:>14} {:>8} {:>8} {:>8} {:>8} {:>5}",
iter,
format_e(obj, 7),
format_e(inf_pr, 2),
format_e(inf_du, 2),
"-",
"-",
"-",
),
}
}
pub struct SqpIterationInfo<'a> {
pub iter: u32,
pub obj: Number,
pub inf_pr: Number,
pub inf_du: Number,
pub p_inf: Number,
pub alpha: Number,
pub x: &'a [Number],
pub lambda_g: &'a [Number],
pub lambda_x: &'a [Number],
pub c_vals: &'a [Number],
pub grad_lag_x: &'a [Number],
}
pub type SqpIterationHook = Box<dyn FnMut(&SqpIterationInfo<'_>) -> bool>;
pub struct SqpAlgorithm {
iteration_hook: Option<SqpIterationHook>,
qp_solver: ParametricActiveSetSolver,
qp_opts: QpOptions,
opts: SqpOptions,
hessian: SqpHessianSource,
iterates: Option<SqpIterates>,
filter: SqpFilter,
ipm_cold_start: bool,
ipm_seed: IpmSeed,
}
#[derive(Default)]
pub(crate) struct IpmSeed {
pub(crate) last_iters: std::cell::Cell<Option<usize>>,
}
impl SqpAlgorithm {
pub fn new(qp_solver: ParametricActiveSetSolver, opts: SqpOptions) -> Self {
Self {
qp_solver,
qp_opts: QpOptions::sqp_subproblem(),
hessian: opts.hessian,
opts,
iterates: None,
filter: SqpFilter::new(),
iteration_hook: None,
ipm_cold_start: false,
ipm_seed: IpmSeed::default(),
}
}
pub fn set_iteration_hook(&mut self, hook: SqpIterationHook) {
self.iteration_hook = Some(hook);
}
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 saved = (self.qp_opts.max_iter, self.qp_opts.use_schur_updates);
self.qp_opts.max_iter = step_qp_iter_budget(saved.0, nlp.n(), nlp.m());
if let Some(on) = self.opts.step_qp_schur_updates(nlp.n() + nlp.m(), None) {
self.qp_opts.use_schur_updates = on;
}
self.ipm_cold_start = self
.opts
.qp_ipm_cold_start_min_dim
.is_some_and(|d| nlp.n() + nlp.m() >= d);
self.ipm_seed.last_iters.set(None);
let res = self.optimize_inner(nlp, warm);
(self.qp_opts.max_iter, self.qp_opts.use_schur_updates) = saved;
res
}
fn optimize_inner<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 n_qp_working_set_changes: u32 = 0;
let mut final_stationarity = 0.0;
let mut final_constr_viol = 0.0;
let mut nu = self.opts.l1_penalty;
let mut prev_step: Option<SqpStepRecord> = None;
self.filter = SqpFilter::new();
let mut f_cached: Option<Number> = None;
let mut c_cached: Option<Vec<Number>> = None;
let mut prev_point: Option<(Vec<Number>, Vec<Number>, Triplet)> = None;
self.hessian = self.opts.hessian.resolve(n, self.opts.dense_hessian_max_n);
let lift_lbfgs =
self.hessian == SqpHessianSource::Lbfgs && n > self.opts.dense_hessian_max_n;
let mut bfgs: Option<DampedBfgs> = if matches!(self.hessian, SqpHessianSource::DampedBfgs) {
Some(DampedBfgs::new(n))
} else {
None
};
let mut lbfgs: Option<LBfgs> = if matches!(self.hessian, SqpHessianSource::Lbfgs) {
Some(LBfgs::new(n, self.opts.lbfgs_max_history.max(1) as usize))
} else {
None
};
if bfgs.is_some() || lbfgs.is_some() {
let g0 = nlp.eval_grad_f(&iter.x);
let g_norm = g0.iter().map(|v| v * v).sum::<Number>().sqrt();
if g_norm.is_finite() && g_norm > 0.0 {
let x_scale = iter.x.iter().map(|v| v.abs()).fold(1.0, f64::max);
let eps = 1e-7 * x_scale;
let step: Vec<Number> = g0.iter().map(|gi| -eps * gi / g_norm).collect();
let x_probe: Vec<Number> =
iter.x.iter().zip(step.iter()).map(|(a, d)| a + d).collect();
let linear_constraints = m == 0 || {
let j0 = nlp.eval_jac_c(&iter.x);
let j1 = nlp.eval_jac_c(&x_probe);
j0.vals.len() == j1.vals.len()
&& j0.vals.iter().zip(j1.vals.iter()).all(|(a, c)| {
let scale = a.abs().max(c.abs()).max(1.0);
(a - c).abs() <= 1e-12 * scale
})
};
if linear_constraints {
let g1 = nlp.eval_grad_f(&x_probe);
let s_y: Number = step
.iter()
.zip(g1.iter().zip(g0.iter()))
.map(|(si, (a, bg))| si * (a - bg))
.sum();
let s_s: Number = step.iter().map(|v| v * v).sum();
if s_s > 0.0 && s_y.is_finite() {
if let Some(b) = bfgs.as_mut() {
b.seed_scale(s_y / s_s);
}
if let Some(b) = lbfgs.as_mut() {
b.seed_scale(s_y / s_s);
}
}
}
}
}
const MAX_SECOND_ORDER_ESCAPES: u32 = 8;
let mut escapes: u32 = 0;
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 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;
if let Some(hook) = self.iteration_hook.as_mut() {
let (p_inf, alpha) = prev_step
.as_ref()
.map_or((0.0, 0.0), |s| (s.p_inf, s.alpha));
let mut grad_lag_x = grad_f.clone();
for k in 0..jac_c.irow.len() {
let (i, j) = ((jac_c.irow[k] - 1) as usize, (jac_c.jcol[k] - 1) as usize);
if let (Some(g), Some(&l)) = (grad_lag_x.get_mut(j), iter.lambda_g.get(i)) {
*g += jac_c.vals[k] * l;
}
}
for (g, &lx) in grad_lag_x.iter_mut().zip(iter.lambda_x.iter()) {
*g -= lx;
}
let keep_going = hook(&SqpIterationInfo {
iter: outer,
obj: f_curr,
inf_pr: kkt.constr_viol,
inf_du: kkt.stationarity,
p_inf,
alpha,
x: &iter.x,
lambda_g: &iter.lambda_g,
lambda_x: &iter.lambda_x,
c_vals: &c_vals,
grad_lag_x: &grad_lag_x,
});
if !keep_going {
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::UserRequestedStop,
n_iter: outer,
n_qp_solves,
n_qp_working_set_changes,
final_stationarity,
final_constr_viol,
working_set: iter.working,
});
}
}
let mut low_rank = None;
let hess_lag = match self.hessian {
SqpHessianSource::Exact => nlp.eval_hess_lag(&iter.x, &iter.lambda_g),
SqpHessianSource::DampedBfgs => {
let bfgs = bfgs.as_mut().expect("DampedBfgs state initialized above");
if let Some((s, y)) =
curvature_pair(prev_point.as_ref(), &iter, &grad_f, &jac_c, n)
{
bfgs.update_sy(&s, &y);
}
bfgs.as_triplet()
}
SqpHessianSource::Lbfgs => {
let lb = lbfgs.as_mut().expect("LBfgs state initialized above");
if let Some((s, y)) =
curvature_pair(prev_point.as_ref(), &iter, &grad_f, &jac_c, n)
{
lb.update_sy(&s, &y);
}
if lift_lbfgs {
low_rank = Some(lb.low_rank());
Triplet {
n_rows: n,
n_cols: n,
irow: Vec::new(),
jcol: Vec::new(),
vals: Vec::new(),
}
} else {
lb.as_triplet()
}
}
SqpHessianSource::QuasiNewtonAuto => {
unreachable!("resolved at the top of the solve")
}
};
prev_point = Some((iter.x.clone(), grad_f.clone(), jac_c.clone()));
if !kkt.stationarity.is_finite() || !kkt.constr_viol.is_finite() || !f_curr.is_finite()
{
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::InvalidNumber,
n_iter: outer,
n_qp_solves,
n_qp_working_set_changes,
final_stationarity,
final_constr_viol,
working_set: iter.working,
});
}
if self.opts.print_level >= 1 {
if outer % 10 == 0 {
println!("{SQP_ITER_HEADER}");
}
println!(
"{}",
format_sqp_row(
outer,
f_curr,
kkt.constr_viol,
kkt.stationarity,
prev_step.as_ref(),
)
);
}
if self.opts.print_level >= 2 {
println!(
" x={:?} ν={:.2e}",
iter.x.iter().map(|v| format!("{v:.3}")).collect::<Vec<_>>(),
nu,
);
}
let stationarity_tol = self.opts.tol.min(self.opts.dual_inf_tol);
if kkt.stationarity <= stationarity_tol && kkt.constr_viol <= self.opts.constr_viol_tol
{
let exact_hess = if matches!(self.hessian, SqpHessianSource::Exact) {
None
} else {
Some(nlp.eval_hess_lag(&iter.x, &iter.lambda_g))
};
if escapes < MAX_SECOND_ORDER_ESCAPES
&& let Some(d) = negative_curvature_at_kkt_point(
n,
m,
&iter.x,
exact_hess.as_ref().unwrap_or(&hess_lag),
&jac_c,
&c_vals,
&bl_c,
&bu_c,
&xl,
&xu,
self.opts.constr_viol_tol,
)
&& let Some(next) = exhibit_better_point(
nlp,
&iter.x,
&d,
f_curr,
&xl,
&xu,
&bl_c,
&bu_c,
self.opts.constr_viol_tol,
)
{
tracing::debug!(target: "pounce::sqp",
"first-order KKT point refuted at second order; stepping \
along negative curvature to a strictly better feasible \
point (gh #856)");
escapes += 1;
iter.x = next;
iter.working = None;
f_cached = None;
c_cached = None;
prev_point = None;
continue;
}
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,
n_qp_working_set_changes,
final_stationarity,
final_constr_viol,
working_set: iter.working,
});
}
if let Some(kind) = self.opts.deadline.as_ref().and_then(|d| d.exceeded()) {
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: kind.into(),
n_iter: outer,
n_qp_solves,
n_qp_working_set_changes,
final_stationarity,
final_constr_viol,
working_set: iter.working,
});
}
let qp_budget = self.opts.deadline.as_ref().map(deadline_remaining);
let qp_time_limit =
qp_time_limit_now(self.opts.deadline.as_ref(), self.qp_opts.time_limit);
let qp_data = match low_rank.as_ref() {
Some(b) => SqpQpData::build_low_rank(
&iter.x, &grad_f, &c_vals, &bl_c, &bu_c, &xl, &xu, jac_c, b,
),
None => SqpQpData::build(
&iter.x,
&grad_f,
&c_vals,
&bl_c,
&bu_c,
&xl,
&xu,
jac_c,
hess_lag,
self.hessian_inertia(),
),
};
const SCALE_MAX: Number = 1e6;
let g_inf = grad_f.iter().map(|v| v.abs()).fold(0.0, f64::max);
let b_inf = qp_data.hess_inf;
let base = self.qp_opts.clone();
self.qp_opts.time_limit = qp_time_limit;
let scale = g_inf.max(b_inf).clamp(1.0, SCALE_MAX);
if scale > 1.0 {
self.qp_opts.opt_tol = base.opt_tol * scale;
self.qp_opts.feas_tol = base.feas_tol * scale;
}
let warm_started = iter.working.is_some();
let mut sol = if let Some(prev_w) = iter.working.as_ref() {
let warm_opts = self.warm_qp_opts(&self.qp_opts, n + m, qp_data.hessian_inertia);
match solve_step_qp(
&mut self.qp_solver,
&qp_data,
Some(prev_w),
warm_opts.as_ref().unwrap_or(&self.qp_opts),
self.ipm_cold_start.then_some(&self.ipm_seed),
) {
Ok(v) => v,
Err(e) => {
tracing::debug!(target: "pounce::sqp",
"warm-started step QP failed hard ({e:?}); re-solving \
from cold, since the carried working set is what a \
cold start rebuilds (gh #855)");
n_qp_solves += 1;
solve_step_qp(
&mut self.qp_solver,
&qp_data,
None,
&self.qp_opts,
self.ipm_cold_start.then_some(&self.ipm_seed),
)?
}
}
} else {
solve_step_qp(
&mut self.qp_solver,
&qp_data,
None,
&self.qp_opts,
self.ipm_cold_start.then_some(&self.ipm_seed),
)?
};
n_qp_solves += 1;
n_qp_working_set_changes += sol.stats.n_working_set_changes;
if warm_started && matches!(sol.status, QpStatus::MaxIter | QpStatus::NumericalError) {
let cold = solve_step_qp(
&mut self.qp_solver,
&qp_data,
None,
&self.qp_opts,
self.ipm_cold_start.then_some(&self.ipm_seed),
)?;
n_qp_solves += 1;
n_qp_working_set_changes += cold.stats.n_working_set_changes;
if matches!(cold.status, QpStatus::Optimal | QpStatus::Unbounded) {
sol = cold;
}
}
let mut qp_data = qp_data;
if matches!(sol.status, QpStatus::MaxIter | QpStatus::NumericalError)
&& let Some(b) = bfgs.as_mut()
{
b.reset_to_scale();
qp_data = SqpQpData::build(
&iter.x,
&grad_f,
&c_vals,
&bl_c,
&bu_c,
&xl,
&xu,
nlp.eval_jac_c(&iter.x),
b.as_triplet(),
self.hessian_inertia(),
);
let retry = solve_step_qp(
&mut self.qp_solver,
&qp_data,
None,
&self.qp_opts,
self.ipm_cold_start.then_some(&self.ipm_seed),
)?;
n_qp_solves += 1;
n_qp_working_set_changes += retry.stats.n_working_set_changes;
if matches!(retry.status, QpStatus::Optimal | QpStatus::Unbounded) {
sol = retry;
}
}
let mut ray_certified = false;
if sol.status == QpStatus::Unbounded {
ray_certified = sol.unbounded_ray.as_ref().is_some_and(|d| {
ray_certifies_unbounded(
nlp,
&iter.x,
d,
f_curr,
&grad_f,
&bl_c,
&bu_c,
&xl,
&xu,
self.opts.constr_viol_tol,
)
});
if !ray_certified {
tracing::debug!(target: "pounce::sqp",
"unbounded step QP whose recession ray does not survive \
re-testing against the NLP — re-solving for the δ-shifted \
proximal step (gh #423)");
let prox_opts = QpOptions {
certify_recession_ray: false,
..self.qp_opts.clone()
};
let prox = solve_step_qp(
&mut self.qp_solver,
&qp_data,
None,
&prox_opts,
self.ipm_cold_start.then_some(&self.ipm_seed),
)?;
n_qp_solves += 1;
if prox.status == QpStatus::Optimal {
sol = prox;
}
}
}
self.qp_opts = base;
let elastic_step = sol.status == QpStatus::Infeasible && {
let zero = vec![0.0; n];
let before = linearized_l1_violation(&qp_data, &zero);
let after = linearized_l1_violation(&qp_data, &sol.x);
tracing::debug!(target: "pounce::sqp",
"infeasible step QP: linearized ℓ1 violation {before:.3e} → {after:.3e} \
along its elastic step");
after < (1.0 - ELASTIC_STEP_MIN_GAIN) * before
};
if elastic_step {
sol.lambda_g.clone_from(&iter.lambda_g);
sol.lambda_x.clone_from(&iter.lambda_x);
}
match sol.status {
QpStatus::Optimal => {}
QpStatus::Infeasible if elastic_step => {}
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,
n_qp_working_set_changes,
final_stationarity,
final_constr_viol,
working_set: iter.working,
});
}
QpStatus::MaxIter | QpStatus::TimeLimit | QpStatus::NumericalError => {
let obj = nlp.eval_f(&iter.x);
self.iterates = Some(iter.clone());
let status = match sol.status {
QpStatus::TimeLimit => match qp_budget {
Some((left, budget))
if self.qp_opts.time_limit.is_none_or(|own| left <= own) =>
{
budget.into()
}
_ => SqpStatus::QpIterationLimit,
},
QpStatus::MaxIter => SqpStatus::QpIterationLimit,
_ => SqpStatus::QpStepFailed,
};
return Ok(SqpResult {
x: iter.x,
lambda_g: iter.lambda_g,
lambda_x: iter.lambda_x,
obj,
status,
n_iter: outer,
n_qp_solves,
n_qp_working_set_changes,
final_stationarity,
final_constr_viol,
working_set: iter.working,
});
}
QpStatus::Unbounded => {
let certified = ray_certified;
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: if certified {
SqpStatus::Unbounded
} else {
SqpStatus::QpStepFailed
},
n_iter: outer,
n_qp_solves,
n_qp_working_set_changes,
final_stationarity,
final_constr_viol,
working_set: iter.working,
});
}
}
let p_inf = sol.x.iter().map(|v| v.abs()).fold(0.0_f64, f64::max);
if self.opts.print_level >= 2 {
println!(
" 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 a_p = if m > 0 {
mat_vec_gen(&qp_data.a, &sol.x, m)
} else {
Vec::new()
};
let mut n_soc_solves: u32 = 0;
let mut soc_working: Option<WorkingSet> = None;
let ls = {
let warm_opts = self.warm_qp_opts(&self.qp_opts, n + m, qp_data.hessian_inertia);
let qp_solver = &mut self.qp_solver;
let qp_opts = warm_opts.as_ref().unwrap_or(&self.qp_opts);
let deadline = self.opts.deadline.as_ref();
let seed = self.ipm_cold_start.then_some(&self.ipm_seed);
let qp_data_ref = &qp_data;
let c_curr_ref = &c_vals;
let a_p_ref = &a_p;
let sol_working = &sol.working;
let n_soc = &mut n_soc_solves;
let soc_working_slot = &mut soc_working;
let mut soc = |c_trial: &[Number]| -> Option<crate::sqp::line_search::SocStep> {
let mm = qp_data_ref.m;
let mut bl_soc = qp_data_ref.bl.clone();
let mut bu_soc = qp_data_ref.bu.clone();
for i in 0..mm {
let delta = c_curr_ref[i] - c_trial[i] + a_p_ref[i];
if qp_data_ref.bl[i] > NLP_LOWER_BOUND_INF {
bl_soc[i] = qp_data_ref.bl[i] + delta;
}
if qp_data_ref.bu[i] < NLP_UPPER_BOUND_INF {
bu_soc[i] = qp_data_ref.bu[i] + delta;
}
}
let qp_soc = qp_data_ref.as_qp_with_row_bounds(&bl_soc, &bu_soc);
let soc_opts = QpOptions {
time_limit: qp_time_limit_now(deadline, qp_opts.time_limit),
..qp_opts.clone()
};
let sol_soc = solve_subproblem_qp(
qp_solver,
qp_data_ref,
qp_soc,
Some(sol_working),
&soc_opts,
seed,
"SOC QP",
)
.ok()?;
*n_soc += 1;
if sol_soc.status == QpStatus::Optimal {
*soc_working_slot = Some(sol_soc.working);
Some(crate::sqp::line_search::SocStep {
p: sol_soc.x,
lambda_g: sol_soc.lambda_g,
lambda_x: sol_soc.lambda_x,
})
} else {
None
}
};
let soc_ref: Option<crate::sqp::line_search::SocProvider<'_>> =
if m > 0 { Some(&mut soc) } else { None };
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,
soc_ref,
),
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,
soc_ref,
),
}
};
n_qp_solves += n_soc_solves;
if self.opts.print_level >= 2 {
println!(
" 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,
n_qp_working_set_changes,
final_stationarity,
final_constr_viol,
working_set: Some(sol.working),
});
}
iter.x = ls.x_new;
let qp_ws_changes = sol.stats.n_working_set_changes;
let soc_taken = ls.soc_duals.is_some();
match ls.soc_duals {
Some((soc_lg, soc_lx)) => {
iter.lambda_g = soc_lg;
iter.lambda_x = soc_lx;
iter.working = soc_working.take().or(Some(sol.working));
}
None => {
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);
prev_step = Some(SqpStepRecord {
p_inf,
alpha: ls.alpha,
soc: soc_taken,
ws_changes: qp_ws_changes,
});
}
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,
n_qp_working_set_changes,
final_stationarity,
final_constr_viol,
working_set: iter.working,
})
}
pub(crate) fn warm_qp_opts(
&self,
base: &QpOptions,
dim: usize,
inertia: HessianInertia,
) -> Option<QpOptions> {
self.opts
.step_qp_schur_updates(dim, Some(inertia))
.filter(|&on| on != base.use_schur_updates)
.map(|on| QpOptions {
use_schur_updates: on,
..base.clone()
})
}
fn hessian_inertia(&self) -> HessianInertia {
if self.hessian.is_quasi_newton() {
HessianInertia::Psd
} else {
HessianInertia::Indefinite
}
}
}
#[allow(clippy::too_many_arguments)]
fn ray_certifies_unbounded<N: SqpProblemSpec>(
nlp: &mut N,
x: &[Number],
dir: &[Number],
f_x: Number,
grad_f: &[Number],
bl_c: &[Number],
bu_c: &[Number],
xl: &[Number],
xu: &[Number],
constr_viol_tol: Number,
) -> bool {
const PROBES: [Number; 7] = [1e0, 1e2, 1e4, 1e6, 1e8, 1e10, 1e12];
const ROUNDOFF_REL: Number = 1e-12;
let n = x.len();
if dir.len() != n || grad_f.len() != n || !f_x.is_finite() {
return false;
}
let scale = dir.iter().map(|v| v.abs()).fold(0.0, f64::max);
if !scale.is_finite() || scale <= 0.0 {
return false;
}
let d: Vec<Number> = dir.iter().map(|v| v / scale).collect();
let slope: Number = grad_f.iter().zip(d.iter()).map(|(g, di)| g * di).sum();
let g_norm = grad_f.iter().map(|v| v * v).sum::<Number>().sqrt();
let descent_bar = -1e-9 * g_norm.max(1.0);
if !slope.is_finite() || slope >= descent_bar {
return false;
}
let m = bl_c.len();
let mut row_scale: Vec<Number> = vec![0.0; m];
{
let jac = nlp.eval_jac_c(x);
for k in 0..jac.vals.len() {
let i = (jac.irow[k] - 1) as usize;
row_scale[i] = row_scale[i].max(jac.vals[k].abs());
}
}
for &t in PROBES.iter() {
let xt: Vec<Number> = x.iter().zip(d.iter()).map(|(xi, di)| xi + t * di).collect();
if xt.iter().any(|v| !v.is_finite()) {
return false;
}
for i in 0..n {
let tol = 1e-9 * (1.0 + xt[i].abs());
if xl[i] > NLP_LOWER_BOUND_INF && xt[i] < xl[i] - tol {
return false;
}
if xu[i] < NLP_UPPER_BOUND_INF && xt[i] > xu[i] + tol {
return false;
}
}
let x_inf = xt.iter().map(|v| v.abs()).fold(0.0, f64::max);
let c = nlp.eval_c(&xt);
if c.len() != m {
return false;
}
for i in 0..m {
if c[i].is_nan() {
return false;
}
let tol =
constr_viol_tol.max(0.0) * (1.0 + c[i].abs()) + ROUNDOFF_REL * row_scale[i] * x_inf;
if bl_c[i] > NLP_LOWER_BOUND_INF && c[i] < bl_c[i] - tol {
return false;
}
if bu_c[i] < NLP_UPPER_BOUND_INF && c[i] > bu_c[i] + tol {
return false;
}
}
let f_t = nlp.eval_f(&xt);
if f_t.is_nan() || f_t > f_x + 0.5 * slope * t {
return false;
}
}
true
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct KktError {
pub stationarity: Number,
pub constr_viol: Number,
}
const ELASTIC_STEP_MIN_GAIN: Number = 1e-6;
fn linearized_l1_violation(data: &SqpQpData, d: &[Number]) -> Number {
let (m, n) = (data.m, data.n);
let ad = mat_vec_gen(&data.a, d, m);
crate::sqp::line_search::l1_violation(
&d[..n],
&ad,
&data.bl[..m],
&data.bu[..m],
&data.xl[..n],
&data.xu[..n],
)
}
fn mat_vec_gen(a: &GenTMatrix, p: &[Number], m: usize) -> Vec<Number> {
let mut out = vec![0.0; m];
let irows = a.irows();
let jcols = a.jcols();
let vals = a.values();
for k in 0..vals.len() {
let i = (irows[k] - 1) as usize;
let j = (jcols[k] - 1) as usize;
out[i] += vals[k] * p[j];
}
out
}
fn curvature_pair(
prev: Option<&(Vec<Number>, Vec<Number>, Triplet)>,
iter: &SqpIterates,
grad_f: &[Number],
jac_c: &Triplet,
n: usize,
) -> Option<(Vec<Number>, Vec<Number>)> {
let (prev_x, prev_grad_f, prev_jac) = prev?;
let s: Vec<Number> = iter
.x
.iter()
.zip(prev_x.iter())
.map(|(a, b)| a - b)
.collect();
let lag_curr = compute_grad_lag(grad_f, jac_c, &iter.lambda_g, n);
let lag_prev = compute_grad_lag(prev_grad_f, prev_jac, &iter.lambda_g, n);
let y: Vec<Number> = lag_curr
.iter()
.zip(lag_prev.iter())
.map(|(a, b)| a - b)
.collect();
Some((s, y))
}
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
}
#[allow(clippy::too_many_arguments)]
fn exhibit_better_point<N: SqpProblemSpec>(
nlp: &mut N,
x: &[Number],
d: &[Number],
f_curr: Number,
xl: &[Number],
xu: &[Number],
bl_c: &[Number],
bu_c: &[Number],
constr_viol_tol: Number,
) -> Option<Vec<Number>> {
let n = x.len();
let dn = d.iter().fold(0.0_f64, |a, v| a.max(v.abs()));
if !(dn > 0.0) || !dn.is_finite() {
return None;
}
let viol_at = |nlp: &mut N, v: &[Number]| -> (Number, Number) {
let c = nlp.eval_c(v);
let mut viol = 0.0_f64;
let mut scale = 0.0_f64;
for (j, &cj) in c.iter().enumerate() {
viol = viol
.max((bl_c[j] - cj).max(0.0))
.max((cj - bu_c[j]).max(0.0));
scale = scale.max(cj.abs());
}
(viol, scale)
};
let (viol_curr, _) = viol_at(nlp, x);
let mut best: Option<(Number, Vec<Number>)> = None;
for sign in [1.0_f64, -1.0] {
let mut alpha = f64::INFINITY;
for i in 0..n {
let di = sign * d[i];
if di > 1e-12 * dn && xu[i] < f64::INFINITY {
alpha = alpha.min((xu[i] - x[i]) / di);
}
if di < -1e-12 * dn && xl[i] > f64::NEG_INFINITY {
alpha = alpha.min((x[i] - xl[i]) / -di);
}
}
if !alpha.is_finite() {
alpha = 1.0 / dn;
}
for _ in 0..24 {
if !(alpha > 0.0) {
break;
}
let trial: Vec<Number> = (0..n)
.map(|i| (x[i] + sign * alpha * d[i]).clamp(xl[i], xu[i]))
.collect();
let (viol, c_scale) = viol_at(nlp, &trial);
let feas_bar = viol_curr.max(1e-12 * c_scale).min(constr_viol_tol);
if viol <= feas_bar {
let f_trial = nlp.eval_f(&trial);
if f_trial.is_finite() && best.as_ref().is_none_or(|(b, _)| f_trial < *b) {
best = Some((f_trial, trial));
}
}
alpha *= 0.5;
}
}
let (f_best, x_best) = best?;
let f_scale = f_curr.abs().max(f_best.abs());
let bar = 1e-10 * (f_scale.min(1.0) + f_curr.abs());
(f_best < f_curr - bar).then_some(x_best)
}
fn active_tol(tol: Number, scale: &[Number]) -> Number {
let s = scale
.iter()
.filter(|v| v.is_finite())
.fold(0.0_f64, |a, v| a.max(v.abs()));
tol * s.min(1.0)
}
#[allow(clippy::too_many_arguments)]
fn negative_curvature_at_kkt_point(
n: usize,
m: usize,
x: &[Number],
hess_lag: &Triplet,
jac_c: &Triplet,
c_vals: &[Number],
bl_c: &[Number],
bu_c: &[Number],
xl: &[Number],
xu: &[Number],
tol: Number,
) -> Option<Vec<Number>> {
const MAX_N: usize = 512;
if n == 0 || n > MAX_N {
return None;
}
let mut rows: Vec<Vec<Number>> = Vec::new();
for j in 0..m {
let t = active_tol(tol, &[c_vals[j], bl_c[j], bu_c[j]]);
let lo_active = bl_c[j] > f64::NEG_INFINITY && (c_vals[j] - bl_c[j]).abs() <= t;
let hi_active = bu_c[j] < f64::INFINITY && (bu_c[j] - c_vals[j]).abs() <= t;
if lo_active || hi_active {
let mut r = vec![0.0; n];
for k in 0..jac_c.vals.len() {
if jac_c.irow[k] as usize == j + 1 {
r[jac_c.jcol[k] as usize - 1] += jac_c.vals[k];
}
}
rows.push(r);
}
}
for i in 0..n {
let t = active_tol(tol, &[x[i], xl[i], xu[i]]);
let on_lo = xl[i] > f64::NEG_INFINITY && (x[i] - xl[i]).abs() <= t;
let on_hi = xu[i] < f64::INFINITY && (xu[i] - x[i]).abs() <= t;
if on_lo || on_hi {
let mut r = vec![0.0; n];
r[i] = 1.0;
rows.push(r);
}
}
let mut z: Vec<Number> = Vec::new();
let n_dof;
if rows.is_empty() {
n_dof = n;
z = vec![0.0; n * n];
for i in 0..n {
z[i * n + i] = 1.0;
}
} else {
let mut btb = vec![0.0; n * n];
for r in &rows {
for a in 0..n {
if r[a] == 0.0 {
continue;
}
for c in 0..n {
btb[c * n + a] += r[a] * r[c];
}
}
}
let (mut ev, mut evec) = (vec![0.0; n], vec![0.0; n * n]);
if !pounce_linalg::symmetric_eigen(&btb, n, &mut ev, &mut evec) {
return None;
}
let lam_max = ev.iter().fold(0.0_f64, |a, v| a.max(v.abs()));
let cut = if lam_max > 0.0 { 1e-9 * lam_max } else { 0.0 };
if lam_max == 0.0 {
z.extend_from_slice(&evec[..n * n]);
n_dof = n;
} else {
for (j, &lam) in ev.iter().enumerate() {
if lam.abs() <= cut {
z.extend_from_slice(&evec[j * n..(j + 1) * n]);
}
}
n_dof = z.len() / n;
}
}
if n_dof == 0 {
return None;
}
let mut hz = vec![0.0; n * n_dof];
for k in 0..n_dof {
let (zc, out) = (&z[k * n..(k + 1) * n], &mut hz[k * n..(k + 1) * n]);
for e in 0..hess_lag.vals.len() {
let (r, c, v) = (
hess_lag.irow[e] as usize - 1,
hess_lag.jcol[e] as usize - 1,
hess_lag.vals[e],
);
out[r] += v * zc[c];
if r != c {
out[c] += v * zc[r];
}
}
}
let mut rh = vec![0.0; n_dof * n_dof];
for a in 0..n_dof {
for b in 0..n_dof {
rh[b * n_dof + a] = (0..n).map(|i| z[a * n + i] * hz[b * n + i]).sum();
}
}
let (mut ev, mut evec) = (vec![0.0; n_dof], vec![0.0; n_dof * n_dof]);
if !pounce_linalg::symmetric_eigen(&rh, n_dof, &mut ev, &mut evec) {
return None;
}
let h_scale = hess_lag.vals.iter().fold(0.0_f64, |a, v| a.max(v.abs()));
if ev[0] >= -1e-8 * h_scale {
return None;
}
let mut d = vec![0.0; n];
for (i, di) in d.iter_mut().enumerate() {
*di = (0..n_dof).map(|k| z[k * n + i] * evec[k]).sum();
}
Some(d)
}
fn step_qp_iter_budget(current: u32, n: usize, m: usize) -> u32 {
const DEFAULT_MAX_ITER: u32 = 200;
const PER_DIM: usize = 10;
if current != DEFAULT_MAX_ITER {
return current;
}
n.saturating_add(m)
.saturating_mul(PER_DIM)
.clamp(200, 200_000) as u32
}
fn solve_step_qp(
solver: &mut ParametricActiveSetSolver,
data: &SqpQpData,
warm: Option<&WorkingSet>,
opts: &QpOptions,
seed: Option<&IpmSeed>,
) -> Result<pounce_qp::QpSolution, pounce_qp::QpError> {
solve_subproblem_qp(solver, data, data.as_qp(), warm, opts, seed, "step QP")
}
pub(crate) fn solve_subproblem_qp(
solver: &mut ParametricActiveSetSolver,
data: &SqpQpData,
qp: pounce_qp::QpProblem<'_>,
warm: Option<&WorkingSet>,
opts: &QpOptions,
seed: Option<&IpmSeed>,
kind: &str,
) -> Result<pounce_qp::QpSolution, pounce_qp::QpError> {
let started = std::time::Instant::now();
let guards_before = solver.rank_guard_calls();
let split_before = solver.linsol_time_split();
let seeded = match (warm, seed) {
(None, Some(seed)) => ipm_seeded_step_qp(solver, &qp, data.hess_inf, opts, seed),
_ => None,
};
let budget = seed
.and_then(|s| s.last_iters.get())
.map(|k| (k * WARM_ITERS_PER_SEED_ITER) as u32)
.filter(|&b| b < opts.max_iter);
let budget_opts = budget.map(|b| QpOptions {
max_iter: b,
..opts.clone()
});
let sol = match (seeded, warm) {
(Some(sol), _) => Ok(sol),
(None, Some(w)) if seed.is_some() => {
let seed = seed.expect("guarded");
let lifted = data.lift_working_set(w);
let warm_opts = budget_opts.as_ref().unwrap_or(opts);
match solver.solve_with_working_set_if_feasible(&qp, &lifted, warm_opts) {
Ok(Some(sol)) if budget.is_some() && sol.status == QpStatus::MaxIter => {
tracing::debug!(target: "pounce::sqp::qp",
"warm start over its budget of {} iterations (n={}, m={}, {} working-set \
changes in {:.3?}); seeding from the interior point",
warm_opts.max_iter, qp.n, qp.m, sol.stats.n_working_set_changes,
started.elapsed());
match ipm_seeded_step_qp(solver, &qp, data.hess_inf, opts, seed) {
Some(seeded) => Ok(seeded),
None => solver.solve_with_working_set(&qp, &sol.working, opts),
}
}
Ok(Some(sol)) => Ok(sol),
Ok(None) => {
tracing::debug!(target: "pounce::sqp::qp",
"warm start pins infeasible (n={}, m={}); seeding from the interior point",
qp.n, qp.m);
match ipm_seeded_step_qp(solver, &qp, data.hess_inf, opts, seed) {
Some(sol) => Ok(sol),
None => solver.solve_with_working_set(&qp, &lifted, opts),
}
}
Err(e) => Err(e),
}
}
(None, Some(w)) => solver.solve_with_working_set(&qp, &data.lift_working_set(w), opts),
(None, None) => solver.solve(&qp, None, opts),
};
let elapsed = started.elapsed();
let rank_guard_calls = solver.rank_guard_calls() - guards_before;
let t = solver.linsol_time_split().since(&split_before);
let other = elapsed.saturating_sub(t.factor + t.resolve + t.rank_test);
match &sol {
Ok(s) => tracing::debug!(target: "pounce::sqp::qp",
"{kind} (n={}, m={}, warm={}): {} in {:.3?}, {} working-set changes, \
{} refactors, {} Schur updates, {} rank tests, phase1={}; \
time: {} factorizations {:.3?}, {} back-solves {:.3?}, rank tests {:.3?}, other {:.3?}",
qp.n, qp.m, warm.is_some(), s.status, elapsed,
s.stats.n_working_set_changes, s.stats.n_refactor, s.stats.n_schur_updates,
rank_guard_calls, s.stats.used_phase1,
t.n_factor, t.factor, t.n_resolve, t.resolve, t.rank_test, other),
Err(e) => tracing::debug!(target: "pounce::sqp::qp",
"{kind} (n={}, m={}, warm={}): error {e:?} after {:.3?}, {} rank tests; \
time: {} factorizations {:.3?}, {} back-solves {:.3?}, rank tests {:.3?}, other {:.3?}",
qp.n, qp.m, warm.is_some(), elapsed, rank_guard_calls,
t.n_factor, t.factor, t.n_resolve, t.resolve, t.rank_test, other),
}
Ok(data.unlift_solution(sol?))
}
fn ipm_seeded_step_qp(
solver: &mut ParametricActiveSetSolver,
qp: &pounce_qp::QpProblem,
hess_inf: Number,
opts: &QpOptions,
seed: &IpmSeed,
) -> Option<pounce_qp::QpSolution> {
use pounce_convex::Triplet as T;
let started = std::time::Instant::now();
let (n, m) = (qp.n, qp.m);
let feral = || {
Box::new(pounce_feral::FeralSolverInterface::new())
as Box<dyn pounce_linsol::SparseSymLinearSolverInterface>
};
let p_lower: Vec<T> = (0..qp.h.irows().len())
.map(|k| {
let (i, j) = (qp.h.irows()[k] as usize - 1, qp.h.jcols()[k] as usize - 1);
T::new(i.max(j), i.min(j), qp.h.values()[k])
})
.collect();
if qp.hessian_inertia != HessianInertia::Psd {
let tol = 1e-9 * (1.0 + hess_inf);
if pounce_convex::certify_psd_lower_triangle(n, &p_lower, tol, feral) != Ok(true) {
return None;
}
}
let mut rows: Vec<Vec<(usize, Number)>> = vec![Vec::new(); m];
for k in 0..qp.a.irows().len() {
let (i, j) = (qp.a.irows()[k] as usize - 1, qp.a.jcols()[k] as usize - 1);
rows[i].push((j, qp.a.values()[k]));
}
let finite_lo = |v: Number| v > NLP_LOWER_BOUND_INF;
let finite_hi = |v: Number| v < NLP_UPPER_BOUND_INF;
let (mut a, mut b, mut g, mut h) = (Vec::new(), Vec::new(), Vec::new(), Vec::new());
for (i, row) in rows.iter().enumerate() {
if qp.bl[i] == qp.bu[i] {
a.extend(row.iter().map(|&(j, v)| T::new(b.len(), j, v)));
b.push(qp.bl[i]);
continue;
}
if finite_hi(qp.bu[i]) {
g.extend(row.iter().map(|&(j, v)| T::new(h.len(), j, v)));
h.push(qp.bu[i]);
}
if finite_lo(qp.bl[i]) {
g.extend(row.iter().map(|&(j, v)| T::new(h.len(), j, -v)));
h.push(-qp.bl[i]);
}
}
let prob = pounce_convex::QpProblem {
n,
p_lower,
c: qp.g.to_vec(),
a,
b,
g,
h,
lb: qp
.xl
.iter()
.map(|&v| {
if finite_lo(v) {
v
} else {
pounce_convex::NEG_INF
}
})
.collect(),
ub: qp
.xu
.iter()
.map(|&v| {
if finite_hi(v) {
v
} else {
pounce_convex::POS_INF
}
})
.collect(),
};
let ipm_opts = pounce_convex::QpOptions {
time_limit: opts.time_limit,
max_iter: IPM_SEED_MAX_ITER,
..pounce_convex::QpOptions::default()
};
let ipm = pounce_convex::solve_qp_ipm(&prob, &ipm_opts, feral);
let ipm_time = started.elapsed();
if ipm.status == pounce_convex::QpStatus::Optimal {
seed.last_iters.set(Some(ipm.iters));
}
if ipm.status != pounce_convex::QpStatus::Optimal {
tracing::debug!(target: "pounce::sqp::qp",
"IPM cold start (n={n}, m={m}): {:?} after {} iterations in {ipm_time:.3?}; \
solving from cold instead", ipm.status, ipm.iters);
return None;
}
let near = |v: Number, bound: Number| (v - bound).abs() <= IPM_ACTIVE_TOL * (1.0 + bound.abs());
let ax = mat_vec_gen(qp.a, &ipm.x, m);
let mut working = WorkingSet::cold(n, m);
for i in 0..m {
working.constraints[i] = if qp.bl[i] == qp.bu[i] {
ConsStatus::Equality
} else if finite_lo(qp.bl[i]) && near(ax[i], qp.bl[i]) {
ConsStatus::AtLower
} else if finite_hi(qp.bu[i]) && near(ax[i], qp.bu[i]) {
ConsStatus::AtUpper
} else {
ConsStatus::Inactive
};
}
for j in 0..n {
working.bounds[j] = if qp.xl[j] == qp.xu[j] {
BoundStatus::Fixed
} else if finite_lo(qp.xl[j]) && near(ipm.x[j], qp.xl[j]) {
BoundStatus::AtLower
} else if finite_hi(qp.xu[j]) && near(ipm.x[j], qp.xu[j]) {
BoundStatus::AtUpper
} else {
BoundStatus::Inactive
};
}
let warm_opts = QpOptions {
time_limit: opts.time_limit.map(|t| t.saturating_sub(started.elapsed())),
..opts.clone()
};
let sol = solver
.solve_with_working_set(qp, &working, &warm_opts)
.ok()?;
tracing::debug!(target: "pounce::sqp::qp",
"IPM cold start (n={n}, m={m}): optimal after {} iterations in {ipm_time:.3?}, \
{} rows and bounds active; warm active-set solve {} in {:.3?} with {} \
working-set changes", ipm.iters, working.active_count(), sol.status,
sol.stats.time, sol.stats.n_working_set_changes);
(sol.status == QpStatus::Optimal).then_some(sol)
}
const IPM_SEED_MAX_ITER: usize = 50;
const WARM_ITERS_PER_SEED_ITER: usize = 4;
const IPM_ACTIVE_TOL: Number = 1e-6;
fn qp_time_limit_now(
deadline: Option<&pounce_common::timing::Deadline>,
own: Option<std::time::Duration>,
) -> Option<std::time::Duration> {
match (deadline.map(deadline_remaining), own) {
(Some((left, _)), Some(own)) => Some(left.min(own)),
(Some((left, _)), None) => Some(left),
(None, own) => own,
}
}
pub(crate) fn deadline_remaining(
deadline: &pounce_common::timing::Deadline,
) -> (std::time::Duration, pounce_common::timing::DeadlineKind) {
use pounce_common::timing::DeadlineKind;
let wall = deadline.remaining_wall();
let cpu = deadline.remaining_cpu();
let (left, kind) = if cpu < wall {
(cpu, DeadlineKind::Cpu)
} else {
(wall, DeadlineKind::Wall)
};
const UNLIMITED_S: Number = 3.2e9;
let left = if left.is_finite() {
left.clamp(0.0, UNLIMITED_S)
} else {
UNLIMITED_S
};
(std::time::Duration::from_secs_f64(left), kind)
}
pub(crate) 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;
let mut nonfinite = false;
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
};
nonfinite |= !c_vals[i].is_finite();
viol = viol.max(lo).max(hi);
}
for i in 0..n {
nonfinite |= !iter.x[i].is_finite();
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 = crate::sqp::line_search::inf_norm(&stat);
if nonfinite {
return KktError {
stationarity: Number::NAN,
constr_viol: Number::NAN,
};
}
KktError {
stationarity: stat_max,
constr_viol: viol,
}
}
#[cfg(test)]
mod kkt_nan_tests {
use super::*;
use crate::sqp::qp_assembly::Triplet;
use pounce_common::types::Index;
fn setup(x: Vec<Number>, c: Vec<Number>, lam_g: Vec<Number>) -> KktError {
let n = x.len();
let m = c.len();
let iter = SqpIterates {
x,
lambda_g: lam_g,
lambda_x: vec![0.0; n],
working: None,
};
let jac = Triplet {
n_rows: m,
n_cols: n,
irow: (1..=m as i32).map(|i| i as Index).collect(),
jcol: vec![1 as Index; m],
vals: vec![1.0; m],
};
check_kkt(
n,
m,
&iter,
&vec![1.0; n],
&c,
&vec![0.0; m],
&vec![0.0; m],
&vec![NLP_LOWER_BOUND_INF; n],
&vec![NLP_UPPER_BOUND_INF; n],
&jac,
)
}
#[test]
fn a_finite_iterate_is_measured_exactly_as_before() {
let k = setup(vec![2.0], vec![2.0], vec![-1.0]);
assert_eq!(k.constr_viol, 2.0);
assert_eq!(k.stationarity, 0.0);
}
#[test]
fn a_nan_constraint_value_is_not_perfect_feasibility() {
let k = setup(vec![0.0], vec![Number::NAN], vec![-1.0]);
assert!(
k.constr_viol.is_nan(),
"a NaN constraint value reported constr_viol = {}, which clears \
every tolerance the caller compares it against",
k.constr_viol
);
assert!(k.stationarity.is_nan());
}
#[test]
fn a_nan_iterate_is_not_perfect_feasibility() {
let k = setup(vec![Number::NAN], vec![0.0], vec![-1.0]);
assert!(k.constr_viol.is_nan());
assert!(k.stationarity.is_nan());
}
#[test]
fn a_nan_dual_poisons_only_stationarity() {
let k = setup(vec![0.0], vec![0.0], vec![Number::NAN]);
assert!(
k.stationarity.is_nan(),
"a NaN multiplier reduced to stationarity = {}",
k.stationarity
);
assert_eq!(k.constr_viol, 0.0);
}
}
#[cfg(test)]
mod inf_norm_tests {
use crate::sqp::line_search::inf_norm;
#[test]
fn a_nan_entry_propagates_rather_than_reducing_away() {
assert!(inf_norm(&[1.0, f64::NAN, 2.0]).is_nan());
assert!(inf_norm(&[f64::NAN]).is_nan());
}
#[test]
fn finite_vectors_are_unchanged() {
assert_eq!(inf_norm(&[]), 0.0);
assert_eq!(inf_norm(&[-3.0, 1.0, 2.0]), 3.0);
assert_eq!(inf_norm(&[f64::INFINITY, 1.0]), f64::INFINITY);
}
}