1 #include "common/math/levenberg_marquardt.h"
2
3 #include <stdbool.h>
4 #include <stdio.h>
5 #include <string.h>
6
7 #include "common/math/macros.h"
8 #include "common/math/mat.h"
9 #include "common/math/vec.h"
10
11 // FORWARD DECLARATIONS
12 ////////////////////////////////////////////////////////////////////////
13 static bool checkRelativeStepSize(const float *step, const float *state,
14 size_t dim, float relative_error_threshold);
15
16 static bool computeResidualAndGradients(ResidualAndJacobianFunction func,
17 const float *state, const void *f_data,
18 float *jacobian,
19 float gradient_threshold,
20 size_t state_dim, size_t meas_dim,
21 float *residual, float *gradient,
22 float *hessian);
23
24 static bool computeStep(const float *gradient, float *hessian, float *L,
25 float damping_factor, size_t dim, float *step);
26
27 const static float kEps = 1e-10f;
28
29 // FUNCTION IMPLEMENTATIONS
30 ////////////////////////////////////////////////////////////////////////
lmSolverInit(struct LmSolver * solver,const struct LmParams * params,ResidualAndJacobianFunction func)31 void lmSolverInit(struct LmSolver *solver, const struct LmParams *params,
32 ResidualAndJacobianFunction func) {
33 ASSERT_NOT_NULL(solver);
34 ASSERT_NOT_NULL(params);
35 ASSERT_NOT_NULL(func);
36 memset(solver, 0, sizeof(struct LmSolver));
37 memcpy(&solver->params, params, sizeof(struct LmParams));
38 solver->func = func;
39 solver->num_iter = 0;
40 }
41
lmSolverDestroy(struct LmSolver * solver)42 void lmSolverDestroy(struct LmSolver *solver) {
43 (void)solver;
44 }
45
lmSolverSetData(struct LmSolver * solver,struct LmData * data)46 void lmSolverSetData(struct LmSolver *solver, struct LmData *data) {
47 ASSERT_NOT_NULL(solver);
48 ASSERT_NOT_NULL(data);
49 solver->data = data;
50 }
51
lmSolverSolve(struct LmSolver * solver,const float * initial_state,void * f_data,size_t state_dim,size_t meas_dim,float * state)52 enum LmStatus lmSolverSolve(struct LmSolver *solver, const float *initial_state,
53 void *f_data, size_t state_dim, size_t meas_dim,
54 float *state) {
55 // Initialize parameters.
56 float damping_factor = 0.0f;
57 float v = 2.0f;
58
59 // Check dimensions.
60 if (meas_dim > MAX_LM_MEAS_DIMENSION || state_dim > MAX_LM_STATE_DIMENSION) {
61 return INVALID_DATA_DIMENSIONS;
62 }
63
64 // Check pointers (note that f_data can be null if no additional data is
65 // required by the error function).
66 ASSERT_NOT_NULL(solver);
67 ASSERT_NOT_NULL(initial_state);
68 ASSERT_NOT_NULL(state);
69 ASSERT_NOT_NULL(solver->data);
70
71 // Allocate memory for intermediate variables.
72 float state_new[MAX_LM_STATE_DIMENSION];
73 struct LmData *data = solver->data;
74
75 // state = initial_state, num_iter = 0
76 memcpy(state, initial_state, sizeof(float) * state_dim);
77 solver->num_iter = 0;
78
79 // Compute initial cost function gradient and return if already sufficiently
80 // small to satisfy solution.
81 if (computeResidualAndGradients(solver->func, state, f_data, data->temp,
82 solver->params.gradient_threshold, state_dim,
83 meas_dim, data->residual,
84 data->gradient,
85 data->hessian)) {
86 return GRADIENT_SUFFICIENTLY_SMALL;
87 }
88
89 // Initialize damping parameter.
90 damping_factor = solver->params.initial_u_scale *
91 matMaxDiagonalElement(data->hessian, state_dim);
92
93 // Iterate solution.
94 for (solver->num_iter = 0;
95 solver->num_iter < solver->params.max_iterations;
96 ++solver->num_iter) {
97
98 // Compute new solver step.
99 if (!computeStep(data->gradient, data->hessian, data->temp, damping_factor,
100 state_dim, data->step)) {
101 return CHOLESKY_FAIL;
102 }
103
104 // If the new step is already sufficiently small, we have a solution.
105 if (checkRelativeStepSize(data->step, state, state_dim,
106 solver->params.relative_step_threshold)) {
107 return RELATIVE_STEP_SUFFICIENTLY_SMALL;
108 }
109
110 // state_new = state + step.
111 vecAdd(state_new, state, data->step, state_dim);
112
113 // Compute new cost function residual.
114 solver->func(state_new, f_data, data->residual_new, NULL);
115
116 // Compute ratio of expected to actual cost function gain for this step.
117 const float gain_ratio = computeGainRatio(data->residual,
118 data->residual_new,
119 data->step, data->gradient,
120 damping_factor, state_dim,
121 meas_dim);
122
123 // If gain ratio is positive, the step size is good, otherwise adjust
124 // damping factor and compute a new step.
125 if (gain_ratio > 0.0f) {
126 // Set state to new state vector: state = state_new.
127 memcpy(state, state_new, sizeof(float) * state_dim);
128
129 // Check if cost function gradient is now sufficiently small,
130 // in which case we have a local solution.
131 if (computeResidualAndGradients(solver->func, state, f_data, data->temp,
132 solver->params.gradient_threshold,
133 state_dim, meas_dim, data->residual,
134 data->gradient, data->hessian)) {
135 return GRADIENT_SUFFICIENTLY_SMALL;
136 }
137
138 // Update damping factor based on gain ratio.
139 // Note, this update logic comes from Equation 2.21 in the following:
140 // [Madsen, Kaj, Hans Bruun Nielsen, and Ole Tingleff.
141 // "Methods for non-linear least squares problems." (2004)].
142 const float tmp = 2.f * gain_ratio - 1.f;
143 damping_factor *= NANO_MAX(0.33333f, 1.f - tmp * tmp * tmp);
144 v = 2.f;
145 } else {
146 // Update damping factor and try again.
147 damping_factor *= v;
148 v *= 2.f;
149 }
150 }
151
152 return HIT_MAX_ITERATIONS;
153 }
154
computeGainRatio(const float * residual,const float * residual_new,const float * step,const float * gradient,float damping_factor,size_t state_dim,size_t meas_dim)155 float computeGainRatio(const float *residual, const float *residual_new,
156 const float *step, const float *gradient,
157 float damping_factor, size_t state_dim,
158 size_t meas_dim) {
159 // Compute true_gain = residual' residual - residual_new' residual_new.
160 const float true_gain = vecDot(residual, residual, meas_dim)
161 - vecDot(residual_new, residual_new, meas_dim);
162
163 // predicted gain = 0.5 * step' * (damping_factor * step + gradient).
164 float tmp[MAX_LM_STATE_DIMENSION];
165 vecScalarMul(tmp, step, damping_factor, state_dim);
166 vecAddInPlace(tmp, gradient, state_dim);
167 const float predicted_gain = 0.5f * vecDot(step, tmp, state_dim);
168
169 // Check that we don't divide by zero! If denominator is too small,
170 // set gain_ratio = 1 to use the current step.
171 if (predicted_gain < kEps) {
172 return 1.f;
173 }
174
175 return true_gain / predicted_gain;
176 }
177
178 /*
179 * Tests if a solution is found based on the size of the step relative to the
180 * current state magnitude. Returns true if a solution is found.
181 *
182 * TODO(dvitus): consider optimization of this function to use squared norm
183 * rather than norm for relative error computation to avoid square root.
184 */
checkRelativeStepSize(const float * step,const float * state,size_t dim,float relative_error_threshold)185 bool checkRelativeStepSize(const float *step, const float *state,
186 size_t dim, float relative_error_threshold) {
187 // r = eps * (||x|| + eps)
188 const float relative_error = relative_error_threshold *
189 (vecNorm(state, dim) + relative_error_threshold);
190
191 // solved if ||step|| <= r
192 // use squared version of this compare to avoid square root.
193 return (vecNormSquared(step, dim) <= relative_error * relative_error);
194 }
195
196 /*
197 * Computes the residual, f(x), as well as the gradient and hessian of the cost
198 * function for the given state.
199 *
200 * Returns a boolean indicating if the computed gradient is sufficiently small
201 * to indicate that a solution has been found.
202 *
203 * INPUTS:
204 * state: state estimate (x) for which to compute the gradient & hessian.
205 * f_data: pointer to parameter data needed for the residual or jacobian.
206 * jacobian: pointer to temporary memory for storing jacobian.
207 * Must be at least MAX_LM_STATE_DIMENSION * MAX_LM_MEAS_DIMENSION.
208 * gradient_threshold: if gradient is below this threshold, function returns 1.
209 *
210 * OUTPUTS:
211 * residual: f(x).
212 * gradient: - J' f(x), where J = df(x)/dx
213 * hessian: df^2(x)/dx^2 = J' J
214 */
computeResidualAndGradients(ResidualAndJacobianFunction func,const float * state,const void * f_data,float * jacobian,float gradient_threshold,size_t state_dim,size_t meas_dim,float * residual,float * gradient,float * hessian)215 bool computeResidualAndGradients(ResidualAndJacobianFunction func,
216 const float *state, const void *f_data,
217 float *jacobian, float gradient_threshold,
218 size_t state_dim, size_t meas_dim,
219 float *residual, float *gradient,
220 float *hessian) {
221 // Compute residual and Jacobian.
222 ASSERT_NOT_NULL(state);
223 ASSERT_NOT_NULL(residual);
224 ASSERT_NOT_NULL(gradient);
225 ASSERT_NOT_NULL(hessian);
226 func(state, f_data, residual, jacobian);
227
228 // Compute the cost function hessian = jacobian' jacobian and
229 // gradient = -jacobian' residual
230 matTransposeMultiplyMat(hessian, jacobian, meas_dim, state_dim);
231 matTransposeMultiplyVec(gradient, jacobian, residual, meas_dim, state_dim);
232 vecScalarMulInPlace(gradient, -1.f, state_dim);
233
234 // Check if solution is found (cost function gradient is sufficiently small).
235 return (vecMaxAbsoluteValue(gradient, state_dim) < gradient_threshold);
236 }
237
238 /*
239 * Computes the Levenberg-Marquardt solver step to satisfy the following:
240 * (J'J + uI) * step = - J' f
241 *
242 * INPUTS:
243 * gradient: -J'f
244 * hessian: J'J
245 * L: temp memory of at least MAX_LM_STATE_DIMENSION * MAX_LM_STATE_DIMENSION.
246 * damping_factor: u
247 * dim: state dimension
248 *
249 * OUTPUTS:
250 * step: solution to the above equation.
251 * Function returns false if the solution fails (due to cholesky failure),
252 * otherwise returns true.
253 *
254 * Note that the hessian is modified in this function in order to reduce
255 * local memory requirements.
256 */
computeStep(const float * gradient,float * hessian,float * L,float damping_factor,size_t dim,float * step)257 bool computeStep(const float *gradient, float *hessian, float *L,
258 float damping_factor, size_t dim, float *step) {
259
260 // 1) A = hessian + damping_factor * Identity.
261 matAddConstantDiagonal(hessian, damping_factor, dim);
262
263 // 2) Solve A * step = gradient for step.
264 // a) compute cholesky decomposition of A = L L^T.
265 if (!matCholeskyDecomposition(L, hessian, dim)) {
266 return false;
267 }
268
269 // b) solve for step via back-solve.
270 return matLinearSolveCholesky(step, L, gradient, dim);
271 }
272