#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NelderMeadOptions {
pub f_tolerance: f64,
pub x_tolerance: f64,
pub max_evaluations: usize,
}
impl NelderMeadOptions {
pub fn for_dimension(n: usize) -> Self {
Self {
f_tolerance: 1e-10,
x_tolerance: 1e-8,
max_evaluations: (1000 * n).max(2000),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Minimum {
pub x: Vec<f64>,
pub value: f64,
pub evaluations: usize,
pub converged: bool,
}
pub fn nelder_mead<F: FnMut(&[f64]) -> f64>(
mut f: F,
x0: &[f64],
options: NelderMeadOptions,
) -> Minimum {
let n = x0.len();
let evaluations = std::cell::Cell::new(0usize);
let mut eval = |x: &[f64]| {
evaluations.set(evaluations.get() + 1);
let v = f(x);
if v.is_finite() {
v
} else {
f64::INFINITY
}
};
if n == 0 {
let value = eval(x0);
return Minimum {
x: Vec::new(),
value,
evaluations: evaluations.get(),
converged: true,
};
}
let nf = n as f64;
let (rho, chi, psi, sigma) = if n <= 2 {
(1.0, 2.0, 0.5, 0.5)
} else {
(1.0, 1.0 + 2.0 / nf, 0.75 - 1.0 / (2.0 * nf), 1.0 - 1.0 / nf)
};
let mut simplex: Vec<Vec<f64>> = Vec::with_capacity(n + 1);
simplex.push(x0.to_vec());
for i in 0..n {
let mut v = x0.to_vec();
v[i] = if v[i] != 0.0 { v[i] * 1.05 } else { 0.00025 };
simplex.push(v);
}
let mut values: Vec<f64> = simplex.iter().map(|v| eval(v)).collect();
let mut order: Vec<usize> = (0..=n).collect();
let mut converged = false;
loop {
order.sort_by(|&a, &b| values[a].total_cmp(&values[b]));
let best = order[0];
let worst = order[n];
let f_spread = order[1..]
.iter()
.map(|&i| (values[i] - values[best]).abs())
.fold(0.0, f64::max);
let x_close = order[1..].iter().all(|&i| {
simplex[i]
.iter()
.zip(&simplex[best])
.all(|(a, b)| (a - b).abs() <= options.x_tolerance * (1.0 + b.abs()))
});
if f_spread <= options.f_tolerance * (1.0 + values[best].abs()) && x_close {
converged = true;
break;
}
if evaluations.get() >= options.max_evaluations {
break;
}
let mut centroid = vec![0.0; n];
for &i in &order[..n] {
for (c, v) in centroid.iter_mut().zip(&simplex[i]) {
*c += v / nf;
}
}
let toward = |t: f64| -> Vec<f64> {
centroid
.iter()
.zip(&simplex[worst])
.map(|(c, w)| c + t * (c - w))
.collect()
};
let xr = toward(rho);
let fr = eval(&xr);
let second_worst = values[order[n - 1]];
if fr < values[best] {
let xe = toward(rho * chi);
let fe = eval(&xe);
if fe < fr {
simplex[worst] = xe;
values[worst] = fe;
} else {
simplex[worst] = xr;
values[worst] = fr;
}
continue;
}
if fr < second_worst {
simplex[worst] = xr;
values[worst] = fr;
continue;
}
let (xc, fc, accept) = if fr < values[worst] {
let xc = toward(rho * psi);
let fc = eval(&xc);
let ok = fc <= fr;
(xc, fc, ok)
} else {
let xc = toward(-psi);
let fc = eval(&xc);
let ok = fc < values[worst];
(xc, fc, ok)
};
if accept {
simplex[worst] = xc;
values[worst] = fc;
continue;
}
let anchor = simplex[best].clone();
for &i in &order[1..] {
for (v, a) in simplex[i].iter_mut().zip(&anchor) {
*v = a + sigma * (*v - a);
}
values[i] = eval(&simplex[i]);
}
}
order.sort_by(|&a, &b| values[a].total_cmp(&values[b]));
Minimum {
x: simplex[order[0]].clone(),
value: values[order[0]],
evaluations: evaluations.get(),
converged,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_quadratic_bowl_in_five_dimensions() {
let centre = [1.0, -2.0, 3.0, 0.5, -0.25];
let f = |x: &[f64]| {
x.iter()
.zip(¢re)
.enumerate()
.map(|(i, (a, c))| (i + 1) as f64 * (a - c).powi(2))
.sum::<f64>()
};
let m = nelder_mead(f, &[0.0; 5], NelderMeadOptions::for_dimension(5));
assert!(m.converged, "{m:?}");
for (a, c) in m.x.iter().zip(¢re) {
assert!((a - c).abs() < 1e-4, "{m:?}");
}
}
#[test]
fn the_same_start_gives_the_same_point() {
let f = |x: &[f64]| (x[0] - 3.0).powi(2) + (x[0] * x[1] - 1.0).powi(2);
let a = nelder_mead(f, &[0.5, 0.5], NelderMeadOptions::for_dimension(2));
let b = nelder_mead(f, &[0.5, 0.5], NelderMeadOptions::for_dimension(2));
assert_eq!(a, b);
}
#[test]
fn a_refused_point_is_avoided() {
let f = |x: &[f64]| {
if x.iter().any(|v| !(0.0..=1.0).contains(v)) {
f64::NAN
} else {
(x[0] - 2.0).powi(2) + (x[1] - 2.0).powi(2)
}
};
let m = nelder_mead(f, &[0.5, 0.5], NelderMeadOptions::for_dimension(2));
assert!(m.value.is_finite());
assert!(m.x.iter().all(|v| (0.0..=1.0).contains(v)), "{m:?}");
assert!(m.value < 2.0 + 1e-4, "{m:?}");
}
#[test]
fn the_budget_is_kept() {
let f = |x: &[f64]| (1.0 - x[0]).powi(2) + 100.0 * (x[1] - x[0] * x[0]).powi(2);
let options = NelderMeadOptions {
max_evaluations: 30,
..NelderMeadOptions::for_dimension(2)
};
let m = nelder_mead(f, &[-1.2, 1.0], options);
assert!(!m.converged);
assert!(m.evaluations <= 30 + 2, "{}", m.evaluations);
}
#[test]
fn a_change_of_scale_moves_the_minimum_with_it() {
let f = |k: f64| {
move |x: &[f64]| 1e6 * k * (((x[0] - 2.0 * k) / k).powi(2) + ((x[1] + k) / k).powi(2))
};
for k in [1e-6, 1.0, 1e6] {
let m = nelder_mead(f(k), &[k, k], NelderMeadOptions::for_dimension(2));
assert!(m.converged, "{k}: {m:?}");
assert!(
(m.x[0] / k - 2.0).abs() < 1e-5 && (m.x[1] / k + 1.0).abs() < 1e-5,
"{k}: {m:?}"
);
}
}
}