mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-09 12:00:01 -06:00
198 lines
7.6 KiB
Rust
198 lines
7.6 KiB
Rust
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)
|
||
}
|