LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
neldermead.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_UTIL_NELDERMEAD_H
6#define LINE_UTIL_NELDERMEAD_H
7
8/**
9 * @file
10 * @ingroup line_util
11 * Derivative-free simplex minimization (Nelder and Mead, 1965), with optional
12 * box bounds imposed by a change of variables.
13 *
14 * This is the fallback for objectives that are not a sum of squares, that are
15 * only piecewise smooth (the max(.) in an augmented Lagrangian's inequality
16 * term, the max/min clamping in the AMAP(2) autocorrelation bounds), or whose
17 * derivative is not worth n extra evaluations. Where the objective *is* a sum
18 * of squares, prefer line/util/levmar.h: it converges in far fewer
19 * evaluations and reports the residual vector.
20 *
21 * ACCEPTANCE CONTRACT. As for levmar.h: this replaces MATLAB's fminsearch /
22 * fminsearchbnd / patternsearch / PSwarm, and the iterates do not and cannot
23 * agree with theirs. Correctness is judged on the returned objective value and
24 * on the specification the caller states, never on iterate-by-iterate
25 * agreement with a reference implementation.
26 *
27 * DETERMINISM. The initial simplex is constructed from x0 alone: vertex 0 is
28 * x0 and vertex j+1 perturbs coordinate j by step_rel * |x0_j|, or by step_abs
29 * when x0_j is zero. This is the fminsearch construction and involves no
30 * random numbers, so a given (objective, x0, options) triple always produces
31 * the same answer, on any platform. Ties in the vertex ordering are broken by
32 * vertex index, which keeps the sort stable and the run reproducible.
33 *
34 * BOX BOUNDS. Bounds are enforced by an unconstrained reparameterization of
35 * each variable, so the simplex itself is unconstrained and every evaluated
36 * point is strictly feasible:
37 * lower and upper: x = lo + (hi - lo) (sin(u) + 1)/2
38 * lower only: x = lo + u^2
39 * upper only: x = hi - u^2
40 * The transformation is not a bijection (it folds), which is harmless for
41 * minimization but means the returned x, not the internal u, is the answer.
42 * A variable whose bounds coincide is held fixed at that value and removed
43 * from the search. Note the well-known cost of the technique: the objective
44 * seen by the simplex is flat at an active bound (dx/du = 0 there), so
45 * convergence *onto* a bound is slower than in the interior; the returned
46 * point still satisfies the bound exactly.
47 *
48 * Gated on transcendental arithmetic: the method stops on tolerances, and the
49 * two-sided bound transformation needs sin/asin.
50 */
51
52#include <algorithm>
53#include <cmath>
54#include <cstddef>
55#include <vector>
56
57#include "line/num/number.h"
58#include "line/util/error.h"
59
60namespace line {
61
62/** Tuning of the simplex iteration. */
63template <class T>
65 T ftol; ///< stop when the spread of f over the simplex falls below this
66 T xtol; ///< stop when the diameter of the simplex falls below this
67 T step_rel; ///< initial perturbation, relative, for a nonzero coordinate
68 T step_abs; ///< initial perturbation, absolute, for a zero coordinate
69 T alpha; ///< reflection coefficient
70 T gamma; ///< expansion coefficient
71 T rho; ///< contraction coefficient
72 T sigma; ///< shrink coefficient
73 unsigned max_iter; ///< cap on simplex iterations
74 unsigned max_eval; ///< cap on objective evaluations
75};
76
77/** fminsearch's coefficients and initial simplex, with tighter tolerances. */
78template <class T>
93
94/** Outcome of a simplex minimization. */
95template <class T>
97 std::vector<T> x; ///< best point found
98 T fval; ///< objective there
99 unsigned iterations; ///< simplex iterations performed
100 unsigned evaluations; ///< objective evaluations
101 bool converged; ///< both tolerances met before the caps
102};
103
104/**
105 * Box constraint on one variable. Absent bounds are represented by the flags,
106 * not by an infinite value, because not every supported number type has an
107 * infinity.
108 */
109template <class T>
110struct Bound {
111 bool has_lo;
112 bool has_hi;
113 T lo;
114 T hi;
115
116 Bound() : has_lo(false), has_hi(false), lo(num_traits<T>::from_int(0)),
117 hi(num_traits<T>::from_int(0)) {}
118};
119
120/** Unbounded variable. */
121template <class T>
123 return Bound<T>();
124}
125
126/** lo <= x. */
127template <class T>
128Bound<T> bound_lower(const T& lo) {
129 Bound<T> b;
130 b.has_lo = true;
131 b.lo = lo;
132 return b;
133}
134
135/** x <= hi. */
136template <class T>
137Bound<T> bound_upper(const T& hi) {
138 Bound<T> b;
139 b.has_hi = true;
140 b.hi = hi;
141 return b;
142}
143
144/** lo <= x <= hi. */
145template <class T>
146Bound<T> bound_box(const T& lo, const T& hi) {
147 Bound<T> b;
148 b.has_lo = true;
149 b.has_hi = true;
150 b.lo = lo;
151 b.hi = hi;
152 return b;
153}
154
155namespace nmdetail {
156
157template <class T>
158inline T nm_sin(const T& v) {
159 using std::sin;
160 return sin(v);
161}
162
163template <class T>
164inline T nm_asin(const T& v) {
165 using std::asin;
166 return asin(v);
167}
168
169template <class T>
170inline T nm_sqrt(const T& v) {
171 using std::sqrt;
172 return sqrt(v);
173}
174
175/** u -> x for one variable. */
176template <class T>
177T untransform(const T& u, const Bound<T>& b) {
178 const T one = num_traits<T>::from_int(1);
179 const T two = num_traits<T>::from_int(2);
180 if (b.has_lo && b.has_hi) {
181 if (b.hi <= b.lo) return b.lo;
182 const T s = nm_sin(u);
183 return b.lo + (b.hi - b.lo) * (s + one) / two;
184 }
185 if (b.has_lo) return b.lo + u * u;
186 if (b.has_hi) return b.hi - u * u;
187 return u;
188}
189
190/** x -> u for one variable, clamping x into the box first. */
191template <class T>
192T transform(const T& x, const Bound<T>& b) {
193 const T one = num_traits<T>::from_int(1);
194 const T two = num_traits<T>::from_int(2);
195 const T zero = num_traits<T>::from_int(0);
196 if (b.has_lo && b.has_hi) {
197 if (b.hi <= b.lo) return zero;
198 T z = two * (x - b.lo) / (b.hi - b.lo) - one;
199 if (z < -one) z = -one;
200 if (z > one) z = one;
201 return nm_asin(z);
202 }
203 if (b.has_lo) {
204 const T d = x - b.lo;
205 return d > zero ? nm_sqrt(d) : zero;
206 }
207 if (b.has_hi) {
208 const T d = b.hi - x;
209 return d > zero ? nm_sqrt(d) : zero;
210 }
211 return x;
212}
213
214} // namespace nmdetail
215
216/**
217 * Unconstrained simplex minimization.
218 *
219 * @param f objective, x -> T
220 * @param x0 starting point, which becomes vertex 0 of the simplex
221 * @param opt tuning
222 */
223template <class T, class F>
224NelderMeadResult<T> nelder_mead(F f, const std::vector<T>& x0, const NelderMeadOptions<T>& opt) {
226 "nelder_mead requires transcendental arithmetic (it stops on tolerances)");
227 const std::size_t n = x0.size();
228 if (n == 0) throw InputError("nelder_mead: no variables");
229 const T zero = num_traits<T>::from_int(0);
230
231 std::vector<std::vector<T>> v(n + 1, x0);
232 for (std::size_t j = 0; j < n; ++j) {
233 if (x0[j] == zero)
234 v[j + 1][j] = opt.step_abs;
235 else
236 v[j + 1][j] = x0[j] * (num_traits<T>::from_int(1) + opt.step_rel);
237 }
238
239 std::vector<T> fv(n + 1);
240 unsigned evals = 0;
241 for (std::size_t i = 0; i <= n; ++i) {
242 fv[i] = f(v[i]);
243 ++evals;
244 }
245
246 std::vector<std::size_t> ord(n + 1);
248 res.converged = false;
249 res.iterations = 0;
250
251 for (unsigned it = 0; it < opt.max_iter; ++it) {
252 res.iterations = it + 1;
253 for (std::size_t i = 0; i <= n; ++i) ord[i] = i;
254 std::stable_sort(ord.begin(), ord.end(),
255 [&fv](std::size_t a, std::size_t b) { return fv[a] < fv[b]; });
256
257 const std::size_t best = ord[0];
258 const std::size_t worst = ord[n];
259 const std::size_t second = ord[n - 1];
260
261 T fspread = zero;
262 T xspread = zero;
263 for (std::size_t i = 0; i <= n; ++i) {
264 const T df = num_abs(T(fv[i] - fv[best]));
265 if (df > fspread) fspread = df;
266 for (std::size_t j = 0; j < n; ++j) {
267 const T dx = num_abs(T(v[i][j] - v[best][j]));
268 if (dx > xspread) xspread = dx;
269 }
270 }
271 if (fspread <= opt.ftol && xspread <= opt.xtol) {
272 res.converged = true;
273 break;
274 }
275 if (evals >= opt.max_eval) break;
276
277 // centroid of everything but the worst vertex
278 std::vector<T> c(n, zero);
279 for (std::size_t i = 0; i < n; ++i)
280 for (std::size_t j = 0; j < n; ++j) c[j] += v[ord[i]][j];
281 for (std::size_t j = 0; j < n; ++j) c[j] /= num_traits<T>::from_int(long(n));
282
283 std::vector<T> xr(n);
284 for (std::size_t j = 0; j < n; ++j) xr[j] = c[j] + opt.alpha * (c[j] - v[worst][j]);
285 const T fr = f(xr);
286 ++evals;
287
288 if (fr < fv[best]) {
289 std::vector<T> xe(n);
290 for (std::size_t j = 0; j < n; ++j) xe[j] = c[j] + opt.gamma * (xr[j] - c[j]);
291 const T fe = f(xe);
292 ++evals;
293 if (fe < fr) {
294 v[worst] = xe;
295 fv[worst] = fe;
296 } else {
297 v[worst] = xr;
298 fv[worst] = fr;
299 }
300 continue;
301 }
302 if (fr < fv[second]) {
303 v[worst] = xr;
304 fv[worst] = fr;
305 continue;
306 }
307
308 // contraction, outside when the reflection improved on the worst
309 bool shrink = false;
310 if (fr < fv[worst]) {
311 std::vector<T> xc(n);
312 for (std::size_t j = 0; j < n; ++j) xc[j] = c[j] + opt.rho * (xr[j] - c[j]);
313 const T fc = f(xc);
314 ++evals;
315 if (fc <= fr) {
316 v[worst] = xc;
317 fv[worst] = fc;
318 } else {
319 shrink = true;
320 }
321 } else {
322 std::vector<T> xc(n);
323 for (std::size_t j = 0; j < n; ++j) xc[j] = c[j] + opt.rho * (v[worst][j] - c[j]);
324 const T fc = f(xc);
325 ++evals;
326 if (fc < fv[worst]) {
327 v[worst] = xc;
328 fv[worst] = fc;
329 } else {
330 shrink = true;
331 }
332 }
333
334 if (shrink) {
335 for (std::size_t i = 0; i <= n; ++i) {
336 if (i == best) continue;
337 for (std::size_t j = 0; j < n; ++j)
338 v[i][j] = v[best][j] + opt.sigma * (v[i][j] - v[best][j]);
339 fv[i] = f(v[i]);
340 ++evals;
341 }
342 }
343 }
344
345 std::size_t best = 0;
346 for (std::size_t i = 1; i <= n; ++i)
347 if (fv[i] < fv[best]) best = i;
348 res.x = v[best];
349 res.fval = fv[best];
350 res.evaluations = evals;
351 return res;
352}
353
354/** nelder_mead with the default tuning. */
355template <class T, class F>
356NelderMeadResult<T> nelder_mead(F f, const std::vector<T>& x0) {
357 return nelder_mead(f, x0, nelder_mead_defaults<T>());
358}
359
360/**
361 * Box-constrained simplex minimization by the transformation described in the
362 * header comment. Every point at which f is evaluated satisfies the bounds.
363 *
364 * @param f objective, x -> T, called only at feasible x
365 * @param x0 starting point, clamped into the box if it is outside
366 * @param bounds one Bound per variable
367 * @param opt tuning
368 */
369template <class T, class F>
370NelderMeadResult<T> nelder_mead_box(F f, const std::vector<T>& x0,
371 const std::vector<Bound<T>>& bounds,
372 const NelderMeadOptions<T>& opt) {
374 "nelder_mead_box requires transcendental arithmetic");
375 const std::size_t n = x0.size();
376 if (bounds.size() != n) throw InputError("nelder_mead_box: one bound per variable is required");
377 for (std::size_t j = 0; j < n; ++j)
378 if (bounds[j].has_lo && bounds[j].has_hi && bounds[j].hi < bounds[j].lo)
379 throw InputError("nelder_mead_box: upper bound below lower bound");
380
381 // variables with coincident bounds are fixed and taken out of the search
382 std::vector<std::size_t> free_idx;
383 std::vector<T> xfix = x0;
384 for (std::size_t j = 0; j < n; ++j) {
385 if (bounds[j].has_lo && bounds[j].has_hi && bounds[j].hi == bounds[j].lo)
386 xfix[j] = bounds[j].lo;
387 else
388 free_idx.push_back(j);
389 }
390 if (free_idx.empty()) {
392 r.x = xfix;
393 r.fval = f(xfix);
394 r.iterations = 0;
395 r.evaluations = 1;
396 r.converged = true;
397 return r;
398 }
399
400 std::vector<T> u0(free_idx.size());
401 for (std::size_t k = 0; k < free_idx.size(); ++k)
402 u0[k] = nmdetail::transform(x0[free_idx[k]], bounds[free_idx[k]]);
403
404 const std::vector<std::size_t> idx = free_idx;
405 const std::vector<Bound<T>> bnd = bounds;
406 std::vector<T> xbuf = xfix;
407 auto expand = [idx, bnd, xbuf](const std::vector<T>& u) {
408 std::vector<T> x = xbuf;
409 for (std::size_t k = 0; k < idx.size(); ++k)
410 x[idx[k]] = nmdetail::untransform(u[k], bnd[idx[k]]);
411 return x;
412 };
413
414 const NelderMeadResult<T> ru =
415 nelder_mead([f, expand](const std::vector<T>& u) { return f(expand(u)); }, u0, opt);
416
418 r.x = expand(ru.x);
419 r.fval = ru.fval;
420 r.iterations = ru.iterations;
421 r.evaluations = ru.evaluations;
422 r.converged = ru.converged;
423 return r;
424}
425
426/** nelder_mead_box with the default tuning. */
427template <class T, class F>
428NelderMeadResult<T> nelder_mead_box(F f, const std::vector<T>& x0,
429 const std::vector<Bound<T>>& bounds) {
430 return nelder_mead_box(f, x0, bounds, nelder_mead_defaults<T>());
431}
432
433} // namespace line
434
435#endif // LINE_UTIL_NELDERMEAD_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
T num_abs(const T &v)
Definition number.h:172
Bound< T > bound_free()
Unbounded variable.
Definition neldermead.h:122
NelderMeadResult< T > nelder_mead(F f, const std::vector< T > &x0, const NelderMeadOptions< T > &opt)
Unconstrained simplex minimization.
Definition neldermead.h:224
NelderMeadResult< T > nelder_mead_box(F f, const std::vector< T > &x0, const std::vector< Bound< T > > &bounds, const NelderMeadOptions< T > &opt)
Box-constrained simplex minimization by the transformation described in the header comment.
Definition neldermead.h:370
Bound< T > bound_upper(const T &hi)
x <= hi.
Definition neldermead.h:137
NelderMeadOptions< T > nelder_mead_defaults()
fminsearch's coefficients and initial simplex, with tighter tolerances.
Definition neldermead.h:79
Bound< T > bound_box(const T &lo, const T &hi)
lo <= x <= hi.
Definition neldermead.h:146
Bound< T > bound_lower(const T &lo)
lo <= x.
Definition neldermead.h:128
Number-type abstraction for the templated API port.
Box constraint on one variable.
Definition neldermead.h:110
Tuning of the simplex iteration.
Definition neldermead.h:64
unsigned max_eval
cap on objective evaluations
Definition neldermead.h:74
T ftol
stop when the spread of f over the simplex falls below this
Definition neldermead.h:65
T step_rel
initial perturbation, relative, for a nonzero coordinate
Definition neldermead.h:67
T sigma
shrink coefficient
Definition neldermead.h:72
T rho
contraction coefficient
Definition neldermead.h:71
T gamma
expansion coefficient
Definition neldermead.h:70
T xtol
stop when the diameter of the simplex falls below this
Definition neldermead.h:66
T alpha
reflection coefficient
Definition neldermead.h:69
T step_abs
initial perturbation, absolute, for a zero coordinate
Definition neldermead.h:68
unsigned max_iter
cap on simplex iterations
Definition neldermead.h:73
Outcome of a simplex minimization.
Definition neldermead.h:96
std::vector< T > x
best point found
Definition neldermead.h:97
T fval
objective there
Definition neldermead.h:98
unsigned evaluations
objective evaluations
Definition neldermead.h:100
bool converged
both tolerances met before the caps
Definition neldermead.h:101
unsigned iterations
simplex iterations performed
Definition neldermead.h:99