Source code for giant.calibration.estimators.geometric.lma

from dataclasses import dataclass

import warnings

import numpy as np

from giant.calibration.estimators.geometric.iterative_nonlinear_lstsq import IterativeNonlinearLSTSQ, IterativeNonlinearLstSqOptions
from giant.camera_models import CameraModel


[docs] @dataclass class LMAEstimatorOptions(IterativeNonlinearLstSqOptions): max_divergence_steps: int = 5 """ The maximum number of steps in a row that can diverge before breaking iteration """
[docs] class LMAEstimator(IterativeNonlinearLSTSQ, LMAEstimatorOptions): """ This implements a Levenberg-Marquardt Algorithm estimator, which is analogous to a damped iterative non-linear least squares. This class is nearly exactly the same as the :class:`IterativeNonlinearLSTSQ` except that it adds damping to the update step of the iterative non-linear least squares algorithm and allows a few diverging steps in a row where the damping parameter is updated before failing. The number of diverging steps that are allowed is controlled by the :attr:`max_divergence_steps` setting. This represents only difference from the :class:`IterativeNonlinearLSTSQ` interface from the user's perspective. In general, this algorithm will result in the same answer as the :class:`IterativeNonlinearLSTSQ` algorithm but at a slower convergence rate. In a few cases however, this estimator can be more robust to initial guess errors, achieving convergence when the standard iterative nonlinear least squares diverges. Therefore, it is likely best to start with the :class:`IterativeNonlinearLSTSQ` class an only switch to this if you experience convergence issues. The implementation of the LMA in this class is inspired by https://link.springer.com/article/10.1007/s40295-016-0091-3 """ def __init__(self, model: CameraModel, options: LMAEstimatorOptions | None = None): r""" :param model: The camera model instance to be estimated set with an initial guess of the state. :param options: The options dataclass to configure the class with """ if options is None: # need to manually do this due to the way the inheritance is structured options = LMAEstimatorOptions() super().__init__(model=model, options=options)
[docs] def estimate(self) -> None: """ This method estimates the postfit residuals based on the model, weight matrix, lma coefficient, etc. Convergence is achieved once the standard deviation of the computed residuals is less than the absolute tolerance or the difference between the prefit and postfit residuals is less than the relative tolerance. """ if self.measurements is None: raise ValueError("measurements must not be None before a call to estimate") if self.camera_frame_directions is None: raise ValueError("camera_frame_directions must not be None before a call to estimate") if self.weighted_estimation and (self.measurement_covariance is None): raise ValueError("measurement_covariance must not be None before a call to estimate " "if weighed_estimation is True") if self.model.use_a_priori and (self.a_priori_model_covariance is None): raise ValueError("a_priori_model_covariance must not be None before a call to estimate " "if model.use_a_priori is True") # get the size of the state vector a_priori_state = np.array(self.model.state_vector) state_size = len(a_priori_state) # get the number of measurements num_meas = self.measurements.size # get the weight matrix weight_matrix = self._compute_weight_matrix(state_size, num_meas) # calculate the prefit residuals prefit_residuals = self.compute_residuals() pre_ss = prefit_residuals.ravel() @ prefit_residuals.ravel() # a flag specifying this is the first time through so we need to initialize the lma_coefficient first = True lma_coefficient = 0 n_diverge = 0 # iterate to convergence for _ in range(self.max_iter): # get the jacobian matrix jacobian = self.model.compute_jacobian(self.camera_frame_directions, temperature=self.temperatures) if first: # initialize the lma_coefficient lma_coefficient = 0.001 * np.trace(jacobian.T @ jacobian) / jacobian.shape[1] if self.model.use_a_priori: residuals_vec = np.concatenate([prefit_residuals.reshape((-1, 1), order='F'), np.zeros((state_size, 1))], axis=0) else: residuals_vec = prefit_residuals.reshape((-1, 1), order='F') if np.isscalar(weight_matrix): lhs: np.ndarray = np.sqrt(weight_matrix) * jacobian.T @ jacobian rhs: np.ndarray = np.sqrt(weight_matrix) * jacobian.T @ residuals_vec else: lhs: np.ndarray = jacobian.T @ weight_matrix @ jacobian rhs: np.ndarray = jacobian.T @ weight_matrix @ residuals_vec # get the update vector using LMA update_vec = np.linalg.solve(lhs + lma_coefficient*np.diag(np.diag(lhs)), rhs).astype(np.float64) model_copy = self.model.copy() model_copy.apply_update(update_vec) postfit_residuals = self.compute_residuals(model=model_copy) post_ss = postfit_residuals.ravel() @ postfit_residuals.ravel() resid_change = abs(pre_ss - post_ss) # check for convergence if resid_change <= (self.residual_atol + self.residual_rtol * pre_ss): self._successful = True self._postfit_residuals = postfit_residuals self.model = model_copy self._jacobian = self.model.compute_jacobian(self.camera_frame_directions, temperature=self.temperatures) return elif (np.abs(update_vec) <= (self.state_atol + self.state_rtol * a_priori_state)).all(): self._successful = True self._postfit_residuals = postfit_residuals self.model = model_copy self._jacobian = self.model.compute_jacobian(self.camera_frame_directions, temperature=self.temperatures) return elif pre_ss < post_ss: # check for divergence n_diverge += 1 if n_diverge > self.max_divergence_steps: warnings.warn('Solution is diverging. Stopping iteration.' '\n\tpre-update residuals {}' '\n\tpost-update residuals {}' '\n\tdiverged for {} iterations'.format(pre_ss, post_ss, n_diverge)) self._successful = False self._postfit_residuals = None self._jacobian = None return # update the lma coefficient lma_coefficient *= 10 else: # converging # reset the divergence counter n_diverge = 0 # update the lma coefficient lma_coefficient /= 10 # prepare for the next iteration self.model = model_copy prefit_residuals = postfit_residuals pre_ss = post_ss a_priori_state = np.array(self.model.state_vector) warnings.warn("Solution didn't converge in the requested number of iterations") self._successful = False self._postfit_residuals = prefit_residuals self.model self._jacobian = self.model.compute_jacobian(self.camera_frame_directions, temperature=self.temperatures)