/* * This module contains the definition of a Levenberg-Marquardt solver for * non-linear least squares problems of the following form: * * arg min ||f(X)|| = arg min f(X)^T f(X) * X X * * where X is an Nx1 state vector and f(X) is an Mx1 nonlinear measurement error * function of X which we wish to minimize the norm of. * * Levenberg-Marquardt solves the above problem through a damped Gauss-Newton * method where the solver step on each iteration is chosen such that: * (J' J + uI) * step = - J' f(x) * where J = df(x)/dx is the Jacobian of the error function, u is a damping * factor, and I is the identity matrix. * * The algorithm implemented here follows Algorithm 3.16 outlined in this paper: * Madsen, Kaj, Hans Bruun Nielsen, and Ole Tingleff. * "Methods for non-linear least squares problems." (2004). * * This algorithm uses a variant of the Marquardt method for updating the * damping factor which ensures that changes in the factor have no * discontinuities or fluttering behavior between solver steps. */ #ifndef LOCATION_LBS_CONTEXTHUB_NANOAPPS_COMMON_MATH_LEVENBERG_MARQUARDT_H_ #define LOCATION_LBS_CONTEXTHUB_NANOAPPS_COMMON_MATH_LEVENBERG_MARQUARDT_H_ #include #include #ifdef __cplusplus extern "C" { #endif // Function pointer for computing residual, f(X), and Jacobian, J = df(X)/dX // for a given state value, X, and other parameter data needed for computing // these terms, f_data. // // Note if f_data is not needed, it is allowable for f_data to be passed in // as NULL. // // jacobian is also allowed to be passed in as NULL, and in this case only the // residual will be computed and returned. typedef void (*ResidualAndJacobianFunction)(const float *state, const void *f_data, float *residual, float *jacobian); #define MAX_LM_STATE_DIMENSION (10) #define MAX_LM_MEAS_DIMENSION (20) // Structure containing fixed parameters for a single LM solve. struct LmParams { // maximum number of iterations allowed by the solver. uint32_t max_iterations; // initial scaling factor for setting the damping factor, i.e.: // damping_factor = initial_u_scale * max(J'J). float initial_u_scale; // magnitude of the cost function gradient required for solution, i.e. // max(gradient) = max(J'f(x)) < gradient_threshold. float gradient_threshold; // magnitude of relative error required for solution step, i.e. // ||step|| / ||state|| < relative_step_thresold. float relative_step_threshold; }; // Structure containing data needed for a single LM solve. // Defining the data here allows flexibility in how the memory is allocated // for the solver. struct LmData { float step[MAX_LM_STATE_DIMENSION]; float residual[MAX_LM_MEAS_DIMENSION]; float residual_new[MAX_LM_MEAS_DIMENSION]; float gradient[MAX_LM_MEAS_DIMENSION]; float hessian[MAX_LM_STATE_DIMENSION * MAX_LM_STATE_DIMENSION]; float temp[MAX_LM_STATE_DIMENSION * MAX_LM_MEAS_DIMENSION]; }; // Enumeration indicating status of the LM solver. enum LmStatus { RUNNING = 0, // Successful solve conditions: RELATIVE_STEP_SUFFICIENTLY_SMALL, // solver step is sufficiently small. GRADIENT_SUFFICIENTLY_SMALL, // cost function gradient is below threshold. HIT_MAX_ITERATIONS, // Solver failure modes: CHOLESKY_FAIL, INVALID_DATA_DIMENSIONS, }; // Structure for containing variables needed for a Levenberg-Marquardt solver. struct LmSolver { // Solver parameters. struct LmParams params; // Pointer to solver data. struct LmData *data; // Function for computing residual (f(x)) and jacobian (df(x)/dx). ResidualAndJacobianFunction func; // Number of iterations in current solution. uint32_t num_iter; }; // Initializes LM solver with provided parameters and error function. void lmSolverInit(struct LmSolver *solver, const struct LmParams *params, ResidualAndJacobianFunction error_func); void lmSolverDestroy(struct LmSolver *solver); // Sets pointer for temporary data needed for an individual LM solve. // Note, this must be called prior to calling lmSolverSolve(). void lmSolverSetData(struct LmSolver *solver, struct LmData *data); /* * Runs the LM solver for a given set of data and error function. * * INPUTS: * solver : pointer to an struct LmSolver structure. * initial_state : initial guess of the estimation state. * f_data : pointer to additional data needed by the error function. * state_dim : dimension of X. * meas_dim : dimension of f(X), defined in the error function. * * OUTPUTS: * LmStatus : enum indicating state of the solver on completion. * est_state : estimated state. */ enum LmStatus lmSolverSolve(struct LmSolver *solver, const float *initial_state, void *f_data, size_t state_dim, size_t meas_dim, float *est_state); ////////////////////////// TEST UTILITIES //////////////////////////////////// // This function is exposed here for testing purposes only. /* * Computes the ratio of actual cost function gain over expected cost * function gain for the given solver step. This ratio is used to adjust * the solver step size for the next solver iteration. * * INPUTS: * residual: f(x) for the current state x. * residual_new: f(x + step) for the new state after the solver step. * step: the solver step. * gradient: gradient of the cost function F(x). * damping_factor: the current damping factor used in the solver. * state_dim: dimension of the state, x. * meas_dim: dimension of f(x). */ float 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); #ifdef __cplusplus } #endif #endif // LOCATION_LBS_CONTEXTHUB_NANOAPPS_COMMON_MATH_LEVENBERG_MARQUARDT_H_