use crate::sqp::options::SqpOptions;
use crate::sqp::problem::SqpProblemSpec;
use pounce_common::types::{NLP_LOWER_BOUND_INF, NLP_UPPER_BOUND_INF, Number};
pub fn l1_violation(
x: &[Number],
c_vals: &[Number],
bl: &[Number],
bu: &[Number],
xl: &[Number],
xu: &[Number],
) -> Number {
let mut v = 0.0_f64;
for (i, &ci) in c_vals.iter().enumerate() {
if bl[i] > NLP_LOWER_BOUND_INF {
v += (bl[i] - ci).max(0.0);
}
if bu[i] < NLP_UPPER_BOUND_INF {
v += (ci - bu[i]).max(0.0);
}
}
for (j, &xj) in x.iter().enumerate() {
if xl[j] > NLP_LOWER_BOUND_INF {
v += (xl[j] - xj).max(0.0);
}
if xu[j] < NLP_UPPER_BOUND_INF {
v += (xj - xu[j]).max(0.0);
}
}
v
}
pub struct LineSearchResult {
pub alpha: Number,
pub nu: Number,
pub x_new: Vec<Number>,
pub f_new: Number,
pub c_new: Vec<Number>,
pub success: bool,
pub soc_duals: Option<(Vec<Number>, Vec<Number>)>,
}
pub struct SocStep {
pub p: Vec<Number>,
pub lambda_g: Vec<Number>,
pub lambda_x: Vec<Number>,
}
pub type SocProvider<'a> = &'a mut (dyn FnMut(&[Number]) -> Option<SocStep> + 'a);
pub(crate) const KAPPA_SOC: Number = 0.99;
pub(crate) const SOC_MAX_STEP_GROWTH: Number = 2.0;
pub(crate) fn inf_norm(v: &[Number]) -> Number {
let mut m = 0.0_f64;
for &x in v {
if x.is_nan() {
return Number::NAN;
}
m = m.max(x.abs());
}
m
}
#[allow(clippy::too_many_arguments)]
pub fn l1_merit_line_search<N: SqpProblemSpec>(
nlp: &mut N,
x: &[Number],
p: &[Number],
qp_lambda_g: &[Number],
grad_f: &[Number],
f_curr: Number,
c_curr: &[Number],
bl: &[Number],
bu: &[Number],
xl: &[Number],
xu: &[Number],
current_nu: Number,
opts: &SqpOptions,
mut soc: Option<SocProvider<'_>>,
) -> LineSearchResult {
let lambda_inf = qp_lambda_g.iter().map(|l| l.abs()).fold(0.0_f64, f64::max);
let nu = current_nu
.max(lambda_inf + opts.l1_penalty_safety)
.min(opts.l1_penalty_max);
let viol_curr = l1_violation(x, c_curr, bl, bu, xl, xu);
let phi_curr = f_curr + nu * viol_curr;
let grad_p: Number = grad_f.iter().zip(p.iter()).map(|(g, pi)| g * pi).sum();
let predicted = grad_p - nu * viol_curr;
let eta = 1e-4_f64;
let mut alpha = 1.0_f64;
let mut x_trial = vec![0.0; x.len()];
let mut last_f = f_curr;
let mut last_c = c_curr.to_vec();
let mut first_trial = true;
while alpha > opts.bt_min_alpha {
for (xt, (&xi, &pi)) in x_trial.iter_mut().zip(x.iter().zip(p.iter())) {
*xt = xi + alpha * pi;
}
let f_trial = nlp.eval_f(&x_trial);
let c_trial = nlp.eval_c(&x_trial);
let viol_trial = l1_violation(&x_trial, &c_trial, bl, bu, xl, xu);
let phi_trial = f_trial + nu * viol_trial;
last_f = f_trial;
last_c.clone_from(&c_trial);
let target = phi_curr + eta * alpha * predicted;
let armijo_ok = if predicted < 0.0 {
phi_trial <= target
} else {
phi_trial < phi_curr
};
#[cfg(test)]
if opts.print_level >= 2 {
tracing::debug!(target: "pounce::sqp",
" ls trial α={alpha:.3e} phi_t={phi_trial:.4e} \
phi_c={phi_curr:.4e} target={target:.4e} pred={predicted:.3e} \
grad_p={grad_p:.3e} viol_c={viol_curr:.3e} viol_t={viol_trial:.3e} \
ok={armijo_ok}"
);
}
if armijo_ok {
return LineSearchResult {
alpha,
nu,
x_new: x_trial,
f_new: f_trial,
c_new: c_trial,
success: true,
soc_duals: None,
};
}
if first_trial {
first_trial = false;
if viol_trial > viol_curr {
if let Some(soc_fn) = soc.as_deref_mut() {
if let Some(step) = soc_fn(&c_trial) {
if inf_norm(&step.p) <= SOC_MAX_STEP_GROWTH * inf_norm(p) {
let mut x_soc = vec![0.0; x.len()];
for (xt, (&xi, &pi)) in
x_soc.iter_mut().zip(x.iter().zip(step.p.iter()))
{
*xt = xi + pi;
}
let f_soc = nlp.eval_f(&x_soc);
let c_soc = nlp.eval_c(&x_soc);
let viol_soc = l1_violation(&x_soc, &c_soc, bl, bu, xl, xu);
let phi_soc = f_soc + nu * viol_soc;
let armijo_soc = if predicted < 0.0 {
phi_soc <= phi_curr + eta * predicted
} else {
phi_soc < phi_curr
};
let soc_ok = armijo_soc && viol_soc <= KAPPA_SOC * viol_trial;
if soc_ok {
return LineSearchResult {
alpha: 1.0,
nu,
x_new: x_soc,
f_new: f_soc,
c_new: c_soc,
success: true,
soc_duals: Some((step.lambda_g, step.lambda_x)),
};
}
}
}
}
}
}
alpha *= opts.bt_reduction;
}
LineSearchResult {
alpha,
nu,
x_new: x_trial,
f_new: last_f,
c_new: last_c,
success: false,
soc_duals: None,
}
}