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