1#[derive(Debug, Clone, Copy, PartialEq)]
31pub struct NelderMeadOptions {
32 pub f_tolerance: f64,
35 pub x_tolerance: f64,
39 pub max_evaluations: usize,
41}
42
43impl NelderMeadOptions {
44 pub fn for_dimension(n: usize) -> Self {
46 Self {
47 f_tolerance: 1e-10,
48 x_tolerance: 1e-8,
49 max_evaluations: (1000 * n).max(2000),
50 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq)]
56pub struct Minimum {
57 pub x: Vec<f64>,
59 pub value: f64,
61 pub evaluations: usize,
63 pub converged: bool,
66}
67
68pub fn nelder_mead<F: FnMut(&[f64]) -> f64>(
90 mut f: F,
91 x0: &[f64],
92 options: NelderMeadOptions,
93) -> Minimum {
94 let n = x0.len();
95 let evaluations = std::cell::Cell::new(0usize);
96 let mut eval = |x: &[f64]| {
97 evaluations.set(evaluations.get() + 1);
98 let v = f(x);
99 if v.is_finite() {
100 v
101 } else {
102 f64::INFINITY
103 }
104 };
105 if n == 0 {
106 let value = eval(x0);
107 return Minimum {
108 x: Vec::new(),
109 value,
110 evaluations: evaluations.get(),
111 converged: true,
112 };
113 }
114
115 let nf = n as f64;
116 let (rho, chi, psi, sigma) = if n <= 2 {
117 (1.0, 2.0, 0.5, 0.5)
118 } else {
119 (1.0, 1.0 + 2.0 / nf, 0.75 - 1.0 / (2.0 * nf), 1.0 - 1.0 / nf)
120 };
121
122 let mut simplex: Vec<Vec<f64>> = Vec::with_capacity(n + 1);
123 simplex.push(x0.to_vec());
124 for i in 0..n {
125 let mut v = x0.to_vec();
126 v[i] = if v[i] != 0.0 { v[i] * 1.05 } else { 0.00025 };
127 simplex.push(v);
128 }
129 let mut values: Vec<f64> = simplex.iter().map(|v| eval(v)).collect();
130 let mut order: Vec<usize> = (0..=n).collect();
131 let mut converged = false;
132
133 loop {
134 order.sort_by(|&a, &b| values[a].total_cmp(&values[b]));
135 let best = order[0];
136 let worst = order[n];
137 let f_spread = order[1..]
138 .iter()
139 .map(|&i| (values[i] - values[best]).abs())
140 .fold(0.0, f64::max);
141 let x_close = order[1..].iter().all(|&i| {
142 simplex[i]
143 .iter()
144 .zip(&simplex[best])
145 .all(|(a, b)| (a - b).abs() <= options.x_tolerance * (1.0 + b.abs()))
146 });
147 if f_spread <= options.f_tolerance * (1.0 + values[best].abs()) && x_close {
149 converged = true;
150 break;
151 }
152 if evaluations.get() >= options.max_evaluations {
153 break;
154 }
155
156 let mut centroid = vec![0.0; n];
158 for &i in &order[..n] {
159 for (c, v) in centroid.iter_mut().zip(&simplex[i]) {
160 *c += v / nf;
161 }
162 }
163 let toward = |t: f64| -> Vec<f64> {
164 centroid
165 .iter()
166 .zip(&simplex[worst])
167 .map(|(c, w)| c + t * (c - w))
168 .collect()
169 };
170
171 let xr = toward(rho);
172 let fr = eval(&xr);
173 let second_worst = values[order[n - 1]];
174 if fr < values[best] {
175 let xe = toward(rho * chi);
176 let fe = eval(&xe);
177 if fe < fr {
178 simplex[worst] = xe;
179 values[worst] = fe;
180 } else {
181 simplex[worst] = xr;
182 values[worst] = fr;
183 }
184 continue;
185 }
186 if fr < second_worst {
187 simplex[worst] = xr;
188 values[worst] = fr;
189 continue;
190 }
191 let (xc, fc, accept) = if fr < values[worst] {
193 let xc = toward(rho * psi);
194 let fc = eval(&xc);
195 let ok = fc <= fr;
196 (xc, fc, ok)
197 } else {
198 let xc = toward(-psi);
199 let fc = eval(&xc);
200 let ok = fc < values[worst];
201 (xc, fc, ok)
202 };
203 if accept {
204 simplex[worst] = xc;
205 values[worst] = fc;
206 continue;
207 }
208 let anchor = simplex[best].clone();
210 for &i in &order[1..] {
211 for (v, a) in simplex[i].iter_mut().zip(&anchor) {
212 *v = a + sigma * (*v - a);
213 }
214 values[i] = eval(&simplex[i]);
215 }
216 }
217
218 order.sort_by(|&a, &b| values[a].total_cmp(&values[b]));
219 Minimum {
220 x: simplex[order[0]].clone(),
221 value: values[order[0]],
222 evaluations: evaluations.get(),
223 converged,
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn a_quadratic_bowl_in_five_dimensions() {
233 let centre = [1.0, -2.0, 3.0, 0.5, -0.25];
234 let f = |x: &[f64]| {
235 x.iter()
236 .zip(¢re)
237 .enumerate()
238 .map(|(i, (a, c))| (i + 1) as f64 * (a - c).powi(2))
239 .sum::<f64>()
240 };
241 let m = nelder_mead(f, &[0.0; 5], NelderMeadOptions::for_dimension(5));
242 assert!(m.converged, "{m:?}");
243 for (a, c) in m.x.iter().zip(¢re) {
244 assert!((a - c).abs() < 1e-4, "{m:?}");
245 }
246 }
247
248 #[test]
249 fn the_same_start_gives_the_same_point() {
250 let f = |x: &[f64]| (x[0] - 3.0).powi(2) + (x[0] * x[1] - 1.0).powi(2);
251 let a = nelder_mead(f, &[0.5, 0.5], NelderMeadOptions::for_dimension(2));
252 let b = nelder_mead(f, &[0.5, 0.5], NelderMeadOptions::for_dimension(2));
253 assert_eq!(a, b);
254 }
255
256 #[test]
257 fn a_refused_point_is_avoided() {
258 let f = |x: &[f64]| {
261 if x.iter().any(|v| !(0.0..=1.0).contains(v)) {
262 f64::NAN
263 } else {
264 (x[0] - 2.0).powi(2) + (x[1] - 2.0).powi(2)
265 }
266 };
267 let m = nelder_mead(f, &[0.5, 0.5], NelderMeadOptions::for_dimension(2));
268 assert!(m.value.is_finite());
269 assert!(m.x.iter().all(|v| (0.0..=1.0).contains(v)), "{m:?}");
270 assert!(m.value < 2.0 + 1e-4, "{m:?}");
271 }
272
273 #[test]
274 fn the_budget_is_kept() {
275 let f = |x: &[f64]| (1.0 - x[0]).powi(2) + 100.0 * (x[1] - x[0] * x[0]).powi(2);
276 let options = NelderMeadOptions {
277 max_evaluations: 30,
278 ..NelderMeadOptions::for_dimension(2)
279 };
280 let m = nelder_mead(f, &[-1.2, 1.0], options);
281 assert!(!m.converged);
282 assert!(m.evaluations <= 30 + 2, "{}", m.evaluations);
284 }
285
286 #[test]
287 fn a_change_of_scale_moves_the_minimum_with_it() {
288 let f = |k: f64| {
289 move |x: &[f64]| 1e6 * k * (((x[0] - 2.0 * k) / k).powi(2) + ((x[1] + k) / k).powi(2))
290 };
291 for k in [1e-6, 1.0, 1e6] {
292 let m = nelder_mead(f(k), &[k, k], NelderMeadOptions::for_dimension(2));
293 assert!(m.converged, "{k}: {m:?}");
294 assert!(
295 (m.x[0] / k - 2.0).abs() < 1e-5 && (m.x[1] / k + 1.0).abs() < 1e-5,
296 "{k}: {m:?}"
297 );
298 }
299 }
300}