use super::PRECISION; use super::lp::{Outcome, Row, Sense}; use super::model::Goal; use super::problem::{Evaluator, Point}; // ---------------------------------------------------------------- nonlinear /// Augmented-Lagrangian value of a point: objective `phi` plus the constraint terms. fn aug(phi: f64, cons: &[f64], senses: &[Sense], lam: &[f64], mu: f64) -> f64 { let mut v = phi; for ((c, s), l) in cons.iter().zip(senses).zip(lam) { v += match s { Sense::Eq => l * c + 0.5 * mu * c * c, _ => { let t = (l + mu * c).max(0.0); (t * t - l * l) / (2.0 * mu) } }; } v } /// Constraint values in the `≤ 0` / `= 0` form, and the objective to minimise. pub(super) fn shape(p: &Point, rows: &[Row], goal: Goal) -> (f64, Vec) { let phi = match goal { Goal::Max => -p.f, Goal::Min => p.f, Goal::Value(v) => (p.f - v) * (p.f - v), }; let cons = p.g.iter().zip(rows).map(|(g, r)| if r.sense == Sense::Ge { -g } else { *g }).collect(); (phi, cons) } pub(super) fn violation(cons: &[f64], senses: &[Sense]) -> f64 { cons.iter().zip(senses).map(|(c, s)| if *s == Sense::Eq { c.abs() } else { c.max(0.0) }).fold(0.0, f64::max) } pub(super) fn project(x: &mut [f64], lo: &[f64], hi: &[f64]) { for ((v, l), h) in x.iter_mut().zip(lo).zip(hi) { if *v < *l { *v = *l; } if *v > *h { *v = *h; } } } /// Local minimisation of the model within bounds `lo..hi` from `x0`. pub(super) fn nlp(ev: &mut Evaluator, x0: &[f64], lo: &[f64], hi: &[f64], goal: Goal) -> Outcome { let n = x0.len(); let rows = ev.rows.clone(); let senses: Vec = rows.iter().map(|r| r.sense).collect(); let mut x = x0.to_vec(); project(&mut x, lo, hi); let mut lam = vec![0.0; rows.len()]; let mut mu = 10.0; let Some(p) = ev.eval(&x) else { return Outcome::Limit }; let (mut phi, mut cons) = shape(&p, &rows, goal); if !phi.is_finite() { return Outcome::Infeasible; } let phi0 = phi; let mut prev_viol = violation(&cons, &senses); let mut converged = false; let mut out_of_budget = false; let mut carried: Option = None; for _outer in 0..40 { // Inner: projected gradient on the augmented Lagrangian. let lval = |phi: f64, cons: &[f64], lam: &[f64], mu: f64| aug(phi, cons, &senses, lam, mu); let grad = |ev: &mut Evaluator, x: &[f64], lam: &[f64], mu: f64| -> Option> { let mut gr = vec![0.0; n]; for j in 0..n { let xj = x.get(j).copied().unwrap_or(0.0); let h = 1e-6 * xj.abs().max(1.0); let (l, u) = (lo.get(j).copied().unwrap_or(f64::NEG_INFINITY), hi.get(j).copied().unwrap_or(f64::INFINITY)); let (a, b) = (if xj - h >= l { xj - h } else { xj }, if xj + h <= u { xj + h } else { xj }); if b <= a { continue; } let mut xa = x.to_vec(); let mut xb = x.to_vec(); if let Some(v) = xa.get_mut(j) { *v = a; } if let Some(v) = xb.get_mut(j) { *v = b; } let (pa, ca) = shape(&ev.eval(&xa)?, &rows, goal); let (pb, cb) = shape(&ev.eval(&xb)?, &rows, goal); let d = (lval(pb, &cb, lam, mu) - lval(pa, &ca, lam, mu)) / (b - a); if let Some(v) = gr.get_mut(j) { *v = if d.is_finite() { d } else { 0.0 }; } } Some(gr) }; let mut l_cur = lval(phi, &cons, &lam, mu); let Some(mut g) = grad(ev, &x, &lam, mu) else { out_of_budget = true; break; }; // The step length carries over between outer iterations once known. let gnorm = g.iter().map(|v| v.abs()).fold(0.0, f64::max); let mut alpha = carried.unwrap_or(if gnorm > 0.0 { 1.0 / gnorm } else { 1.0 }); let mut inner_done = false; let mut quiet = 0; for _inner in 0..500 { let mut accepted = None; let mut a = alpha; for _ in 0..40 { let mut xn: Vec = x.iter().zip(&g).map(|(v, d)| v - a * d).collect(); project(&mut xn, lo, hi); let Some(pn) = ev.eval(&xn) else { break }; let (phn, cn) = shape(&pn, &rows, goal); let ln = lval(phn, &cn, &lam, mu); let decrease: f64 = g.iter().zip(x.iter().zip(&xn)).map(|(d, (u, v))| d * (u - v)).sum(); if ln.is_finite() && ln <= l_cur - 1e-4 * decrease { accepted = Some((xn, phn, cn, ln)); break; } a *= 0.25; } if ev.exhausted() { out_of_budget = true; } let Some((xn, phn, cn, ln)) = accepted else { inner_done = true; break; }; // Variables or objective running off towards infinity: unbounded. if phn.abs() > 1e15 * (1.0 + phi0.abs()) || xn.iter().zip(x0).any(|(v, s)| v.abs() > 1e15 * (1.0 + s.abs())) { return Outcome::Unbounded; } let s: Vec = xn.iter().zip(&x).map(|(a, b)| a - b).collect(); let step = s.iter().map(|v| v.abs()).fold(0.0, f64::max); let xmax = xn.iter().map(|v| v.abs()).fold(1.0, f64::max); let small = step <= 1e-10 * xmax || (l_cur - ln).abs() <= 1e-13 * (1.0 + l_cur.abs()); quiet = if small { quiet + 1 } else { 0 }; x = xn; phi = phn; cons = cn; l_cur = ln; if quiet >= 3 { inner_done = true; break; } let Some(gn) = grad(ev, &x, &lam, mu) else { out_of_budget = true; break; }; // Barzilai–Borwein step. let yv: Vec = gn.iter().zip(&g).map(|(a, b)| a - b).collect(); let sy: f64 = s.iter().zip(&yv).map(|(a, b)| a * b).sum(); let ss: f64 = s.iter().map(|v| v * v).sum(); alpha = if sy > 0.0 { (ss / sy).clamp(1e-12, 1e12) } else { (a * 4.0).min(1e12) }; g = gn; if g.iter().all(|v| *v == 0.0) { inner_done = true; break; } } carried = Some(alpha); if out_of_budget { break; } let viol = violation(&cons, &senses); let scale = rows.iter().map(|r| r.rhs_const.abs()).fold(1.0, f64::max); if viol <= PRECISION * scale && inner_done { converged = true; if rows.is_empty() { break; } } // Multiplier and penalty updates. for ((l, c), s) in lam.iter_mut().zip(&cons).zip(&senses) { *l = match s { Sense::Eq => *l + mu * c, _ => (*l + mu * c).max(0.0), }; } if viol > 0.25 * prev_viol { mu = (mu * 10.0).min(1e10); } if converged && viol <= PRECISION * scale * 1e-2 { break; } prev_viol = viol; converged = false; } let viol = violation(&cons, &senses); let scale = rows.iter().map(|r| r.rhs_const.abs()).fold(1.0, f64::max); if viol > PRECISION * scale * 10.0 { return if out_of_budget { Outcome::Limit } else { Outcome::Infeasible }; } Outcome::Solved(x, phi, (converged || viol <= PRECISION * scale) && !out_of_budget) }