LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
levmar.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_LEVMAR_H
6#define LINE_UTIL_LEVMAR_H
7
8/**
9 * @file
10 * @ingroup line_util
11 * Levenberg-Marquardt for nonlinear least squares.
12 *
13 * Minimizes S(x) = sum_i r_i(x)^2 for a caller-supplied residual map
14 * r : R^n -> R^m. This is the workhorse behind the moment-matching fits in
15 * line/api/mam: every one of them states its target as a vector of relative
16 * errors (moment_fitted/moment_target - 1) that would be zero at an exact
17 * match, which is exactly the shape LM wants.
18 *
19 * ACCEPTANCE CONTRACT. Substituting an optimizer is not a transcription. The
20 * caller must NOT expect the iterates, the iteration count, or the last digits
21 * of the answer to agree with MATLAB's fmincon / fminsearch / optimproblem
22 * solve, which are different algorithms with different termination rules and,
23 * in the GlobalSearch cases, a random multi-start. What a caller may rely on
24 * is stated per entry point in terms of the *specification*: the achieved
25 * objective value, which is returned so it can be compared against any other
26 * optimizer's on the same input.
27 *
28 * Implementation notes:
29 * - the step solves (J^T J + mu I) dx = -J^T r with mu = lambda max_j
30 * (J^T J)_jj, the uniform damping of Madsen, Nielsen and Tingleff, which
31 * is scaled to the problem yet insensitive to a column of J that is only
32 * differencing noise (see the note in levmar_jac on why Marquardt's
33 * per-column damping cannot be used with a numeric Jacobian);
34 * - lambda is multiplied by lambda_increase on a rejected step and by
35 * lambda_decrease on an accepted one;
36 * - the Jacobian defaults to central differences with a relative step, which
37 * costs 2n residual evaluations per iteration and is second-order
38 * accurate; an analytic Jacobian is supplied through levmar_jac.
39 *
40 * Deterministic: no random restarts, no global state, no exit(), no output.
41 * Failure to converge is reported through LevmarResult::converged, never
42 * thrown, since a partially converged fit is still usable and the caller is
43 * the one that knows the tolerance it needs.
44 *
45 * Gated on transcendental arithmetic: the method stops on tolerances, so it
46 * is meaningless at exact arithmetic (which would run to the iteration cap
47 * carrying ever larger rationals).
48 */
49
50#include <cstddef>
51#include <memory>
52#include <vector>
53
54#include "line/num/number.h"
55#include "line/util/error.h"
56#include "line/util/lu.h"
57#include "line/util/matrix.h"
58
59namespace line {
60
61/** Tuning of the Levenberg-Marquardt iteration. */
62template <class T>
64 T ftol; ///< stop when the relative decrease of S falls below this
65 T xtol; ///< stop when the relative step length falls below this
66 T gtol; ///< stop when max|J^T r| falls below this
67 T lambda0; ///< initial damping
68 T lambda_increase; ///< factor applied to lambda after a rejected step
69 T lambda_decrease; ///< factor applied to lambda after an accepted step
70 T lambda_max; ///< give up on the iteration once lambda exceeds this
71 T diff_step; ///< relative step of the central-difference Jacobian
72 unsigned max_iter; ///< cap on accepted-or-rejected outer iterations
73};
74
75/** MINPACK-like defaults, with a central-difference step of eps^(1/3). */
76template <class T>
90
91/** Outcome of a least-squares solve. */
92template <class T>
94 std::vector<T> x; ///< best point found
95 std::vector<T> residual; ///< r(x)
96 T ssq; ///< sum of squares at x, the objective value
97 unsigned iterations; ///< outer iterations performed
98 unsigned evaluations; ///< residual evaluations, differencing included
99 bool converged; ///< a tolerance was met before the caps
100};
101
102namespace levmardetail {
103
104template <class T>
105T sum_squares(const std::vector<T>& r) {
107 for (std::size_t i = 0; i < r.size(); ++i) s += r[i] * r[i];
108 return s;
109}
110
111template <class T>
112T norm2(const std::vector<T>& v) {
113 using std::sqrt;
114 return sqrt(sum_squares(v));
115}
116
117} // namespace levmardetail
118
119/**
120 * Central-difference Jacobian of r at x.
121 *
122 * The step for component j is diff_step * max(|x_j|, 1), so a variable of any
123 * magnitude gets a meaningful perturbation and a variable at zero still gets
124 * one.
125 *
126 * @param f residual map, x -> vector of length m
127 * @param x evaluation point
128 * @param m number of residuals
129 * @param diff_step relative differencing step
130 * @return the m x n Jacobian
131 */
132template <class T, class F>
133Matrix<T> levmar_jacobian_fd(F f, const std::vector<T>& x, std::size_t m, const T& diff_step) {
135 "levmar requires transcendental arithmetic (it stops on tolerances)");
136 const std::size_t n = x.size();
137 const T one = num_traits<T>::from_int(1);
138 const T two = num_traits<T>::from_int(2);
140 std::vector<T> xp = x;
141 for (std::size_t j = 0; j < n; ++j) {
142 const T ax = num_abs(x[j]);
143 const T scale = ax > one ? ax : one;
144 const T h = diff_step * scale;
145 xp[j] = x[j] + h;
146 const std::vector<T> rp = f(xp);
147 xp[j] = x[j] - h;
148 const std::vector<T> rm = f(xp);
149 xp[j] = x[j];
150 if (rp.size() != m || rm.size() != m)
151 throw InputError("levmar_jacobian_fd: residual length changed between evaluations");
152 const T den = two * h;
153 for (std::size_t i = 0; i < m; ++i) J(i, j) = (rp[i] - rm[i]) / den;
154 }
155 return J;
156}
157
158/**
159 * Levenberg-Marquardt with a caller-supplied Jacobian.
160 *
161 * @param f residual map, x -> vector of length m
162 * @param jac Jacobian map, x -> m x n Matrix
163 * @param x0 starting point
164 * @param m number of residuals
165 * @param opt tuning
166 */
167template <class T, class F, class J>
168LevmarResult<T> levmar_jac(F f, J jac, const std::vector<T>& x0, std::size_t m,
169 const LevmarOptions<T>& opt) {
171 "levmar requires transcendental arithmetic (it stops on tolerances)");
172 const T zero = num_traits<T>::from_int(0);
173 const std::size_t n = x0.size();
174 if (n == 0) throw InputError("levmar: no variables");
175 if (m == 0) throw InputError("levmar: no residuals");
176
177 LevmarResult<T> res;
178 res.x = x0;
179 res.residual = f(res.x);
180 if (res.residual.size() != m) throw InputError("levmar: residual length disagrees with m");
181 res.ssq = levmardetail::sum_squares(res.residual);
182 res.iterations = 0;
183 res.evaluations = 1;
184 res.converged = false;
185
186 T lambda = opt.lambda0;
187 // ftol/xtol two-consecutive-step convergence test: see _kb/14-cpp-multiprecision.md
188 unsigned tol_hits = 0;
189
190 for (unsigned it = 0; it < opt.max_iter; ++it) {
191 res.iterations = it + 1;
192 const Matrix<T> Jm = jac(res.x);
193 if (Jm.rows() != m || Jm.cols() != n) throw InputError("levmar: Jacobian has wrong shape");
194
195 // gradient of S/2 and the Gauss-Newton normal matrix
196 std::vector<T> g(n, zero);
197 for (std::size_t j = 0; j < n; ++j) {
198 T s = zero;
199 for (std::size_t i = 0; i < m; ++i) s += Jm(i, j) * res.residual[i];
200 g[j] = s;
201 }
202 T gmax = zero;
203 for (std::size_t j = 0; j < n; ++j) {
204 const T a = num_abs(g[j]);
205 if (a > gmax) gmax = a;
206 }
207 if (gmax <= opt.gtol) {
208 res.converged = true;
209 return res;
210 }
211
212 Matrix<T> A(n, n, zero);
213 for (std::size_t j = 0; j < n; ++j)
214 for (std::size_t k = j; k < n; ++k) {
215 T s = zero;
216 for (std::size_t i = 0; i < m; ++i) s += Jm(i, j) * Jm(i, k);
217 A(j, k) = s;
218 A(k, j) = s;
219 }
220
221 // uniform Levenberg-Marquardt damping: see _kb/14-cpp-multiprecision.md
222 T dmax = zero;
223 for (std::size_t j = 0; j < n; ++j)
224 if (A(j, j) > dmax) dmax = A(j, j);
225 if (dmax == zero) dmax = num_traits<T>::from_int(1);
226
227 bool accepted = false;
228 while (!accepted && lambda <= opt.lambda_max) {
229 Matrix<T> Aug = A;
230 for (std::size_t j = 0; j < n; ++j) Aug(j, j) = A(j, j) + lambda * dmax;
231 std::vector<T> rhs(n);
232 for (std::size_t j = 0; j < n; ++j) rhs[j] = -g[j];
233
234 std::vector<T> dx;
235 bool solved = true;
236 try {
237 dx = solve(Aug, rhs);
238 } catch (const NumericError&) {
239 solved = false;
240 }
241 if (!solved) {
242 lambda *= opt.lambda_increase;
243 continue;
244 }
245
246 std::vector<T> xn(n);
247 for (std::size_t j = 0; j < n; ++j) xn[j] = res.x[j] + dx[j];
248 const std::vector<T> rn = f(xn);
249 ++res.evaluations;
250 if (rn.size() != m) throw InputError("levmar: residual length changed between calls");
251 const T sn = levmardetail::sum_squares(rn);
252
253 if (sn < res.ssq) {
254 const T dnorm = levmardetail::norm2(dx);
255 const T xnorm = levmardetail::norm2(res.x);
256 const T dssq = res.ssq - sn;
257 const bool ftol_met = dssq <= opt.ftol * res.ssq;
258 const bool xtol_met = dnorm <= opt.xtol * (xnorm + opt.xtol);
259 res.x = xn;
260 res.residual = rn;
261 res.ssq = sn;
262 lambda *= opt.lambda_decrease;
263 accepted = true;
264 if (ftol_met || xtol_met) {
265 ++tol_hits;
266 if (tol_hits >= 2) {
267 res.converged = true;
268 return res;
269 }
270 } else {
271 tol_hits = 0;
272 }
273 } else {
274 lambda *= opt.lambda_increase;
275 }
276 }
277
278 if (!accepted) {
279 // damping saturated: no downhill step exists within the model, so
280 // the point is a (local) minimum to the precision of the Jacobian.
281 res.converged = true;
282 return res;
283 }
284 if (res.ssq == zero) {
285 res.converged = true;
286 return res;
287 }
288 }
289 return res;
290}
291
292/**
293 * Levenberg-Marquardt with a central-difference Jacobian.
294 *
295 * @param f residual map, x -> vector of length m
296 * @param x0 starting point
297 * @param m number of residuals
298 * @param opt tuning
299 */
300template <class T, class F>
301LevmarResult<T> levmar(F f, const std::vector<T>& x0, std::size_t m,
302 const LevmarOptions<T>& opt) {
303 const T step = opt.diff_step;
304 // the differencing evaluations happen inside the Jacobian callable, so they
305 // are tallied separately and folded into the reported count
306 std::shared_ptr<unsigned> extra = std::make_shared<unsigned>(0u);
308 f,
309 [f, m, step, extra](const std::vector<T>& x) {
310 *extra += 2u * static_cast<unsigned>(x.size());
311 return levmar_jacobian_fd(f, x, m, step);
312 },
313 x0, m, opt);
314 r.evaluations += *extra;
315 return r;
316}
317
318/** levmar with the default tuning. */
319template <class T, class F>
320LevmarResult<T> levmar(F f, const std::vector<T>& x0, std::size_t m) {
321 return levmar(f, x0, m, levmar_defaults<T>());
322}
323
324} // namespace line
325
326#endif // LINE_UTIL_LEVMAR_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
The algorithm cannot proceed on this instance (singular matrix, ...).
Definition error.h:43
The exception types the port throws.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
Matrix< T > levmar_jacobian_fd(F f, const std::vector< T > &x, std::size_t m, const T &diff_step)
Central-difference Jacobian of r at x.
Definition levmar.h:133
T num_abs(const T &v)
Definition number.h:172
LevmarResult< T > levmar_jac(F f, J jac, const std::vector< T > &x0, std::size_t m, const LevmarOptions< T > &opt)
Levenberg-Marquardt with a caller-supplied Jacobian.
Definition levmar.h:168
LevmarResult< T > levmar(F f, const std::vector< T > &x0, std::size_t m, const LevmarOptions< T > &opt)
Levenberg-Marquardt with a central-difference Jacobian.
Definition levmar.h:301
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
LevmarOptions< T > levmar_defaults()
MINPACK-like defaults, with a central-difference step of eps^(1/3).
Definition levmar.h:77
Number-type abstraction for the templated API port.
Tuning of the Levenberg-Marquardt iteration.
Definition levmar.h:63
T xtol
stop when the relative step length falls below this
Definition levmar.h:65
T ftol
stop when the relative decrease of S falls below this
Definition levmar.h:64
T lambda_max
give up on the iteration once lambda exceeds this
Definition levmar.h:70
T lambda0
initial damping
Definition levmar.h:67
unsigned max_iter
cap on accepted-or-rejected outer iterations
Definition levmar.h:72
T diff_step
relative step of the central-difference Jacobian
Definition levmar.h:71
T gtol
stop when max|J^T r| falls below this
Definition levmar.h:66
T lambda_increase
factor applied to lambda after a rejected step
Definition levmar.h:68
T lambda_decrease
factor applied to lambda after an accepted step
Definition levmar.h:69
Outcome of a least-squares solve.
Definition levmar.h:93
std::vector< T > x
best point found
Definition levmar.h:94
T ssq
sum of squares at x, the objective value
Definition levmar.h:96
std::vector< T > residual
r(x)
Definition levmar.h:95
bool converged
a tolerance was met before the caps
Definition levmar.h:99
unsigned iterations
outer iterations performed
Definition levmar.h:97
unsigned evaluations
residual evaluations, differencing included
Definition levmar.h:98