Files
gridcraft/crates/engine/src/cmd/solver/nlp.rs
T

198 lines
7.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<f64>) {
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<Sense> = 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<f64> = 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<Vec<f64>> {
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<f64> = 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<f64> = 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<f64> = 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)
}