use crate::core::math::Scalar;
use crate::solver::powell::QuadraticModel;
use super::geometry::{geostep, setdrop_tr, update_rescon};
use super::getact::ActiveSetQr;
use super::init::LinearSetup;
use super::trstep::trstep;
pub(crate) enum Transition {
Continue,
RhoReduced,
Converged,
}
pub(crate) struct StepOutcome {
pub(crate) transition: Transition,
}
pub(crate) struct LincoaWork<F = f64> {
pub(crate) model: QuadraticModel<F>,
amat: Vec<F>,
bvec: Vec<F>,
rescon: Vec<F>,
qr: ActiveSetQr<F>,
rho_end: F,
rho: F,
delta: F,
dnorm_rec: [F; 4],
qalt_better: [bool; 3],
n: usize,
m: usize,
}
fn trrad<F: Scalar>(delta: F, dnorm: F, ratio: F) -> F {
let g1 = F::from_f64(0.5).expect("0.5 representable");
let g2 = F::from_f64(2.0).expect("2.0 representable");
let eta1 = F::from_f64(0.1).expect("0.1 representable");
let eta2 = F::from_f64(0.7).expect("0.7 representable");
if ratio <= eta1 {
(g1 * delta).min(dnorm)
} else if ratio <= eta2 {
(g1 * delta).max(dnorm)
} else {
(g1 * delta).max(g2 * dnorm)
}
}
fn redrho<F: Scalar>(rho: F, rho_end: F) -> F {
let ratio = rho / rho_end;
if ratio > F::from_f64(250.0).expect("250.0 representable") {
F::from_f64(0.1).expect("0.1 representable") * rho
} else if ratio <= F::from_f64(16.0).expect("16.0 representable") {
rho_end
} else {
ratio.sqrt() * rho_end
}
}
fn redrat<F: Scalar>(ared: F, pred: F) -> F {
let eta1 = F::from_f64(0.1).expect("0.1 representable");
let half = F::from_f64(0.5).expect("0.5 representable");
if ared.is_nan() {
F::neg_infinity()
} else if pred.is_nan() || pred <= F::zero() {
if ared > F::zero() {
half * eta1
} else {
F::neg_infinity()
}
} else {
ared / pred
}
}
fn norm<F: Scalar>(v: &[F]) -> F {
v.iter().fold(F::zero(), |a, x| a + *x * *x).sqrt()
}
fn col_dot<F: Scalar>(v: &[F], amat: &[F], j: usize, n: usize) -> F {
(0..n).fold(F::zero(), |acc, r| acc + v[r] * amat[r + j * n])
}
impl<F: Scalar> LincoaWork<F> {
pub(crate) fn try_init<E>(
x0: Vec<F>,
amat: Vec<F>,
bvec_abs: Vec<F>,
rho_beg: F,
rho_end: F,
npt: usize,
eval: &mut impl FnMut(&[F]) -> Result<F, E>,
) -> Result<(Self, Vec<F>, F), E> {
assert!(
rho_beg > rho_end && rho_end > F::zero(),
"LINCOA needs rho_beg > rho_end > 0"
);
let n = x0.len();
let m = bvec_abs.len();
let LinearSetup { model, amat, bvec } =
QuadraticModel::try_initialize_linear(x0, amat, bvec_abs, rho_beg, npt, eval)?;
let zero = F::zero();
let xopt = model.xpt_row(model.kopt()).to_vec();
let mut rescon = vec![zero; m];
for j in 0..m {
let r = (bvec[j] - col_dot(&xopt, &amat, j, n)).max(zero);
rescon[j] = if r >= rho_beg { -r } else { r };
}
let best_x = model.best_point();
let best_f = model.fopt();
let big = F::from_f64(1e30).expect("1e30 representable");
let work = Self {
model,
amat,
bvec,
rescon,
qr: ActiveSetQr::new(n, m),
rho_end,
rho: rho_beg,
delta: rho_beg,
dnorm_rec: [big; 4],
qalt_better: [false; 3],
n,
m,
};
Ok((work, best_x, best_f))
}
pub(crate) fn rho(&self) -> F {
self.rho
}
pub(crate) fn best(&self) -> (Vec<F>, F) {
(self.model.best_point(), self.model.fopt())
}
fn abs_point(&self, disp: &[F]) -> Vec<F> {
let x0 = self.model.x0();
(0..self.n).map(|i| x0[i] + disp[i]).collect()
}
fn far_point(&self) -> (usize, F) {
let xopt = self.model.xpt_row(self.model.kopt());
let mut best_k = 0;
let mut best = F::zero();
for k in 0..self.model.m() {
let row = self.model.xpt_row(k);
let d2 = (0..self.n).fold(F::zero(), |a, i| {
a + (row[i] - xopt[i]) * (row[i] - xopt[i])
});
if d2 > best {
best = d2;
best_k = k;
}
}
(best_k, best)
}
fn record_moderr(&mut self, f_new: F, f_opt: F, xopt: &[F], d: &[F]) {
let n = self.n;
let xnew: Vec<F> = (0..n).map(|i| xopt[i] + d[i]).collect();
let q_change = self.model.eval_change(&xnew) - self.model.eval_change(xopt);
let moderr = f_new - f_opt - q_change;
let moderr_alt = f_new - f_opt - self.model.alt_model_change(d);
let tenth = F::from_f64(0.1).expect("0.1 representable");
let flag = moderr_alt.abs() < tenth * moderr.abs();
self.qalt_better = [self.qalt_better[1], self.qalt_better[2], flag];
}
fn try_qalt(&mut self) {
if self.qalt_better.iter().all(|&b| b) {
self.model.adopt_alt_model();
self.qalt_better = [false; 3];
}
}
fn maybe_shift_origin(&mut self) {
let xopt = self.model.xpt_row(self.model.kopt()).to_vec();
let xopt_sq = xopt.iter().fold(F::zero(), |a, v| a + *v * *v);
let thr = F::from_f64(1e3).expect("1e3 representable") * self.delta * self.delta;
if xopt_sq >= thr {
for j in 0..self.m {
self.bvec[j] = self.bvec[j] - col_dot(&xopt, &self.amat, j, self.n);
}
self.model.shift_origin();
}
}
pub(crate) fn step<E>(
&mut self,
eval: &mut impl FnMut(&[F]) -> Result<F, E>,
) -> Result<StepOutcome, E> {
let n = self.n;
let zero = F::zero();
let half = F::from_f64(0.5).expect("0.5 representable");
let tenth = F::from_f64(0.1).expect("0.1 representable");
let pt2 = F::from_f64(0.2).expect("0.2 representable");
let pt1999 = F::from_f64(0.1999).expect("0.1999 representable");
let gamma3 = F::from_f64(1.5).expect("1.5 representable");
let eta1 = F::from_f64(0.1).expect("0.1 representable");
let big = F::from_f64(1e30).expect("1e30 representable");
let (trs, ngetact) = trstep(
&self.model,
self.delta,
&self.amat,
&self.rescon,
&mut self.qr,
);
let d = trs.d;
let qred = trs.predicted_reduction;
let dnorm = self.delta.min(norm(&d));
let shortd = (dnorm < half * self.delta && ngetact < 2) || dnorm < pt1999 * self.delta;
self.dnorm_rec = [
self.dnorm_rec[1],
self.dnorm_rec[2],
self.dnorm_rec[3],
dnorm,
];
if self.delta > self.rho || !shortd {
self.dnorm_rec = [big; 4];
}
let qred_thr = F::from_f64(1e-5).expect("1e-5 representable") * self.rho * self.rho;
let trfail = qred <= qred_thr || qred.is_nan();
let mut ratio = -F::one();
let mut knew_tr: Option<usize> = None;
if shortd || trfail {
self.delta = half * self.delta;
if self.delta <= gamma3 * self.rho {
self.delta = self.rho;
}
} else {
let f_opt = self.model.fopt();
let kopt = self.model.kopt();
let xopt = self.model.xpt_row(kopt).to_vec();
let xnew_disp: Vec<F> = (0..n).map(|i| xopt[i] + d[i]).collect();
let xabs = self.abs_point(&xnew_disp);
let f_new = eval(&xabs)?;
self.record_moderr(f_new, f_opt, &xopt, &d);
ratio = redrat(f_opt - f_new, qred);
self.delta = trrad(self.delta, dnorm, ratio);
if self.delta <= gamma3 * self.rho {
self.delta = self.rho;
}
let ximproved = f_new < f_opt;
knew_tr = setdrop_tr(&self.model, ximproved, &d, self.delta, self.rho);
if let Some(knew) = knew_tr {
let old_kopt = self.model.kopt();
let ctx = self.model.prepare_update(&xnew_disp);
let sc = self.model.update_params(knew, &ctx);
if sc.sigma != zero {
self.model.commit_update(knew, &ctx, &sc, f_new);
self.model.kopt = if ximproved { knew } else { old_kopt };
self.try_qalt();
let new_xopt = self.model.xpt_row(self.model.kopt()).to_vec();
update_rescon(
ximproved,
&self.amat,
&self.bvec,
self.delta,
norm(&d),
&new_xopt,
&mut self.rescon,
n,
);
}
}
}
let accurate_mod = self.dnorm_rec.iter().all(|&x| x <= half * self.rho)
|| self.dnorm_rec[2..].iter().all(|&x| x <= pt2 * self.rho);
let (far_k, far_d2) = self.far_point();
let close_itpset = {
let four = F::from_f64(4.0).expect("4.0 representable");
far_d2 <= four * self.delta * self.delta
};
let adequate_geo = (shortd && accurate_mod) || close_itpset;
let small_trrad = self.delta.max(dnorm) <= self.rho;
let bad_geo = shortd || trfail || ratio <= eta1 || knew_tr.is_none();
let improve_geo = bad_geo && !adequate_geo;
let bad_rho = shortd || trfail || ratio <= zero || knew_tr.is_none();
let reduce_rho = bad_rho && adequate_geo && small_trrad;
if improve_geo {
let knew_geo = far_k;
let delbar = (tenth * self.delta).max(self.rho);
let (dgeo, feasible) = geostep(
&self.model,
knew_geo,
delbar,
&self.amat,
&self.rescon,
&self.qr,
);
let f_opt = self.model.fopt();
let kopt = self.model.kopt();
let xopt = self.model.xpt_row(kopt).to_vec();
let xnew_disp: Vec<F> = (0..n).map(|i| xopt[i] + dgeo[i]).collect();
let xabs = self.abs_point(&xnew_disp);
let f_new = eval(&xabs)?;
self.record_moderr(f_new, f_opt, &xopt, &dgeo);
let ximproved = f_new < f_opt && feasible;
let old_kopt = self.model.kopt();
let ctx = self.model.prepare_update(&xnew_disp);
let sc = self.model.update_params(knew_geo, &ctx);
if sc.sigma != zero {
self.model.commit_update(knew_geo, &ctx, &sc, f_new);
self.model.kopt = if ximproved { knew_geo } else { old_kopt };
self.try_qalt();
let new_xopt = self.model.xpt_row(self.model.kopt()).to_vec();
update_rescon(
ximproved,
&self.amat,
&self.bvec,
self.delta,
norm(&dgeo),
&new_xopt,
&mut self.rescon,
n,
);
}
}
let mut transition = Transition::Continue;
if reduce_rho {
if self.rho <= self.rho_end {
if shortd {
let xopt = self.model.xpt_row(self.model.kopt()).to_vec();
let disp: Vec<F> = (0..n).map(|i| xopt[i] + d[i]).collect();
let xabs = self.abs_point(&disp);
eval(&xabs)?;
}
return Ok(StepOutcome {
transition: Transition::Converged,
});
}
let rho_new = redrho(self.rho, self.rho_end);
self.delta = (half * self.rho).max(rho_new);
self.rho = rho_new;
self.dnorm_rec = [big; 4];
transition = Transition::RhoReduced;
}
self.maybe_shift_origin();
Ok(StepOutcome { transition })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::solver::lincoa::init::fold_constraints;
#[allow(clippy::too_many_arguments)]
fn run(
x0: Vec<f64>,
amat: Vec<f64>,
bvec: Vec<f64>,
rho_beg: f64,
rho_end: f64,
npt: usize,
max_evals: usize,
f: impl Fn(&[f64]) -> f64,
) -> (Vec<f64>, f64, usize) {
let evals = core::cell::Cell::new(0usize);
let mut eval = |x: &[f64]| -> Result<f64, core::convert::Infallible> {
evals.set(evals.get() + 1);
Ok(f(x))
};
let (mut work, _x0_best, _f0_best) =
LincoaWork::try_init(x0, amat, bvec, rho_beg, rho_end, npt, &mut eval).unwrap();
for _ in 0..max_evals {
let out = work.step(&mut eval).unwrap();
if matches!(out.transition, Transition::Converged) || evals.get() >= max_evals {
break;
}
}
let (mx, mf) = work.best();
(mx, mf, evals.get())
}
#[test]
fn unconstrained_sphere_converges() {
let f = |x: &[f64]| (x[0] - 1.0).powi(2) + (x[1] + 2.0).powi(2);
let (x, fx, _evals) = run(vec![0.0, 0.0], vec![], vec![], 0.5, 1e-6, 5, 200, f);
assert!((x[0] - 1.0).abs() < 1e-4, "x0 = {}", x[0]);
assert!((x[1] + 2.0).abs() < 1e-4, "x1 = {}", x[1]);
assert!(fx < 1e-8, "f = {fx}");
}
#[test]
fn linearly_constrained_quadratic_hits_projection() {
let c = [2.0, 2.0];
let f = move |x: &[f64]| (x[0] - c[0]).powi(2) + (x[1] - c[1]).powi(2);
let x0 = vec![0.0, 0.0];
let (amat, bvec) =
fold_constraints::<f64>(2, &x0, None, None, &[], &[(vec![1.0, 1.0], 2.0)]);
let (x, fx, _evals) = run(x0, amat, bvec, 0.5, 1e-7, 5, 300, f);
assert!((x[0] - 1.0).abs() < 1e-3, "x0 = {}", x[0]);
assert!((x[1] - 1.0).abs() < 1e-3, "x1 = {}", x[1]);
assert!((fx - 2.0).abs() < 1e-3, "f = {fx}");
assert!(x[0] + x[1] <= 2.0 + 1e-6, "infeasible: {} + {}", x[0], x[1]);
}
#[test]
fn box_constrained_quadratic_hits_corner() {
let c = [5.0, 5.0];
let f = move |x: &[f64]| (x[0] - c[0]).powi(2) + (x[1] - c[1]).powi(2);
let x0 = vec![0.0, 0.0];
let (amat, bvec) =
fold_constraints::<f64>(2, &x0, Some(&[-1.0, -1.0]), Some(&[1.0, 1.0]), &[], &[]);
let (x, _fx, _evals) = run(x0, amat, bvec, 0.3, 1e-7, 5, 300, f);
assert!((x[0] - 1.0).abs() < 1e-3, "x0 = {}", x[0]);
assert!((x[1] - 1.0).abs() < 1e-3, "x1 = {}", x[1]);
}
}