"""Navigation Kalman filters.""" from collections import OrderedDict import numpy as np import pandas as pd from scipy.linalg import cholesky, cho_solve, solve_triangular from . import dcm, earth, util N_BASE_STATES = 7 DR1 = 0 DR2 = 1 DV1 = 2 DV2 = 3 PHI1 = 4 PHI2 = 5 PSI3 = 6 DRE = 0 DRN = 1 DVE = 2 DVN = 3 DH = 4 DP = 5 DR = 6 class FiltResult: def __init__(self, **kwargs): self.__dict__.update(kwargs) def __repr__(self): keys = list(self.__dict__.keys()) items = self.__dict__.items() if keys: m = max(map(len, keys)) + 1 return '\n'.join(["{} : {}".format(k.rjust(m), type(v)) for k, v in sorted(items)]) else: return self.__class__.__name__ + "()" class InertialSensor: """Inertial sensor triad description. Below all parameters might be floats or arrays with 3 elements. In the former case the parameter is assumed to be the same for each of 3 sensors. Setting a parameter to None means that such error is not presented in a sensor. Note that all parameters are measured in International System of Units. Generally this class is not intended for public usage except its construction with desired parameters and passing it to a filter class constructor. Parameters ---------- bias : array_like or None Standard deviation of a bias, which is modeled as a random constant (plus an optional random walk). noise : array_like or None Strength of additive white noise. Known as an angle random walk for gyros. bias_walk : array_like or None Strength of white noise which is integrated into the bias. Known as a rate random walk for gyros. Can be set only if `bias` is set. scale : array_like or None Standard deviation of a scale factor, which is modeled as a random constant (plus an optional random walk). scale_walk : array_like or None Strength of white noise which is integrated into the scale factor. Can be set only if `scale` is set. corr_sd, corr_time : array_like or None Steady state standard deviation and correlation time for exponentially correlated noise. You need to set both or none of these values. """ MAX_STATES = 9 MAX_NOISES = 9 def __init__(self, bias=None, noise=None, bias_walk=None, scale=None, scale_walk=None, corr_sd=None, corr_time=None): bias = self._verify_param(bias, 'bias') noise = self._verify_param(noise, 'noise') bias_walk = self._verify_param(bias_walk, 'bias_walk') scale = self._verify_param(scale, 'scale') scale_walk = self._verify_param(scale_walk, 'scale_walk') corr_time = self._verify_param(corr_time, 'corr_time', True) corr_sd = self._verify_param(corr_sd, 'corr_sd') if (corr_sd is None) + (corr_time is None) == 1: raise ValueError("Set both `corr_sd` and `corr_time`.") if bias is None and bias_walk is not None: raise ValueError("Set `bias` if you want to use `bias_walk`.") if scale is None and scale_walk is not None: raise ValueError("Set `scale` if you want to use `scale_walk`.") F = np.zeros((self.MAX_STATES, self.MAX_STATES)) G = np.zeros((self.MAX_STATES, self.MAX_NOISES)) H = np.zeros((3, self.MAX_STATES)) P = np.zeros((self.MAX_STATES, self.MAX_STATES)) q = np.zeros(self.MAX_NOISES) I = np.identity(3) n_states = 0 n_noises = 0 states = OrderedDict() if bias is not None: P[:3, :3] = I * bias ** 2 H[:, :3] = I states['BIAS_1'] = n_states states['BIAS_2'] = n_states + 1 states['BIAS_3'] = n_states + 2 n_states += 3 if scale is not None or scale_walk is not None: P[n_states: n_states + 3, n_states: n_states + 3] = I * scale ** 2 states['SCALE_1'] = n_states states['SCALE_2'] = n_states + 1 states['SCALE_3'] = n_states + 2 n_states += 3 if bias_walk is not None: G[:3, :3] = I q[:3] = bias_walk n_noises += 3 if scale_walk is not None: G[n_noises: n_noises + 3, n_noises: n_noises + 3] = I q[n_noises: n_noises + 3] = scale_walk n_noises += 3 if corr_sd is not None: F[n_states:n_states + 3, n_states:n_states + 3] = -I / corr_time G[n_noises:n_noises + 3, n_noises:n_noises + 3] = I H[:, n_states: n_states + 3] = I P[n_states:n_states + 3, n_states:n_states + 3] = I * corr_sd ** 2 q[n_noises:n_noises + 3] = (2 / corr_time) ** 0.5 * corr_sd states['CORR_1'] = n_states states['CORR_2'] = n_states + 1 states['CORR_3'] = n_states + 2 n_states += 3 n_noises += 3 F = F[:n_states, :n_states] G = G[:n_states, :n_noises] H = H[:, :n_states] P = P[:n_states, :n_states] q = q[:n_noises] self.n_states = n_states self.n_noises = n_noises self.states = states self.bias = bias self.noise = noise self.bias_walk = bias_walk self.scale = scale self.scale_walk = scale_walk self.corr_sd = corr_sd self.corr_time = corr_time self.P = P self.q = q self.F = F self.G = G self._H = H @staticmethod def _verify_param(param, name, only_positive=False): if param is None: return None param = np.asarray(param) if param.ndim == 0: param = np.resize(param, 3) if param.shape != (3,): raise ValueError("`{}` might be float or array with " "3 elements.".format(name)) if only_positive and np.any(param <= 0): raise ValueError("`{}` must contain positive values.".format(name)) elif np.any(param < 0): raise ValueError("`{}` must contain non-negative values." .format(name)) return param def output_matrix(self, readings=None): if self.scale is not None and readings is None: raise ValueError("Inertial `readings` are required when " "`self.scale` is set.") if self.scale is not None: readings = np.asarray(readings) if readings.ndim == 1: H = self._H.copy() i1 = self.states['SCALE_1'] i2 = self.states['SCALE_3'] + 1 H[:, i1: i2] = np.diag(readings) else: n_readings = readings.shape[0] H = np.zeros((n_readings, 3, self.n_states)) H[:] = self._H i1 = self.states['SCALE_1'] i2 = self.states['SCALE_3'] + 1 I1 = np.repeat(np.arange(n_readings), 3) I2 = np.tile(np.arange(3), n_readings) H_view = H[:, :, i1: i2] H_view[I1, I2, I2] = readings.ravel() return H else: return self._H class Observation: """Base class for observation models. Documentation is given to explain how you can implement a new observation model. All you need to do is to implement `compute_obs` function. See Also section contains links to already implemented models. Parameters ---------- data : DataFrame Observed values as a DataFrame. Index must contain time stamps. gain_curve : None, callable or 3-tuple Kalman correction gain curve. It determines the proportionality of a state correction and a normalized measurement residual (by its theoretical covariance). In the standard Kalman correction it is an identity function. To make the filter robust to outliers a sublinear function can be provided. A convenient parametrization of such function is supported. It described by 3 numbers [L, F, C], if q is a normalized residual then: * If q < L: standard Kalman correction is used. * If L <= q < F: correction is kept constant on a level of q = L. * If F <= q < C: correction decays to 0 as ~1/q. * IF q >= C: the measurement is rejected completely. If None (default), the standard Kalman correction will be used. Attributes ---------- data : DataFrame Data saved from the constructor. See Also -------- LatLonObs VeVnObs """ def __init__(self, data, gain_curve=None): if callable(gain_curve): self.gain_curve = gain_curve elif gain_curve is not None: self.gain_curve = self._create_gain_curve(gain_curve) else: self.gain_curve = None self.data = data @staticmethod def _create_gain_curve(params): L, F, C = params def gain_curve(q): if q > C: return 0 if F < q <= C: return L * F * (C - q) / ((C - F) * q) elif L < q <= F: return L else: return q return gain_curve def compute_obs(self, stamp, traj_point): """Compute ingredients for a single linearized observation. It must compute the observation model (z, H, R) at a given time stamp. If the observation is not available at a given `stamp`, it must return None. Parameters ---------- stamp : int Time stamp. traj_point : Series Point of INS trajectory at `stamp`. Returns ------- z : ndarray, shape (n_obs,) Observation vector. A difference between an INS corresponding value and an observed value. H : ndarray, shape (n_obs, 7) Observation model matrix. It relates the vector `z` to the INS error states. R : ndarray, shape (n_obs, n_obs) Covariance matrix of the observation error. """ raise NotImplementedError() class LatLonObs(Observation): """Observation of latitude and longitude (from GPS or any other source). Parameters ---------- data : DataFrame Must contain columns 'lat' and 'lon' for latitude and longitude. Index must contain time stamps. sd : float Measurement accuracy in meters. gain_curve : None, callable or 3-tuple Kalman correction gain curve. It determines the proportionality of a state correction and a normalized measurement residual (by its theoretical covariance). In the standard Kalman correction it is an identity function. To make the filter robust to outliers a sublinear function can be provided. A convenient parametrization of such function is supported. It described by 3 numbers [L, F, C], if q is a normalized residual then: * If q < L: standard Kalman correction is used. * If L <= q < F: correction is kept constant on a level of q = L. * If F <= q < C: correction decays to 0 as ~1/q. * IF q >= C: the measurement is rejected completely. If None (default), the standard Kalman correction will be used. Attributes ---------- data : DataFrame Data saved from the constructor. """ def __init__(self, data, sd, gain_curve=None): super(LatLonObs, self).__init__(data, gain_curve) self.R = np.diag([sd, sd]) ** 2 H = np.zeros((2, N_BASE_STATES)) H[0, DR1] = 1 H[1, DR2] = 1 self.H = H def compute_obs(self, stamp, traj_point): """Compute ingredients for a single observation. See `Observation.compute_obs`. """ if stamp not in self.data.index: return None d_lat = traj_point.lat - self.data.lat.loc[stamp] d_lon = traj_point.lon - self.data.lon.loc[stamp] clat = np.cos(np.deg2rad(self.data.lat.loc[stamp])) z = np.array([ np.deg2rad(d_lon) * earth.R0 * clat, np.deg2rad(d_lat) * earth.R0 ]) return z, self.H, self.R class VeVnObs(Observation): """Observation of East and North velocity (from GPS or any other source). Parameters ---------- data : DataFrame Must contain columns 'VE' and 'VN' for East and North velocity components. Index must contain time stamps. sd : float Measurement accuracy in m/s. gain_curve : None, callable or 3-tuple Kalman correction gain curve. It determines the proportionality of a state correction and a normalized measurement residual (by its theoretical covariance). In the standard Kalman correction it is an identity function. To make the filter robust to outliers a sublinear function can be provided. A convenient parametrization of such function is supported. It described by 3 numbers [L, F, C], if q is a normalized residual then: * If q < L: standard Kalman correction is used. * If L <= q < F: correction is kept constant on a level of q = L. * If F <= q < C: correction decays to 0 as ~1/q. * IF q >= C: the measurement is rejected completely. If None (default), the standard Kalman correction will be used. Attributes ---------- data : DataFrame Data saved from the constructor. """ def __init__(self, data, sd, gain_curve=None): super(VeVnObs, self).__init__(data, gain_curve) self.R = np.diag([sd, sd]) ** 2 def compute_obs(self, stamp, traj_point): """Compute ingredients for a single observation. See `Observation.compute_obs`. """ if stamp not in self.data.index: return None VE = self.data.VE.loc[stamp] VN = self.data.VN.loc[stamp] z = np.array([traj_point.VE - VE, traj_point.VN - VN]) H = np.zeros((2, N_BASE_STATES)) H[0, DV1] = 1 H[0, PSI3] = VN H[1, DV2] = 1 H[1, PSI3] = -VE return z, H, self.R def _errors_transform_matrix(traj): lat = np.deg2rad(traj.lat) VE = traj.VE VN = traj.VN h = np.deg2rad(traj.h) p = np.deg2rad(traj.p) tlat = np.tan(lat) sh, ch = np.sin(h), np.cos(h) cp, tp = np.cos(p), np.tan(p) T = np.zeros((traj.shape[0], N_BASE_STATES, N_BASE_STATES)) T[:, DRE, DR1] = 1 T[:, DRN, DR2] = 1 T[:, DVE, DR1] = VN * tlat / earth.R0 T[:, DVE, DV1] = 1 T[:, DVE, PSI3] = VN T[:, DVN, DR1] = -VE * tlat / earth.R0 T[:, DVN, DV2] = 1 T[:, DVN, PSI3] = -VE T[:, DH, DR1] = tlat / earth.R0 T[:, DH, PHI1] = -sh * tp T[:, DH, PHI2] = -ch * tp T[:, DH, PSI3] = 1 T[:, DP, PHI1] = -ch T[:, DP, PHI2] = sh T[:, DR, PHI1] = -sh / cp T[:, DR, PHI2] = -ch / cp return T def _error_model_matrices(traj): n_samples = traj.shape[0] lat = np.deg2rad(traj.lat) slat, clat = np.sin(lat), np.cos(lat) tlat = slat / clat u = np.zeros((n_samples, 3)) u[:, 1] = earth.RATE * clat u[:, 2] = earth.RATE * slat rho = np.empty((n_samples, 3)) rho[:, 0] = -traj.VN / earth.R0 rho[:, 1] = traj.VE / earth.R0 rho[:, 2] = rho[:, 1] * tlat Cnb = dcm.from_hpr(traj.h, traj.p, traj.r) F = np.zeros((n_samples, N_BASE_STATES, N_BASE_STATES)) F[:, DR1, DR2] = rho[:, 2] F[:, DR1, DV1] = 1 F[:, DR1, PSI3] = traj.VN F[:, DR2, DR1] = -rho[:, 2] F[:, DR2, DV2] = 1 F[:, DR2, PSI3] = -traj.VE F[:, DV1, DV2] = 2 * u[:, 2] + rho[:, 2] F[:, DV1, PHI2] = -earth.G0 F[:, DV2, DV1] = -2 * u[:, 2] - rho[:, 2] F[:, DV2, PHI1] = earth.G0 F[:, PHI1, DR1] = -u[:, 2] / earth.R0 F[:, PHI1, DV2] = -1 / earth.R0 F[:, PHI1, PHI2] = u[:, 2] + rho[:, 2] F[:, PHI1, PSI3] = -u[:, 1] F[:, PHI2, DR2] = -u[:, 2] / earth.R0 F[:, PHI2, DV1] = 1 / earth.R0 F[:, PHI2, PHI1] = -u[:, 2] - rho[:, 2] F[:, PHI2, PSI3] = u[:, 0] F[:, PSI3, DR1] = (u[:, 0] + rho[:, 0]) / earth.R0 F[:, PSI3, DR2] = (u[:, 1] + rho[:, 1]) / earth.R0 F[:, PSI3, PHI1] = u[:, 1] + rho[:, 1] F[:, PSI3, PHI2] = -u[:, 0] - rho[:, 0] B_gyro = np.zeros((n_samples, N_BASE_STATES, 3)) B_gyro[np.ix_(np.arange(n_samples), [PHI1, PHI2, PSI3], [0, 1, 2])] = -Cnb B_accel = np.zeros((n_samples, N_BASE_STATES, 3)) B_accel[np.ix_(np.arange(n_samples), [DV1, DV2], [0, 1, 2])] = Cnb[:, :2] return F, B_gyro, B_accel def propagate_errors(dt, traj, d_lat=0, d_lon=0, d_VE=0, d_VN=0, d_h=0, d_p=0, d_r=0, d_gyro=0, d_accel=0): """Deterministic linear propagation of INS errors. Parameters ---------- dt : float Time step per stamp. traj : DataFrame Trajectory. d_lat, d_lon : float Initial position errors in meters. d_VE, d_VN : float Initial velocity errors. d_h, d_p, d_r : float Initial heading, pitch and roll errors. d_gyro, d_accel : array_like Gyro and accelerometer errors (in SI units). Can be constant or specified for each time stamp in `traj`. Returns ------- traj_err : DataFrame Trajectory errors. """ Fi, Fig, Fia = _error_model_matrices(traj) Phi = 0.5 * (Fi[1:] + Fi[:-1]) * dt Phi[:] += np.identity(Phi.shape[-1]) d_gyro = np.asarray(d_gyro) d_accel = np.asarray(d_accel) if d_gyro.ndim == 0: d_gyro = np.resize(d_gyro, 3) if d_accel.ndim == 0: d_accel = np.resize(d_accel, 3) d_gyro = util.mv_prod(Fig, d_gyro) d_accel = util.mv_prod(Fia, d_accel) d_sensor = 0.5 * (d_gyro[1:] + d_gyro[:-1] + d_accel[1:] + d_accel[:-1]) T = _errors_transform_matrix(traj) d_h = np.deg2rad(d_h) d_p = np.deg2rad(d_p) d_r = np.deg2rad(d_r) x0 = np.array([d_lon, d_lat, d_VE, d_VN, d_h, d_p, d_r]) x0 = np.linalg.inv(T[0]).dot(x0) n_samples = Fi.shape[0] x = np.empty((n_samples, N_BASE_STATES)) x[0] = x0 for i in range(n_samples - 1): x[i + 1] = Phi[i].dot(x[i]) + d_sensor[i] * dt x = util.mv_prod(T, x) error = pd.DataFrame(index=traj.index) error['lat'] = x[:, DRN] error['lon'] = x[:, DRE] error['VE'] = x[:, DVE] error['VN'] = x[:, DVN] error['h'] = np.rad2deg(x[:, DH]) error['p'] = np.rad2deg(x[:, DP]) error['r'] = np.rad2deg(x[:, DR]) return error def _kalman_correct(x, P, z, H, R, gain_factor, gain_curve): PHT = np.dot(P, H.T) S = np.dot(H, PHT) + R e = z - H.dot(x) L = cholesky(S, lower=True) inn = solve_triangular(L, e, lower=True) if gain_curve is not None: q = (np.dot(inn, inn) / inn.shape[0]) ** 0.5 f = gain_curve(q) if f == 0: return inn L *= (q / f) ** 0.5 K = cho_solve((L, True), PHT.T, overwrite_b=True).T if gain_factor is not None: K *= gain_factor[:, None] U = -K.dot(H) U[np.diag_indices_from(U)] += 1 x += K.dot(z - H.dot(x)) P[:] = U.dot(P).dot(U.T) + K.dot(R).dot(K.T) return inn def _refine_stamps(stamps, max_step): stamps = np.sort(np.unique(stamps)) ds = np.diff(stamps) ds_new = [] for d in ds: if d > max_step: repeat, left = divmod(d, max_step) ds_new.append([max_step] * repeat) if left > 0: ds_new.append(left) else: ds_new.append(d) ds_new = np.hstack(ds_new) stamps_new = stamps[0] + np.cumsum(ds_new) return np.hstack((stamps[0], stamps_new)) def _compute_output_errors(traj, x, P, output_stamps, gyro_model, accel_model): T = _errors_transform_matrix(traj.loc[output_stamps]) y = util.mv_prod(T, x[:, :N_BASE_STATES]) Py = util.mm_prod(T, P[:, :N_BASE_STATES, :N_BASE_STATES]) Py = util.mm_prod(Py, T, bt=True) sd_y = np.diagonal(Py, axis1=1, axis2=2) ** 0.5 err = pd.DataFrame(index=output_stamps) err['lat'] = y[:, DRN] err['lon'] = y[:, DRE] err['VE'] = y[:, DVE] err['VN'] = y[:, DVN] err['h'] = np.rad2deg(y[:, DH]) err['p'] = np.rad2deg(y[:, DP]) err['r'] = np.rad2deg(y[:, DR]) sd = pd.DataFrame(index=output_stamps) sd['lat'] = sd_y[:, DRN] sd['lon'] = sd_y[:, DRE] sd['VE'] = sd_y[:, DVE] sd['VN'] = sd_y[:, DVN] sd['h'] = np.rad2deg(sd_y[:, DH]) sd['p'] = np.rad2deg(sd_y[:, DP]) sd['r'] = np.rad2deg(sd_y[:, DR]) gyro_err = pd.DataFrame(index=output_stamps) gyro_sd = pd.DataFrame(index=output_stamps) n = N_BASE_STATES for i, name in enumerate(gyro_model.states): gyro_err[name] = x[:, n + i] gyro_sd[name] = P[:, n + i, n + i] ** 0.5 accel_err = pd.DataFrame(index=output_stamps) accel_sd = pd.DataFrame(index=output_stamps) ng = gyro_model.n_states for i, name in enumerate(accel_model.states): accel_err[name] = x[:, n + ng + i] accel_sd[name] = P[:, n + ng + i, n + ng + i] ** 0.5 return err, sd, gyro_err, gyro_sd, accel_err, accel_sd def _rts_pass(x, P, xa, Pa, Phi): n_points, n_states = x.shape I = np.identity(n_states) for i in reversed(range(n_points - 1)): L = cholesky(Pa[i + 1], check_finite=False) Pa_inv = cho_solve((L, False), I, check_finite=False) C = P[i].dot(Phi[i].T).dot(Pa_inv) x[i] += C.dot(x[i + 1] - xa[i + 1]) P[i] += C.dot(P[i + 1] - Pa[i + 1]).dot(C.T) return x, P class FeedforwardFilter: """INS Kalman filter in a feedforward form. Parameters ---------- dt : float Time step per stamp. traj_ref : DataFrame Trajectory which is used to propagate the error model. It should be reasonably accurate and must be recorded at each successive time stamp without skips. pos_sd : float Initial position uncertainty in meters. vel_sd : float Initial velocity uncertainty. azimuth_sd : float Initial azimuth (heading) uncertainty. level_sd : float Initial level (pitch and roll) uncertainty. gyro_model, accel_model : None or `InertialSensor`, optional Error models for gyros and accelerometers. If None (default), an empty model will be used. gyro, accel : array_like or None, optional Gyro and accelerometer readings, required only if a scale factor is modeled in `gyro_model` and `accel_model` respectively. Attributes ---------- n_states : int Number of states. n_noises : int Number of noise sources. states : OrderedDict Dictionary mapping state names to their indices. """ def __init__(self, dt, traj_ref, pos_sd, vel_sd, azimuth_sd, level_sd, gyro_model=None, accel_model=None, gyro=None, accel=None): if gyro_model is None: gyro_model = InertialSensor() if accel_model is None: accel_model = InertialSensor() if gyro_model.scale is not None and gyro is None: raise ValueError("`gyro_model` contains scale factor errors, " "thus you must provide `gyro`.") if accel_model.scale is not None and accel is None: raise ValueError("`accel_model` contains scale factor errors, " "thus you must provide `accel`.") self.traj_ref = traj_ref n_points = traj_ref.shape[0] n_states = N_BASE_STATES + gyro_model.n_states + accel_model.n_states n_noises = (gyro_model.n_noises + accel_model.n_noises + 3 * (gyro_model.noise is not None) + 3 * (accel_model.noise is not None)) F = np.zeros((n_points, n_states, n_states)) G = np.zeros((n_points, n_states, n_noises)) q = np.zeros(n_noises) P0 = np.zeros((n_states, n_states)) n = N_BASE_STATES n1 = gyro_model.n_states n2 = accel_model.n_states states = OrderedDict(( ('DR1', DR1), ('DR2', DR2), ('DV1', DV1), ('DV2', DV2), ('PHI1', PHI1), ('PHI2', PHI2), ('PSI3', PSI3) )) for name, state in gyro_model.states.items(): states['GYRO_' + name] = n + state for name, state in accel_model.states.items(): states['ACCEL_' + name] = n + n1 + state level_sd = np.deg2rad(level_sd) azimuth_sd = np.deg2rad(azimuth_sd) P0[DR1, DR1] = P0[DR2, DR2] = pos_sd ** 2 P0[DV1, DV1] = P0[DV2, DV2] = vel_sd ** 2 P0[PHI1, PHI1] = P0[PHI2, PHI2] = level_sd ** 2 P0[PSI3, PSI3] = azimuth_sd ** 2 P0[n: n + n1, n: n + n1] = gyro_model.P P0[n + n1: n + n1 + n2, n + n1: n + n1 + n2] = accel_model.P self.P0 = P0 Fi, Fig, Fia = _error_model_matrices(traj_ref) F[:, :n, :n] = Fi F[:, n: n + n1, n: n + n1] = gyro_model.F F[:, n + n1:n + n1 + n2, n + n1: n + n1 + n2] = accel_model.F if gyro is not None: gyro = np.asarray(gyro) gyro = gyro / dt gyro = np.vstack((gyro, 2 * gyro[-1] - gyro[-2])) if accel is not None: accel = np.asarray(accel) accel = accel / dt accel = np.vstack((accel, 2 * accel[-1] - accel[-2])) H_gyro = gyro_model.output_matrix(gyro) H_accel = accel_model.output_matrix(accel) F[:, :n, n: n + n1] = util.mm_prod(Fig, H_gyro) F[:, :n, n + n1: n + n1 + n2] = util.mm_prod(Fia, H_accel) s = 0 s1 = gyro_model.n_noises s2 = accel_model.n_noises if gyro_model.noise is not None: G[:, :n, :3] = Fig q[:3] = gyro_model.noise s += 3 if accel_model.noise is not None: G[:, :n, s: s + 3] = Fia q[s: s + 3] = accel_model.noise s += 3 G[:, n: n + n1, s: s + s1] = gyro_model.G q[s: s + s1] = gyro_model.q G[:, n + n1: n + n1 + n2, s + s1: s + s1 + s2] = accel_model.G q[s + s1: s + s1 + s2] = accel_model.q self.F = F self.q = q self.G = G self.dt = dt self.n_points = n_points self.n_states = n_states self.n_noises = n_noises self.states = states self.gyro_model = gyro_model self.accel_model = accel_model def _validate_parameters(self, traj, observations, gain_factor, max_step, record_stamps): if traj is None: traj = self.traj_ref if not np.all(traj.index == self.traj_ref.index): raise ValueError("Time stamps of reference and computed " "trajectories don't match.") if gain_factor is not None: gain_factor = np.asarray(gain_factor) if gain_factor.shape != (self.n_states,): raise ValueError("`gain_factor` is expected to have shape {}, " "but actually has {}." .format((self.n_states,), gain_factor.shape)) if np.any(gain_factor < 0): raise ValueError("`gain_factor` must contain positive values.") if observations is None: observations = [] stamps = pd.Index([]) for obs in observations: stamps = stamps.union(obs.data.index) start, end = traj.index[0], traj.index[-1] stamps = stamps.union(pd.Index([start, end])) if record_stamps is not None: end = min(end, record_stamps[-1]) record_stamps = record_stamps[(record_stamps >= start) & (record_stamps <= end)] stamps = stamps.union(pd.Index(record_stamps)) stamps = stamps[(stamps >= start) & (stamps <= end)] max_step = max(1, int(np.floor(max_step / self.dt))) stamps = _refine_stamps(stamps, max_step) if record_stamps is None: record_stamps = stamps return traj, observations, stamps, record_stamps, gain_factor def _forward_pass(self, traj, observations, gain_factor, stamps, record_stamps, data_for_backward=False): inds = stamps - stamps[0] if data_for_backward: n_stamps = stamps.shape[0] x = np.empty((n_stamps, self.n_states)) P = np.empty((n_stamps, self.n_states, self.n_states)) xa = x.copy() Pa = P.copy() Phi_arr = np.empty((n_stamps - 1, self.n_states, self.n_states)) record_stamps = stamps else: n_stamps = record_stamps.shape[0] x = np.empty((n_stamps, self.n_states)) P = np.empty((n_stamps, self.n_states, self.n_states)) xa = None Pa = None Phi_arr = None xc = np.zeros(self.n_states) Pc = self.P0.copy() i_save = 0 H_max = np.zeros((10, self.n_states)) obs_stamps = [[] for _ in range(len(observations))] obs_residuals = [[] for _ in range(len(observations))] for i in range(stamps.shape[0] - 1): stamp = stamps[i] ind = inds[i] next_ind = inds[i + 1] if data_for_backward and record_stamps[i_save] == stamp: xa[i_save] = xc Pa[i_save] = Pc for i_obs, obs in enumerate(observations): ret = obs.compute_obs(stamp, traj.loc[stamp]) if ret is not None: z, H, R = ret H_max[:H.shape[0], :N_BASE_STATES] = H res = _kalman_correct(xc, Pc, z, H_max[:H.shape[0]], R, gain_factor, obs.gain_curve) obs_stamps[i_obs].append(stamp) obs_residuals[i_obs].append(res) if record_stamps[i_save] == stamp: x[i_save] = xc P[i_save] = Pc i_save += 1 dt = self.dt * (next_ind - ind) Phi = 0.5 * (self.F[ind] + self.F[next_ind]) * dt Phi[np.diag_indices_from(Phi)] += 1 Qd = 0.5 * (self.G[ind] + self.G[next_ind]) Qd *= self.q Qd = np.dot(Qd, Qd.T) * dt if data_for_backward: Phi_arr[i] = Phi xc = Phi.dot(xc) Pc = Phi.dot(Pc).dot(Phi.T) + Qd x[-1] = xc P[-1] = Pc if data_for_backward: xa[-1] = xc Pa[-1] = Pc residuals = [] for s, r in zip(obs_stamps, obs_residuals): residuals.append(pd.DataFrame(index=s, data=np.asarray(r))) return x, P, xa, Pa, Phi_arr, residuals def run(self, traj=None, observations=[], gain_factor=None, max_step=1, record_stamps=None): """Run the filter. Parameters ---------- traj : DataFrame or None Trajectory computed by INS of which to estimate the errors. If None (default), use `traj_ref` from the constructor. observations : list of `Observation` Observations which will be processed. Empty by default. gain_factor : array_like with shape (n_states,) or None Factor for Kalman gain for each filter state. It might be beneficial in some practical situations to set factors less than 1 in order to decrease influence of measurements on some states. Setting values higher than 1 is unlikely to be reasonable. If None (default), use standard optimal Kalman gain. max_step : float, optional Maximum allowed time step in seconds for errors propagation. Default is 1 second. Set to 0 if you desire the smallest possible step. record_stamps : array_like or None Stamps at which record estimated errors. If None (default), errors will be saved at each stamp used internally in the filter. Returns ------- Bunch object with the fields listed below. Note that all data frames contain stamps only presented in `record_stamps`. traj : DataFrame Trajectory corrected by estimated errors. err, sd : DataFrame Estimated trajectory errors and their standard deviations. gyro_err, gyro_sd : DataFrame Estimated gyro error states and their standard deviations. accel_err, accel_sd : DataFrame Estimated accelerometer error states and their standard deviations. x : ndarray, shape (n_points, n_states) History of the filter states. P : ndarray, shape (n_points, n_states, n_states) History of the filter covariance. residuals : list of DataFrame Each DataFrame corresponds to an observation from `observations`. Its index is observation time stamps and columns contain normalized observations residuals for each component of the observation vector `z`. """ traj, observations, stamps, record_stamps, gain_factor = \ self._validate_parameters(traj, observations, gain_factor, max_step, record_stamps) x, P, _, _, _, residuals = self._forward_pass( traj, observations, gain_factor, stamps, record_stamps) err, sd, gyro_err, gyro_sd, accel_err, accel_sd = \ _compute_output_errors(self.traj_ref, x, P, record_stamps, self.gyro_model, self.accel_model) traj_corr = correct_traj(traj, err) return FiltResult(traj=traj_corr, err=err, sd=sd, gyro_err=gyro_err, gyro_sd=gyro_sd, accel_err=accel_err, accel_sd=accel_sd, x=x, P=P, residuals=residuals) def run_smoother(self, traj=None, observations=[], gain_factor=None, max_step=1, record_stamps=None): """Run the smoother. It means that observations during the whole time is used to estimate the errors at each moment of time (i.e. it is not real time). The Rauch-Tung-Striebel two pass recursion is used [1]_. Parameters ---------- traj : DataFrame or None Trajectory computed by INS of which to estimate the errors. If None (default), use `traj_ref` from the constructor. observations : list of `Observation` Observations which will be processed. Empty by default. gain_factor : array_like with shape (n_states,) or None Factor for Kalman gain for each filter state. It might be beneficial in some practical situations to set factors less than 1 in order to decrease influence of measurements on some states. Setting values higher than 1 is unlikely to be reasonable. If None (default), use standard optimal Kalman gain. max_step : float, optional Maximum allowed time step in seconds for errors propagation. Default is 1 second. Set to 0 if you desire the smallest possible step. record_stamps : array_like or None Stamps at which record estimated errors. If None (default), errors will be saved at each stamp used internally in the filter. Returns ------- Bunch object with the fields listed below. Note that all data frames contain stamps only presented in `record_stamps`. traj : DataFrame Trajectory corrected by estimated errors. It will only contain stamps presented in `record_stamps`. err, sd : DataFrame Estimated trajectory errors and their standard deviations. gyro_err, gyro_sd : DataFrame Estimated gyro error states and their standard deviations. accel_err, accel_sd : DataFrame Estimated accelerometer error states and their standard deviations. x : ndarray, shape (n_points, n_states) History of the filter states. P : ndarray, shape (n_points, n_states, n_states) History of the filter covariance. residuals : list of DataFrame Each DataFrame corresponds to an observation from `observations`. Its index is observation time stamps and columns contain normalized observations residuals for each component of the observation vector `z`. References ---------- .. [1] H. E. Rauch, F. Tung and C.T. Striebel, "Maximum Likelihood Estimates of Linear Dynamic Systems", AIAA Journal, Vol. 3, No. 8, August 1965. """ traj, observations, stamps, record_stamps, gain_factor = \ self._validate_parameters(traj, observations, gain_factor, max_step, record_stamps) x, P, xa, Pa, Phi_arr, residuals = self._forward_pass( traj, observations, gain_factor, stamps, record_stamps, data_for_backward=True) x, P = _rts_pass(x, P, xa, Pa, Phi_arr) ind = np.searchsorted(stamps, record_stamps) x = x[ind] P = P[ind] err, sd, gyro_err, gyro_sd, accel_err, accel_sd = \ _compute_output_errors(self.traj_ref, x, P, record_stamps, self.gyro_model, self.accel_model) traj_corr = correct_traj(traj, err) return FiltResult(traj=traj_corr, err=err, sd=sd, gyro_err=gyro_err, gyro_sd=gyro_sd, accel_err=accel_err, accel_sd=accel_sd, x=x, P=P, residuals=residuals) class FeedbackFilter: """INS Kalman filter with feedback corrections. Parameters ---------- dt : float Time step per stamp. pos_sd : float Initial position uncertainty in meters. vel_sd : float Initial velocity uncertainty. azimuth_sd : float Initial azimuth (heading) uncertainty. level_sd : float Initial level (pitch and roll) uncertainty. gyro_model, accel_model : None or `InertialSensor`, optional Error models for gyros and accelerometers. If None (default), an empty model will be used. Attributes ---------- n_states : int Number of states. n_noises : int Number of noise sources. states : OrderedDict Dictionary mapping state names to their indices. """ def __init__(self, dt, pos_sd, vel_sd, azimuth_sd, level_sd, gyro_model=None, accel_model=None): if gyro_model is None: gyro_model = InertialSensor() if accel_model is None: accel_model = InertialSensor() n_states = N_BASE_STATES + gyro_model.n_states + accel_model.n_states n_noises = (gyro_model.n_noises + accel_model.n_noises + 3 * (gyro_model.noise is not None) + 3 * (accel_model.noise is not None)) q = np.zeros(n_noises) P0 = np.zeros((n_states, n_states)) n = N_BASE_STATES n1 = gyro_model.n_states n2 = accel_model.n_states level_sd = np.deg2rad(level_sd) azimuth_sd = np.deg2rad(azimuth_sd) P0[DR1, DR1] = P0[DR2, DR2] = pos_sd ** 2 P0[DV1, DV1] = P0[DV2, DV2] = vel_sd ** 2 P0[PHI1, PHI1] = P0[PHI2, PHI2] = level_sd ** 2 P0[PSI3, PSI3] = azimuth_sd ** 2 P0[n: n + n1, n: n + n1] = gyro_model.P P0[n + n1: n + n1 + n2, n + n1: n + n1 + n2] = accel_model.P self.P0 = P0 s = 0 s1 = gyro_model.n_noises s2 = accel_model.n_noises if gyro_model.noise is not None: q[:3] = gyro_model.noise s += 3 if accel_model.noise is not None: q[s: s + 3] = accel_model.noise s += 3 q[s: s + s1] = gyro_model.q q[s + s1: s + s1 + s2] = accel_model.q self.q = q states = OrderedDict(( ('DR1', DR1), ('DR2', DR2), ('DV1', DV1), ('DV2', DV2), ('PHI1', PHI1), ('PHI2', PHI2), ('PSI3', PSI3) )) for name, state in gyro_model.states.items(): states['GYRO_' + name] = n + state for name, state in accel_model.states.items(): states['ACCEL_' + name] = n + n1 + state self.dt = dt self.n_states = n_states self.n_noises = n_noises self.states = states self.gyro_model = gyro_model self.accel_model = accel_model def _validate_parameters(self, integrator, theta, dv, observations, gain_factor, max_step, record_stamps, feedback_period): if gain_factor is not None: gain_factor = np.asarray(gain_factor) if gain_factor.shape != (self.n_states,): raise ValueError("`gain_factor` is expected to have shape {}, " "but actually has {}." .format((self.n_states,), gain_factor.shape)) if np.any(gain_factor < 0): raise ValueError("`gain_factor` must contain positive values.") stamps = pd.Index([]) for obs in observations: stamps = stamps.union(obs.data.index) integrator.reset() n_readings = theta.shape[0] initial_stamp = integrator.traj.index[-1] start = initial_stamp end = start + n_readings if record_stamps is not None: end = min(end, record_stamps[-1]) n_readings = end - start record_stamps = record_stamps[(record_stamps >= start) & (record_stamps <= end)] theta = theta[:n_readings] dv = dv[:n_readings] stamps = stamps.union(pd.Index([start, end])) feedback_period = max(1, int(np.floor(feedback_period / self.dt))) stamps = stamps.union( pd.Index(np.arange(0, n_readings, feedback_period) + initial_stamp)) if record_stamps is not None: stamps = stamps.union(pd.Index(record_stamps)) stamps = stamps[(stamps >= start) & (stamps <= end)] max_step = max(1, int(np.floor(max_step / self.dt))) stamps = _refine_stamps(stamps, max_step) if record_stamps is None: record_stamps = stamps return (theta, dv, observations, stamps, record_stamps, gain_factor, feedback_period) def _forward_pass(self, integrator, theta, dv, observations, gain_factor, stamps, record_stamps, feedback_period, data_for_backward=False): start = integrator.traj.index[0] if data_for_backward: n_stamps = stamps.shape[0] x = np.empty((n_stamps, self.n_states)) P = np.empty((n_stamps, self.n_states, self.n_states)) xa = x.copy() Pa = P.copy() Phi_arr = np.empty((n_stamps - 1, self.n_states, self.n_states)) record_stamps = stamps else: n_stamps = record_stamps.shape[0] x = np.empty((n_stamps, self.n_states)) P = np.empty((n_stamps, self.n_states, self.n_states)) xa = None Pa = None Phi_arr = None xc = np.zeros(self.n_states) Pc = self.P0.copy() H_max = np.zeros((10, self.n_states)) i_reading = 0 # Number of processed readings. i_stamp = 0 # Index of current stamp in stamps array. # Index of current position in x and P arrays for saving xc and Pc. i_save = 0 n = N_BASE_STATES n1 = self.gyro_model.n_states n2 = self.accel_model.n_states if self.gyro_model.scale is not None: gyro = theta / self.dt gyro = np.vstack((gyro, 2 * gyro[-1] - gyro[-2])) else: gyro = None if self.accel_model.scale is not None: accel = dv / self.dt accel = np.vstack((accel, 2 * accel[-1] - accel[-2])) else: accel = None H_gyro = np.atleast_2d(self.gyro_model.output_matrix(gyro)) H_accel = np.atleast_2d(self.accel_model.output_matrix(accel)) F = np.zeros((self.n_states, self.n_states)) F[n: n + n1, n: n + n1] = self.gyro_model.F F[n + n1:n + n1 + n2, n + n1: n + n1 + n2] = self.accel_model.F F1 = F F2 = F.copy() s = 0 s1 = self.gyro_model.n_noises s2 = self.accel_model.n_noises if self.gyro_model.noise is not None: s += 3 if self.accel_model.noise is not None: s += 3 G = np.zeros((self.n_states, self.n_noises)) G[n: n + n1, s: s + s1] = self.gyro_model.G G[n + n1: n + n1 + n2, s + s1: s + s1 + s2] = self.accel_model.G G1 = G G2 = G.copy() obs_stamps = [[] for _ in range(len(observations))] obs_residuals = [[] for _ in range(len(observations))] n_readings = theta.shape[0] while i_reading < n_readings: theta_b = theta[i_reading: i_reading + feedback_period] dv_b = dv[i_reading: i_reading + feedback_period] traj_b = integrator.integrate(theta_b, dv_b) Fi, Fig, Fia = _error_model_matrices(traj_b) i = 0 while i < theta_b.shape[0]: stamp = stamps[i_stamp] stamp_next = stamps[i_stamp + 1] delta_i = stamp_next - stamp i_next = i + delta_i if data_for_backward and record_stamps[i_save] == stamp: xa[i_save] = xc Pa[i_save] = Pc for i_obs, obs in enumerate(observations): ret = obs.compute_obs(stamp, traj_b.iloc[i]) if ret is not None: z, H, R = ret H_max[:H.shape[0], :N_BASE_STATES] = H res = _kalman_correct(xc, Pc, z, H_max[:H.shape[0]], R, gain_factor, obs.gain_curve) obs_stamps[i_obs].append(stamp) obs_residuals[i_obs].append(res) if record_stamps[i_save] == stamp: x[i_save] = xc P[i_save] = Pc i_save += 1 dt = self.dt * delta_i F1[:n, :n] = Fi[i] F2[:n, :n] = Fi[i_next] if H_gyro.ndim == 2: H_gyro_i = H_gyro H_gyro_i_next = H_gyro else: H_gyro_i = H_gyro[stamp - start] H_gyro_i_next = H_gyro[stamp_next - start] if H_accel.ndim == 2: H_accel_i = H_accel H_accel_i_next = H_accel else: H_accel_i = H_accel[stamp - start] H_accel_i_next = H_accel[stamp_next - start] F1[:n, n: n + n1] = Fig[i].dot(H_gyro_i) F2[:n, n: n + n1] = Fig[i_next].dot(H_gyro_i_next) F1[:n, n + n1: n + n1 + n2] = Fia[i].dot(H_accel_i) F2[:n, n + n1: n + n1 + n2] = Fia[i_next].dot(H_accel_i_next) s = 0 if self.gyro_model.noise is not None: G1[:n, :3] = Fig[i] G2[:n, :3] = Fig[i_next] s += 3 if self.accel_model.noise is not None: G1[:n, s: s + 3] = Fia[i] G2[:n, s: s + 3] = Fia[i_next] Phi = 0.5 * (F1 + F2) * dt Phi[np.diag_indices_from(Phi)] += 1 Qd = 0.5 * (G1 + G2) Qd *= self.q Qd = np.dot(Qd, Qd.T) * dt xc = Phi.dot(xc) Pc = Phi.dot(Pc).dot(Phi.T) + Qd if data_for_backward: Phi_arr[i_save - 1] = Phi i = i_next i_stamp += 1 i_reading += feedback_period integrator._correct(xc[:N_BASE_STATES]) xc[:N_BASE_STATES] = 0 if record_stamps[i_save] == stamps[i_stamp]: x[i_save] = xc P[i_save] = Pc if data_for_backward: xa[i_save] = xc Pa[i_save] = Pc residuals = [] for s, r in zip(obs_stamps, obs_residuals): residuals.append(pd.DataFrame(index=s, data=np.asarray(r))) return x, P, xa, Pa, Phi_arr, residuals def run(self, integrator, theta, dv, observations=[], gain_factor=None, max_step=1, feedback_period=500, record_stamps=None): """Run the filter. Parameters ---------- integrator : `pyins.integrate.Integrator` instance Integrator to use for INS state propagation. It will be reset before the filter start. theta, dv : ndarray, shape (n_readings, 3) Rotation vectors and velocity increments computed from gyro and accelerometer readings after applying coning and sculling corrections. observations : list of `Observation` Measurements which will be processed. Empty by default. gain_factor : array_like with shape (n_states,) or None, optional Factor for Kalman gain for each filter's state. It might be beneficial in some practical situations to set factors less than 1 in order to decrease influence of measurements on some states. Setting values higher than 1 is unlikely to be reasonable. If None (default), use standard optimal Kalman gain. max_step : float, optional Maximum allowed time step. Default is 1 second. Set to 0 if you desire the smallest possible step. feedback_period : float Time after which INS state will be corrected by the estimated errors. Default is 500 seconds. record_stamps : array_like or None At which stamps record estimated errors. If None (default), errors will be saved at each stamp used internally in the filter. Returns ------- Bunch object with the fields listed below. Note that all data frames contain stamps only presented in `record_stamps`. traj : DataFrame Trajectory corrected by estimated errors. It will only contain stamps presented in `record_stamps`. sd : DataFrame Estimated standard deviations of trajectory errors. gyro_err, gyro_sd : DataFrame Estimated gyro error states and their standard deviations. accel_err, accel_sd : DataFrame Estimated accelerometer error states and their standard deviations. P : ndarray, shape (n_points, n_states, n_states) History of the filter covariance. residuals : list of DataFrame Each DataFrame corresponds to an observation from `observations`. Its index is observation time stamps and columns contain normalized observations residuals for each component of the observation vector `z`. Notes ----- Estimated trajectory errors and a history of the filter states are not returned because they are computed relative to partially corrected trajectory and are not useful for interpretation. """ (theta, dv, observations, stamps, record_stamps, gain_factor, feedback_period) = \ self._validate_parameters(integrator, theta, dv, observations, gain_factor, max_step, record_stamps, feedback_period) x, P, _, _, _, residuals = \ self._forward_pass(integrator, theta, dv, observations, gain_factor, stamps, record_stamps, feedback_period) traj = integrator.traj.loc[record_stamps] err, sd, accel_err, accel_sd, gyro_err, gyro_sd = \ _compute_output_errors(traj, x, P, record_stamps, self.gyro_model, self.accel_model) traj_corr = correct_traj(integrator.traj, err) return FiltResult(traj=traj_corr, sd=sd, gyro_err=gyro_err, gyro_sd=gyro_sd, accel_err=accel_err, accel_sd=accel_sd, P=P, residuals=residuals) def run_smoother(self, integrator, theta, dv, observations=[], gain_factor=None, max_step=1, feedback_period=500, record_stamps=None): """Run the smoother. It means that observations during the whole time is used to estimate the errors at each moment of time (i.e. it is not real time). The Rauch-Tung-Striebel two pass recursion is used [1]_. Parameters ---------- integrator : `pyins.integrate.Integrator` instance Integrator to use for INS state propagation. It will be reset before the filter start. theta, dv : ndarray, shape (n_readings, 3) Rotation vectors and velocity increments computed from gyro and accelerometer readings after applying coning and sculling corrections. observations : list of `Observation` Measurements which will be processed. Empty by default. gain_factor : array_like with shape (n_states,) or None, optional Factor for Kalman gain for each filter's state. It might be beneficial in some practical situations to set factors less than 1 in order to decrease influence of measurements on some states. Setting values higher than 1 is unlikely to be reasonable. If None (default), use standard optimal Kalman gain. max_step : float, optional Maximum allowed time step. Default is 1 second. Set to 0 if you desire the smallest possible step. feedback_period : float Time after which INS state will be corrected by the estimated errors. Default is 500 seconds. record_stamps : array_like or None At which stamps record estimated errors. If None (default), errors will be saved at each stamp used internally in the filter. Returns ------- Bunch object with the fields listed below. Note that all data frames contain stamps only presented in `record_stamps`. traj : DataFrame Trajectory corrected by estimated errors. It will only contain stamps presented in `record_stamps`. sd : DataFrame Estimated trajectory errors and their standard deviations. gyro_err, gyro_sd : DataFrame Estimated gyro error states and their standard deviations. accel_err, accel_sd : DataFrame Estimated accelerometer error states and their standard deviations. P : ndarray, shape (n_points, n_states, n_states) History of the filter covariance. residuals : list of DataFrame Each DataFrame corresponds to an observation from `observations`. Its index is observation time stamps and columns contain normalized observations residuals for each component of the observation vector `z`. Notes ----- Estimated trajectory errors and a history of the filter states are not returned because they are computed relative to partially corrected trajectory and are not useful for interpretation. """ (theta, dv, observations, stamps, record_stamps, gain_factor, feedback_period) = \ self._validate_parameters(integrator, theta, dv, observations, gain_factor, max_step, record_stamps, feedback_period) x, P, xa, Pa, Phi_arr, residuals = \ self._forward_pass(integrator, theta, dv, observations, gain_factor, stamps, record_stamps, feedback_period, data_for_backward=True) traj = integrator.traj.loc[record_stamps] err, sd, gyro_err, gyro_sd, accel_err, accel_sd = \ _compute_output_errors(traj, x, P, record_stamps, self.gyro_model, self.accel_model) traj = correct_traj(traj, err) xa[:, :N_BASE_STATES] -= x[:, :N_BASE_STATES] x[:, :N_BASE_STATES] = 0 x, P = _rts_pass(x, P, xa, Pa, Phi_arr) ind = np.searchsorted(stamps, record_stamps) x = x[ind] P = P[ind] traj = traj.iloc[ind] err, sd, gyro_err, gyro_sd, accel_err, accel_sd = \ _compute_output_errors(traj, x, P, record_stamps[ind], self.gyro_model, self.accel_model) traj = correct_traj(traj, err) return FiltResult(traj=traj, sd=sd, gyro_err=gyro_err, gyro_sd=gyro_sd, accel_err=accel_err, accel_sd=accel_sd, P=P, residuals=residuals) def traj_diff(t1, t2): """Compute trajectory difference. Parameters ---------- t1, t2 : DataFrame Trajectories. Returns ------- diff : DataFrame Trajectory difference. It can be interpreted as errors in `t1` relative to `t2`. """ diff = t1 - t2 diff['lat'] *= np.deg2rad(earth.R0) diff['lon'] *= np.deg2rad(earth.R0) * np.cos(0.5 * np.deg2rad(t1.lat + t2.lat)) diff['h'] %= 360 diff.h[diff.h < -180] += 360 diff.h[diff.h > 180] -= 360 return diff.loc[t1.index.intersection(t2.index)] def correct_traj(traj, error): """Correct trajectory by estimated errors. Note that it means subtracting errors from the trajectory. Parameters ---------- traj : DataFrame Trajectory. error : DataFrame Estimated errors. Returns ------- traj_corr : DataFrame Corrected trajectory. """ traj_corr = traj.copy() traj_corr['lat'] -= np.rad2deg(error.lat / earth.R0) traj_corr['lon'] -= np.rad2deg(error.lon / (earth.R0 * np.cos(np.deg2rad(traj_corr['lat'])))) traj_corr['VE'] -= error.VE traj_corr['VN'] -= error.VN traj_corr['h'] -= error.h traj_corr['p'] -= error.p traj_corr['r'] -= error.r return traj_corr.dropna()