use crate::core::math::Scalar;
use crate::solver::powell::{QuadraticModel, TrustRegionStep};
use super::getact::{ActiveSetQr, getact};
fn dot<F: Scalar>(a: &[F], b: &[F]) -> F {
a.iter().zip(b).fold(F::zero(), |acc, (x, y)| acc + *x * *y)
}
fn amat_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])
}
fn solve_rt<F: Scalar>(rfac: &[F], n: usize, k: usize, b: &[F]) -> Vec<F> {
let mut z = vec![F::zero(); k];
for i in 0..k {
let mut s = b[i];
for j in 0..i {
s = s - rfac[j + i * n] * z[j]; }
z[i] = s / rfac[i + i * n];
}
z
}
fn max3<F: Scalar>(a: F, b: F, c: F) -> F {
a.max(b).max(c)
}
pub(crate) fn trstep<F: Scalar>(
model: &QuadraticModel<F>,
delta: F,
amat: &[F],
rescon: &[F],
warm: &mut ActiveSetQr<F>,
) -> (TrustRegionStep<F>, usize) {
let n = model.n();
let m = rescon.len();
let (zero, one) = (F::zero(), F::one());
let two = F::from_f64(2.0).expect("2.0 representable");
let half = F::from_f64(0.5).expect("0.5 representable");
let eps = F::epsilon();
let realmin = F::min_positive_value();
let tinycv = two.powi(-200);
let ctest = F::from_f64(0.01).expect("0.01 representable");
let pt2 = F::from_f64(0.2).expect("0.2 representable");
let c1em4 = F::from_f64(1e-4).expect("1e-4 representable");
let make_step = |s: Vec<F>| -> TrustRegionStep<F> {
let hs = model.hessian_matvec(&s);
let gopt = model.gradient_at_opt();
let pred = -(dot(&gopt, &s) + half * dot(&s, &hs));
TrustRegionStep {
d: s,
crvmin: zero,
predicted_reduction: pred,
}
};
let gopt0 = model.gradient_at_opt();
let gmax = gopt0.iter().fold(zero, |acc, v| acc.max(v.abs()));
let modscal = if gmax > F::from_f64(1e12).expect("1e12 representable") {
(two * realmin).max(one / gmax)
} else {
one
};
let mut g: Vec<F> = gopt0.iter().map(|&v| v * modscal).collect();
if !g.iter().fold(zero, |a, v| a + v.abs()).is_finite() {
return (make_step(vec![zero; n]), 0);
}
let mut resnew = vec![zero; m];
for j in 0..m {
let mut r = rescon[j];
if rescon[j] >= zero {
r = tinycv.max(rescon[j]);
}
if rescon[j] >= delta {
r = -one;
}
resnew[j] = r;
}
for ic in 0..warm.nact {
resnew[warm.iact[ic]] = zero;
}
let mut resact = vec![zero; m];
for ic in 0..warm.nact {
resact[ic] = rescon[warm.iact[ic]];
}
let delsq = delta * delta;
let mut s = vec![zero; n];
let mut ss = zero;
let mut reduct = zero;
let mut newact = true;
let mut ngetact: usize = 0;
let mut itercg: isize = -1;
let mut d = vec![zero; n];
let mut gamma = zero;
let mut hd: Vec<F>;
let maxiter = (10 * (m + n)).min(10_000);
for _iter in 0..maxiter {
if newact {
let mut psd = vec![zero; n];
getact(
amat,
n,
m,
delta,
&g,
warm,
&mut resact,
&mut resnew,
&mut psd,
);
ngetact += 1;
let dd = dot(&psd, &psd);
if dd <= eps * delsq || dd.is_nan() {
break;
}
let scale_psd = (pt2 * delta) / dd.sqrt();
for p in psd.iter_mut() {
*p = *p * scale_psd;
}
gamma = zero;
let mut dproj = vec![zero; n];
if (0..warm.nact).any(|ic| resact[ic] > c1em4 * delta) {
let z = solve_rt(&warm.rfac, n, warm.nact, &resact[..warm.nact]);
for k in 0..warm.nact {
for r in 0..n {
dproj[r] = dproj[r] + warm.qfac[r + k * n] * z[k];
}
}
let spsd: Vec<F> = (0..n).map(|i| s[i] + psd[i]).collect();
let ds = dot(&dproj, &spsd);
let dd2 = dot(&dproj, &dproj);
let resid = delsq - dot(&spsd, &spsd);
if resid > zero && dd2 > eps * delsq && !ds.is_nan() {
let sqrtd = max3(
(ds * ds + dd2 * resid).sqrt(),
ds.abs(),
(dd2 * resid).sqrt(),
);
gamma = if ds <= zero {
(sqrtd - ds) / dd2
} else {
resid / (sqrtd + ds)
};
if gamma < zero || !gamma.is_finite() {
gamma = zero;
}
let mut gmin = gamma.min(one);
for j in 0..m {
if resnew[j] > zero {
let adj = amat_col_dot(&dproj, amat, j, n);
if adj > zero {
let psd_aj = amat_col_dot(&psd, amat, j, n);
let fr = (resnew[j] - psd_aj) / adj;
if fr < gmin {
gmin = fr;
}
}
}
}
gamma = gmin;
}
}
if gamma > zero {
for i in 0..n {
d[i] = psd[i] + gamma * dproj[i];
}
itercg = -1;
} else {
d.copy_from_slice(&psd);
itercg = 0;
}
}
itercg += 1;
let resid = delsq - ss;
let dg = dot(&d, &g);
let ds = dot(&d, &s);
let dd = dot(&d, &d);
if resid <= zero || dd <= eps * delsq || ds.is_nan() {
break;
}
let sqrtd = max3((ds * ds + dd * resid).sqrt(), ds.abs(), (dd * resid).sqrt());
let mut alpha = if ds <= zero {
(sqrtd - ds) / dd
} else {
resid / (sqrtd + ds)
};
if alpha <= zero || !alpha.is_finite() {
break;
}
if -alpha * dg <= ctest * reduct || (alpha * dg).is_nan() {
break;
}
hd = model.hessian_matvec(&d);
for h in hd.iter_mut() {
*h = *h * modscal;
}
let dhd = dot(&d, &hd);
let alpht = alpha;
if dg + alpha * dhd > zero {
alpha = -dg / dhd;
}
let alphm = alpha;
let mut ad = vec![-one; m];
for j in 0..m {
if resnew[j] > zero {
ad[j] = amat_col_dot(&d, amat, j, n);
}
}
let mut jsav: Option<usize> = None;
for j in 0..m {
if ad[j] > zero {
let mut fr = resnew[j] / ad[j];
if fr.is_nan() {
fr = alpha;
}
if fr < alpha {
alpha = fr;
jsav = Some(j);
}
}
}
if itercg == 0 {
alpha = one;
} else if itercg == 1 && gamma <= zero {
alpha = alpha.max(one);
} else {
alpha = alpha.max(zero);
}
alpha = alpha.min(alphm);
let sold = s.clone();
for i in 0..n {
s[i] = s[i] + alpha * d[i];
}
ss = dot(&s, &s);
if !ss.is_finite() {
s = sold;
break;
}
for i in 0..n {
g[i] = g[i] + alpha * hd[i];
}
if !g.iter().fold(zero, |a, v| a + v.abs()).is_finite() {
break;
}
for j in 0..m {
if resnew[j] > zero {
resnew[j] = tinycv.max(resnew[j] - alpha * ad[j]);
}
}
if itercg == 0 {
let factor = one - alpha * gamma;
for ic in 0..warm.nact {
resact[ic] = factor * resact[ic];
}
}
reduct = reduct - alpha * (dg + half * alpha * dhd);
if reduct <= zero || reduct.is_nan() {
s = sold;
break;
}
if alpha >= alpht || -alphm * (dg + half * alphm * dhd) <= ctest * reduct {
break;
}
newact = jsav.is_some();
if newact {
continue;
}
if itercg as usize >= n - warm.nact {
break;
}
let pg: Vec<F> = if warm.nact == 0 {
g.clone()
} else {
let mut p = vec![zero; n];
for col in warm.nact..n {
let coeff = (0..n).fold(zero, |a, r| a + g[r] * warm.qfac[r + col * n]);
for r in 0..n {
p[r] = p[r] + coeff * warm.qfac[r + col * n];
}
}
p
};
let beta = if itercg == 0 {
zero
} else {
dot(&pg, &hd) / dhd
};
for i in 0..n {
d[i] = -pg[i] + beta * d[i];
}
}
(make_step(s), ngetact)
}
#[cfg(test)]
mod tests {
use super::*;
fn diag_quadratic(h: &'static [f64], c: &'static [f64]) -> impl Fn(&[f64]) -> f64 {
move |x: &[f64]| {
0.5 * (0..x.len())
.map(|i| h[i] * (x[i] - c[i]).powi(2))
.sum::<f64>()
}
}
#[test]
fn unconstrained_reaches_interior_min() {
let h = &[2.0, 4.0];
let c = &[0.4, -0.3];
let model = QuadraticModel::initialize(vec![0.0, 0.0], 0.3, 5, &diag_quadratic(h, c));
let n = 2;
let mut warm = ActiveSetQr::<f64>::new(n, 0);
let (step, _) = trstep(&model, 5.0, &[], &[], &mut warm);
let gopt = model.gradient_at_opt();
let hs = model.hessian_matvec(&step.d);
for i in 0..n {
assert!(
(gopt[i] + hs[i]).abs() < 1e-7,
"g_final[{i}] = {}",
gopt[i] + hs[i]
);
}
assert!(step.predicted_reduction > 0.0);
}
#[test]
fn constrained_step_is_feasible_and_kkt() {
let h = &[2.0, 2.0];
let c = &[1.0, 0.0]; let model = QuadraticModel::initialize(vec![0.0, 0.0], 0.3, 5, &diag_quadratic(h, c));
let n = 2;
let m = 1;
let amat = vec![1.0, 0.0];
let xopt: Vec<f64> = (0..n)
.map(|i| model.x0()[i] + model.xpt_row(model.kopt())[i])
.collect();
let rescon = vec![0.5 - xopt[0]];
let mut warm = ActiveSetQr::<f64>::new(n, m);
let (step, _) = trstep(&model, 0.3, &amat, &rescon, &mut warm);
let a_dot_s = amat_col_dot(&step.d, &amat, 0, n);
assert!(
a_dot_s <= rescon[0] + 1e-9,
"a·s = {a_dot_s}, rescon = {}",
rescon[0]
);
assert!(step.predicted_reduction > 0.0);
assert_eq!(warm.nact, 1);
let gopt = model.gradient_at_opt();
let hs = model.hessian_matvec(&step.d);
assert!(
(gopt[1] + hs[1]).abs() < 1e-7,
"null-space grad = {}",
gopt[1] + hs[1]
);
assert!(
(a_dot_s - rescon[0]).abs() < 1e-6,
"constraint not tight: {a_dot_s}"
);
}
#[test]
fn step_respects_trust_radius() {
let h = &[1.0, 1.0];
let c = &[10.0, 10.0]; let model = QuadraticModel::initialize(vec![0.0, 0.0], 0.3, 5, &diag_quadratic(h, c));
let n = 2;
let delta = 1.0;
let mut warm = ActiveSetQr::<f64>::new(n, 0);
let (step, _) = trstep(&model, delta, &[], &[], &mut warm);
let snorm = dot(&step.d, &step.d).sqrt();
assert!(snorm <= 2.0 * delta, "‖s‖ = {snorm}");
assert!(
snorm > 0.5 * delta,
"‖s‖ = {snorm} (expected near boundary)"
);
assert!(step.predicted_reduction > 0.0);
}
}