pub(crate) struct NelderMeadConfig {
pub max_iter: usize,
pub diameter_tol: f64,
pub fvalue_tol: f64,
}
pub(crate) struct NelderMeadResult {
pub x: f64,
pub y: f64,
pub fval: f64,
}
pub(crate) fn nelder_mead_2d<F>(
objective: F,
x0: f64,
y0: f64,
step_x: f64,
step_y: f64,
config: &NelderMeadConfig,
) -> NelderMeadResult
where
F: Fn(f64, f64) -> f64,
{
let mut simplex = [(x0, y0), (x0 + step_x, y0), (x0, y0 + step_y)];
let mut f_vals = [
objective(simplex[0].0, simplex[0].1),
objective(simplex[1].0, simplex[1].1),
objective(simplex[2].0, simplex[2].1),
];
for _ in 0..config.max_iter {
let mut idx = [0usize, 1, 2];
idx.sort_by(|&a, &b| f_vals[a].total_cmp(&f_vals[b]));
simplex = [simplex[idx[0]], simplex[idx[1]], simplex[idx[2]]];
f_vals = [f_vals[idx[0]], f_vals[idx[1]], f_vals[idx[2]]];
let diameter = simplex
.iter()
.flat_map(|a| {
simplex
.iter()
.map(move |b| ((a.0 - b.0).powi(2) + (a.1 - b.1).powi(2)).sqrt())
})
.fold(0.0_f64, f64::max);
let f_spread = f_vals[2] - f_vals[0];
if diameter < config.diameter_tol || f_spread < config.fvalue_tol {
break;
}
let cx = (simplex[0].0 + simplex[1].0) / 2.0;
let cy = (simplex[0].1 + simplex[1].1) / 2.0;
let rx = cx + (cx - simplex[2].0);
let ry = cy + (cy - simplex[2].1);
let fr = objective(rx, ry);
if fr < f_vals[1] && fr >= f_vals[0] {
simplex[2] = (rx, ry);
f_vals[2] = fr;
} else if fr < f_vals[0] {
let ex = cx + 2.0 * (rx - cx);
let ey = cy + 2.0 * (ry - cy);
let fe = objective(ex, ey);
if fe < fr {
simplex[2] = (ex, ey);
f_vals[2] = fe;
} else {
simplex[2] = (rx, ry);
f_vals[2] = fr;
}
} else {
let (hx, hy) = if fr < f_vals[2] {
(cx + 0.5 * (rx - cx), cy + 0.5 * (ry - cy))
} else {
(
cx + 0.5 * (simplex[2].0 - cx),
cy + 0.5 * (simplex[2].1 - cy),
)
};
let fh = objective(hx, hy);
if fh < f_vals[2].min(fr) {
simplex[2] = (hx, hy);
f_vals[2] = fh;
} else {
for j in 1..3 {
simplex[j].0 = simplex[0].0 + 0.5 * (simplex[j].0 - simplex[0].0);
simplex[j].1 = simplex[0].1 + 0.5 * (simplex[j].1 - simplex[0].1);
f_vals[j] = objective(simplex[j].0, simplex[j].1);
}
}
}
}
let best_idx = if f_vals[0] <= f_vals[1] && f_vals[0] <= f_vals[2] {
0
} else if f_vals[1] <= f_vals[2] {
1
} else {
2
};
NelderMeadResult {
x: simplex[best_idx].0,
y: simplex[best_idx].1,
fval: f_vals[best_idx],
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_config() -> NelderMeadConfig {
NelderMeadConfig {
max_iter: 1000,
diameter_tol: 1e-12,
fvalue_tol: 1e-14,
}
}
#[test]
fn converges_on_sphere_function() {
let result = nelder_mead_2d(|x, y| x * x + y * y, 1.0, 1.0, 0.5, 0.5, &default_config());
assert!((result.x).abs() < 1e-6, "x should be ~0, got {}", result.x);
assert!((result.y).abs() < 1e-6, "y should be ~0, got {}", result.y);
assert!(
result.fval < 1e-12,
"fval should be ~0, got {}",
result.fval
);
}
#[test]
fn converges_on_rosenbrock() {
let result = nelder_mead_2d(
|x, y| (1.0 - x).powi(2) + 100.0 * (y - x * x).powi(2),
-1.0,
-1.0,
0.5,
0.5,
&NelderMeadConfig {
max_iter: 5000,
diameter_tol: 1e-12,
fvalue_tol: 1e-14,
},
);
assert!(
(result.x - 1.0).abs() < 1e-3,
"x should be ~1, got {}",
result.x
);
assert!(
(result.y - 1.0).abs() < 1e-3,
"y should be ~1, got {}",
result.y
);
}
#[test]
fn converges_on_shifted_minimum() {
let result = nelder_mead_2d(
|x, y| (x - 3.0).powi(2) + (y + 2.0).powi(2),
0.0,
0.0,
1.0,
1.0,
&default_config(),
);
assert!(
(result.x - 3.0).abs() < 1e-6,
"x should be ~3, got {}",
result.x
);
assert!(
(result.y + 2.0).abs() < 1e-6,
"y should be ~-2, got {}",
result.y
);
assert!(result.fval < 1e-12);
}
#[test]
fn respects_max_iter_limit() {
let result = nelder_mead_2d(
|x, y| (1.0 - x).powi(2) + 100.0 * (y - x * x).powi(2),
-1.0,
-1.0,
0.5,
0.5,
&NelderMeadConfig {
max_iter: 5,
diameter_tol: 1e-20,
fvalue_tol: 1e-20,
},
);
assert!(
(result.x - 1.0).abs() > 0.1 || (result.y - 1.0).abs() > 0.1,
"should not converge in 5 iterations"
);
}
#[test]
fn diameter_convergence_terminates() {
let result = nelder_mead_2d(
|x, y| x * x + y * y,
5.0,
5.0,
2.0,
2.0,
&NelderMeadConfig {
max_iter: 100000,
diameter_tol: 100.0, fvalue_tol: 1e-20,
},
);
assert!(result.fval.is_finite(), "should return finite fval");
}
#[test]
fn handles_small_step_gracefully() {
let result = nelder_mead_2d(
|x, y| x * x + y * y,
1.0,
1.0,
0.001,
0.001,
&default_config(),
);
assert!(result.fval < 0.5, "should improve despite small steps");
}
#[test]
fn already_at_minimum() {
let result = nelder_mead_2d(
|x, y| x * x + y * y,
0.0,
0.0,
0.01,
0.01,
&default_config(),
);
assert!(result.fval < 1e-4, "starting near minimum should converge");
}
}