use crate::linalg::{matrix_square_root, robust_spd_solve, symmetrize};
use crate::measurements::MeasurementModel;
use crate::{
IMUData, NavigationFilter, StrapdownState, forward, wrap_to_2pi, wrap_to_180, wrap_to_360,
};
use std::fmt::{self, Debug, Display};
use nalgebra::{DMatrix, DVector, Rotation3, UnitQuaternion, Vector3};
#[derive(Clone, Debug, Default)]
pub struct InitialState {
pub latitude: f64,
pub longitude: f64,
pub altitude: f64,
pub northward_velocity: f64,
pub eastward_velocity: f64,
pub vertical_velocity: f64,
pub roll: f64,
pub pitch: f64,
pub yaw: f64,
pub in_degrees: bool,
pub is_enu: bool,
}
impl InitialState {
#[allow(clippy::too_many_arguments)]
pub fn new(
latitude: f64,
longitude: f64,
altitude: f64,
northward_velocity: f64,
eastward_velocity: f64,
vertical_velocity: f64,
mut roll: f64,
mut pitch: f64,
mut yaw: f64,
in_degrees: bool,
is_enu: Option<bool>,
) -> Self {
let latitude = if in_degrees {
latitude
} else {
latitude.to_degrees()
};
let longitude = if in_degrees {
wrap_to_180(longitude)
} else {
longitude
};
let is_enu = is_enu.unwrap_or(true);
if in_degrees {
roll = wrap_to_360(roll).to_radians();
pitch = wrap_to_360(pitch).to_radians();
yaw = wrap_to_360(yaw).to_radians();
} else {
roll = wrap_to_2pi(roll);
pitch = wrap_to_2pi(pitch);
yaw = wrap_to_2pi(yaw);
}
InitialState {
latitude,
longitude,
altitude,
northward_velocity,
eastward_velocity,
vertical_velocity,
roll,
pitch,
yaw,
in_degrees,
is_enu,
}
}
}
#[derive(Clone)]
pub struct UnscentedKalmanFilter {
mean_state: DVector<f64>,
covariance: DMatrix<f64>,
process_noise: DMatrix<f64>,
lambda: f64,
state_size: usize,
weights_mean: DVector<f64>,
weights_cov: DVector<f64>,
is_enu: bool,
}
impl Debug for UnscentedKalmanFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UKF")
.field("mean_state", &self.mean_state)
.field("covariance", &self.covariance)
.field("process_noise", &self.process_noise)
.field("lambda", &self.lambda)
.field("state_size", &self.state_size)
.finish()
}
}
impl Display for UnscentedKalmanFilter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("UnscentedKalmanFilter")
.field("mean_state", &self.mean_state)
.field("covariance", &self.covariance)
.field("process_noise", &self.process_noise)
.field("lambda", &self.lambda)
.field("state_size", &self.state_size)
.finish()
}
}
impl UnscentedKalmanFilter {
#[allow(clippy::too_many_arguments)]
pub fn new(
initial_state: InitialState,
imu_biases: Vec<f64>,
other_states: Option<Vec<f64>>,
covariance_diagonal: Vec<f64>,
process_noise: DMatrix<f64>,
alpha: f64,
beta: f64,
kappa: f64,
) -> UnscentedKalmanFilter {
let mut mean = if initial_state.in_degrees {
vec![
initial_state.latitude.to_radians(),
initial_state.longitude.to_radians(),
initial_state.altitude,
initial_state.northward_velocity,
initial_state.eastward_velocity,
initial_state.vertical_velocity,
initial_state.roll,
initial_state.pitch,
initial_state.yaw,
]
} else {
vec![
initial_state.latitude,
initial_state.longitude,
initial_state.altitude,
initial_state.northward_velocity,
initial_state.eastward_velocity,
initial_state.vertical_velocity,
initial_state.roll,
initial_state.pitch,
initial_state.yaw,
]
};
mean.extend(imu_biases);
if let Some(ref other_states) = other_states {
mean.extend(other_states.iter().cloned());
}
let state_size = mean.len();
let mean_state = DVector::from_vec(mean);
let covariance = DMatrix::<f64>::from_diagonal(&DVector::from_vec(covariance_diagonal));
let lambda = alpha * alpha * (state_size as f64 + kappa) - state_size as f64;
let mut weights_mean = DVector::zeros(2 * state_size + 1);
let mut weights_cov = DVector::zeros(2 * state_size + 1);
weights_mean[0] = lambda / (state_size as f64 + lambda);
weights_cov[0] = lambda / (state_size as f64 + lambda) + (1.0 - alpha * alpha + beta);
for i in 1..(2 * state_size + 1) {
let w = 1.0 / (2.0 * (state_size as f64 + lambda));
weights_mean[i] = w;
weights_cov[i] = w;
}
UnscentedKalmanFilter {
mean_state,
covariance,
process_noise,
lambda,
state_size,
weights_mean,
weights_cov,
is_enu: initial_state.is_enu,
}
}
pub fn get_sigma_points(&self) -> DMatrix<f64> {
let p = (self.state_size as f64 + self.lambda) * self.covariance.clone();
let sqrt_p = matrix_square_root(&p);
let mu = self.mean_state.clone();
let mut pts = DMatrix::<f64>::zeros(self.state_size, 2 * self.state_size + 1);
pts.column_mut(0).copy_from(&mu);
for i in 0..sqrt_p.ncols() {
pts.column_mut(i + 1).copy_from(&(&mu + sqrt_p.column(i)));
pts.column_mut(i + 1 + self.state_size)
.copy_from(&(&mu - sqrt_p.column(i)));
}
pts
}
fn robust_kalman_gain(
&mut self,
cross_covariance: &DMatrix<f64>,
s: &DMatrix<f64>,
) -> DMatrix<f64> {
let kt = robust_spd_solve(&symmetrize(s), &cross_covariance.transpose());
kt.transpose()
}
}
impl NavigationFilter for UnscentedKalmanFilter {
fn predict<C: crate::InputModel>(&mut self, control_input: &C, dt: f64) {
let imu_input = control_input
.as_any()
.downcast_ref::<IMUData>()
.expect("UnscentedKalmanFilter.predict expects an IMUData InputModel");
let mut sigma_points = self.get_sigma_points();
for i in 0..sigma_points.ncols() {
let mut sigma_point_vec = sigma_points.column(i).clone_owned();
let mut state = StrapdownState {
latitude: sigma_point_vec[0],
longitude: sigma_point_vec[1],
altitude: sigma_point_vec[2],
velocity_north: sigma_point_vec[3],
velocity_east: sigma_point_vec[4],
velocity_vertical: sigma_point_vec[5],
attitude: Rotation3::from_euler_angles(
sigma_point_vec[6],
sigma_point_vec[7],
sigma_point_vec[8],
),
is_enu: self.is_enu,
};
let accel_biases = if self.state_size >= 15 {
DVector::from_vec(vec![
sigma_point_vec[9],
sigma_point_vec[10],
sigma_point_vec[11],
])
} else {
DVector::from_vec(vec![0.0, 0.0, 0.0])
};
let gyro_biases = if self.state_size >= 15 {
DVector::from_vec(vec![
sigma_point_vec[12],
sigma_point_vec[13],
sigma_point_vec[14],
])
} else {
DVector::from_vec(vec![0.0, 0.0, 0.0])
};
let imu_data = IMUData {
accel: imu_input.accel - &accel_biases,
gyro: imu_input.gyro - &gyro_biases,
};
forward(&mut state, imu_data, dt);
sigma_point_vec[0] = state.latitude;
sigma_point_vec[1] = state.longitude;
sigma_point_vec[2] = state.altitude;
sigma_point_vec[3] = state.velocity_north;
sigma_point_vec[4] = state.velocity_east;
sigma_point_vec[5] = state.velocity_vertical;
sigma_point_vec[6] = state.attitude.euler_angles().0;
sigma_point_vec[7] = state.attitude.euler_angles().1;
sigma_point_vec[8] = state.attitude.euler_angles().2;
sigma_points.set_column(i, &sigma_point_vec);
}
let mut mu_bar = DVector::<f64>::zeros(self.state_size);
for (i, sigma_point) in sigma_points.column_iter().enumerate() {
mu_bar += self.weights_mean[i] * sigma_point;
}
let mut p_bar = DMatrix::<f64>::zeros(self.state_size, self.state_size);
for (i, sigma_point) in sigma_points.column_iter().enumerate() {
let diff = sigma_point - &mu_bar;
p_bar += self.weights_cov[i] * &diff * &diff.transpose();
}
p_bar += &self.process_noise;
self.mean_state = mu_bar;
self.covariance = symmetrize(&p_bar);
}
fn update<M: MeasurementModel + ?Sized>(&mut self, measurement: &M) {
let mut measurement_sigma_points =
DMatrix::<f64>::zeros(measurement.get_dimension(), 2 * self.state_size + 1);
let mut z_hat = DVector::<f64>::zeros(measurement.get_dimension());
for (i, sigma_point) in self.get_sigma_points().column_iter().enumerate() {
let sigma_point = measurement.get_expected_measurement(&sigma_point.clone_owned());
measurement_sigma_points.set_column(i, &sigma_point);
z_hat += self.weights_mean[i] * sigma_point;
}
let mut s = DMatrix::<f64>::zeros(measurement.get_dimension(), measurement.get_dimension());
for (i, sigma_point) in measurement_sigma_points.column_iter().enumerate() {
let diff = sigma_point - &z_hat;
s += self.weights_cov[i] * &diff * &diff.transpose();
}
s += measurement.get_noise();
let sigma_points = self.get_sigma_points();
let mut cross_covariance =
DMatrix::<f64>::zeros(self.state_size, measurement.get_dimension());
for (i, measurement_sigma_point) in measurement_sigma_points.column_iter().enumerate() {
let measurement_diff = measurement_sigma_point - &z_hat;
let state_diff = sigma_points.column(i) - &self.mean_state;
cross_covariance += self.weights_cov[i] * state_diff * measurement_diff.transpose();
}
let k = self.robust_kalman_gain(&cross_covariance, &s);
self.mean_state += &k * (measurement.get_measurement(&self.mean_state) - &z_hat);
self.mean_state[6] = wrap_to_2pi(self.mean_state[6]);
self.mean_state[7] = wrap_to_2pi(self.mean_state[7]);
self.mean_state[8] = wrap_to_2pi(self.mean_state[8]);
self.covariance -= &k * &s * &k.transpose();
self.covariance = symmetrize(&self.covariance);
let eps = 1e-9;
for i in 0..self.state_size {
self.covariance[(i, i)] += eps;
}
}
fn get_estimate(&self) -> DVector<f64> {
self.mean_state.clone()
}
fn get_certainty(&self) -> DMatrix<f64> {
self.covariance.clone()
}
}
#[derive(Clone)]
pub struct ExtendedKalmanFilter {
mean_state: DVector<f64>,
covariance: DMatrix<f64>,
process_noise: DMatrix<f64>,
state_size: usize,
use_biases: bool,
is_enu: bool,
}
impl Debug for ExtendedKalmanFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EKF")
.field("mean_state", &self.mean_state)
.field("covariance", &self.covariance)
.field("process_noise", &self.process_noise)
.field("state_size", &self.state_size)
.field("use_biases", &self.use_biases)
.field("is_enu", &self.is_enu)
.finish()
}
}
impl Display for ExtendedKalmanFilter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ExtendedKalmanFilter")
.field("mean_state", &self.mean_state)
.field("covariance", &self.covariance)
.field("process_noise", &self.process_noise)
.field("state_size", &self.state_size)
.field("use_biases", &self.use_biases)
.field("is_enu", &self.is_enu)
.finish()
}
}
impl ExtendedKalmanFilter {
pub fn new(
initial_state: InitialState,
imu_biases: Vec<f64>,
covariance_diagonal: Vec<f64>,
process_noise: DMatrix<f64>,
use_biases: bool,
) -> ExtendedKalmanFilter {
let mut mean = if initial_state.in_degrees {
vec![
initial_state.latitude.to_radians(),
initial_state.longitude.to_radians(),
initial_state.altitude,
initial_state.northward_velocity,
initial_state.eastward_velocity,
initial_state.vertical_velocity,
initial_state.roll,
initial_state.pitch,
initial_state.yaw,
]
} else {
vec![
initial_state.latitude,
initial_state.longitude,
initial_state.altitude,
initial_state.northward_velocity,
initial_state.eastward_velocity,
initial_state.vertical_velocity,
initial_state.roll,
initial_state.pitch,
initial_state.yaw,
]
};
if use_biases {
mean.extend(imu_biases);
}
let expected_state_size = covariance_diagonal.len();
while mean.len() < expected_state_size {
mean.push(0.0);
}
let state_size = mean.len();
let mean_state = DVector::from_vec(mean);
let covariance = DMatrix::<f64>::from_diagonal(&DVector::from_vec(covariance_diagonal));
ExtendedKalmanFilter {
mean_state,
covariance,
process_noise,
state_size,
use_biases,
is_enu: initial_state.is_enu,
}
}
}
impl NavigationFilter for ExtendedKalmanFilter {
fn predict<C: crate::InputModel>(&mut self, control_input: &C, dt: f64) {
let imu_data = control_input
.as_any()
.downcast_ref::<IMUData>()
.expect("ExtendedKalmanFilter.predict expects an IMUData InputModel");
let mut state = StrapdownState {
latitude: self.mean_state[0],
longitude: self.mean_state[1],
altitude: self.mean_state[2],
velocity_north: self.mean_state[3],
velocity_east: self.mean_state[4],
velocity_vertical: self.mean_state[5],
attitude: Rotation3::from_euler_angles(
self.mean_state[6],
self.mean_state[7],
self.mean_state[8],
),
is_enu: self.is_enu,
};
let (accel_biases, gyro_biases) = if self.use_biases && self.state_size >= 15 {
(
DVector::from_vec(vec![
self.mean_state[9],
self.mean_state[10],
self.mean_state[11],
]),
DVector::from_vec(vec![
self.mean_state[12],
self.mean_state[13],
self.mean_state[14],
]),
)
} else {
(
DVector::from_vec(vec![0.0, 0.0, 0.0]),
DVector::from_vec(vec![0.0, 0.0, 0.0]),
)
};
let corrected_imu = IMUData {
accel: imu_data.accel - &accel_biases,
gyro: imu_data.gyro - &gyro_biases,
};
let f_matrix = crate::linearize::state_transition_jacobian(
&state,
&corrected_imu.accel,
&corrected_imu.gyro,
dt,
);
let f_full = if self.use_biases && self.state_size >= 15 {
let mut f_ext = DMatrix::<f64>::identity(self.state_size, self.state_size);
f_ext.view_mut((0, 0), (9, 9)).copy_from(&f_matrix);
f_ext
} else if self.state_size > 9 {
let mut f_ext = DMatrix::<f64>::identity(self.state_size, self.state_size);
f_ext.view_mut((0, 0), (9, 9)).copy_from(&f_matrix);
f_ext
} else {
f_matrix
};
forward(&mut state, corrected_imu, dt);
self.mean_state[0] = state.latitude;
self.mean_state[1] = state.longitude;
self.mean_state[2] = state.altitude;
self.mean_state[3] = state.velocity_north;
self.mean_state[4] = state.velocity_east;
self.mean_state[5] = state.velocity_vertical;
self.mean_state[6] = state.attitude.euler_angles().0;
self.mean_state[7] = state.attitude.euler_angles().1;
self.mean_state[8] = state.attitude.euler_angles().2;
self.covariance = &f_full * &self.covariance * f_full.transpose() + &self.process_noise;
self.covariance = symmetrize(&self.covariance);
let eps = 1e-9;
for i in 0..self.state_size {
self.covariance[(i, i)] += eps;
}
}
fn update<M: MeasurementModel + ?Sized>(&mut self, measurement: &M) {
let z_hat = measurement.get_expected_measurement(&self.mean_state);
let h_9state = measurement.get_jacobian(&self.mean_state);
let h_matrix = if self.use_biases && self.state_size >= 15 {
let meas_dim = measurement.get_dimension();
let mut h_ext = DMatrix::<f64>::zeros(meas_dim, self.state_size);
h_ext.view_mut((0, 0), (meas_dim, 9)).copy_from(&h_9state);
h_ext
} else if self.state_size > 9 {
let meas_dim = measurement.get_dimension();
let mut h_ext = DMatrix::<f64>::zeros(meas_dim, self.state_size);
h_ext.view_mut((0, 0), (meas_dim, 9)).copy_from(&h_9state);
h_ext
} else {
h_9state
};
let s = &h_matrix * &self.covariance * h_matrix.transpose() + measurement.get_noise();
let k = self.covariance.clone()
* h_matrix.transpose()
* robust_spd_solve(&symmetrize(&s), &DMatrix::identity(s.nrows(), s.ncols()))
.transpose();
let innovation = measurement.get_measurement(&self.mean_state) - &z_hat;
self.mean_state += &k * innovation;
self.mean_state[6] = wrap_to_2pi(self.mean_state[6]);
self.mean_state[7] = wrap_to_2pi(self.mean_state[7]);
self.mean_state[8] = wrap_to_2pi(self.mean_state[8]);
let i_kh = DMatrix::identity(self.state_size, self.state_size) - &k * &h_matrix;
let r = measurement.get_noise();
self.covariance = &i_kh * &self.covariance * i_kh.transpose() + &k * r * k.transpose();
self.covariance = symmetrize(&self.covariance);
let eps = 1e-9;
for i in 0..self.state_size {
self.covariance[(i, i)] += eps;
}
}
fn get_estimate(&self) -> DVector<f64> {
self.mean_state.clone()
}
fn get_certainty(&self) -> DMatrix<f64> {
self.covariance.clone()
}
}
#[derive(Clone)]
pub struct ErrorStateKalmanFilter {
nominal_latitude: f64, nominal_longitude: f64, nominal_altitude: f64,
nominal_velocity_north: f64, nominal_velocity_east: f64, nominal_velocity_vertical: f64,
nominal_quaternion: nalgebra::Vector4<f64>,
nominal_accel_bias: Vector3<f64>, nominal_gyro_bias: Vector3<f64>,
error_state: DVector<f64>,
error_covariance: DMatrix<f64>,
process_noise: DMatrix<f64>,
is_enu: bool,
}
impl Debug for ErrorStateKalmanFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ESKF")
.field(
"nominal_position",
&[
self.nominal_latitude,
self.nominal_longitude,
self.nominal_altitude,
],
)
.field(
"nominal_velocity",
&[
self.nominal_velocity_north,
self.nominal_velocity_east,
self.nominal_velocity_vertical,
],
)
.field("nominal_quaternion", &self.nominal_quaternion)
.field("error_state", &self.error_state)
.field("error_covariance", &self.error_covariance)
.field("process_noise", &self.process_noise)
.field("is_enu", &self.is_enu)
.finish()
}
}
impl Display for ErrorStateKalmanFilter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ErrorStateKalmanFilter")
.field(
"nominal_position",
&[
self.nominal_latitude,
self.nominal_longitude,
self.nominal_altitude,
],
)
.field(
"nominal_velocity",
&[
self.nominal_velocity_north,
self.nominal_velocity_east,
self.nominal_velocity_vertical,
],
)
.field("nominal_quaternion", &self.nominal_quaternion)
.field("error_state", &self.error_state)
.field("error_covariance", &self.error_covariance)
.field("process_noise", &self.process_noise)
.field("is_enu", &self.is_enu)
.finish()
}
}
impl ErrorStateKalmanFilter {
pub fn new(
initial_state: InitialState,
imu_biases: Vec<f64>,
error_covariance_diagonal: Vec<f64>,
process_noise: DMatrix<f64>,
) -> ErrorStateKalmanFilter {
let (roll, pitch, yaw) = if initial_state.in_degrees {
(initial_state.roll, initial_state.pitch, initial_state.yaw)
} else {
(
initial_state.roll.to_degrees(),
initial_state.pitch.to_degrees(),
initial_state.yaw.to_degrees(),
)
};
let rotation = Rotation3::from_euler_angles(roll, pitch, yaw);
let unit_quat = UnitQuaternion::from_rotation_matrix(&rotation);
let nominal_quaternion =
nalgebra::Vector4::new(unit_quat.w, unit_quat.i, unit_quat.j, unit_quat.k);
let (nominal_latitude, nominal_longitude) = if initial_state.in_degrees {
(
initial_state.latitude.to_radians(),
initial_state.longitude.to_radians(),
)
} else {
(initial_state.latitude, initial_state.longitude)
};
let nominal_accel_bias = Vector3::new(imu_biases[0], imu_biases[1], imu_biases[2]);
let nominal_gyro_bias = Vector3::new(imu_biases[3], imu_biases[4], imu_biases[5]);
let error_state = DVector::zeros(15);
let error_covariance =
DMatrix::from_diagonal(&DVector::from_vec(error_covariance_diagonal));
ErrorStateKalmanFilter {
nominal_latitude,
nominal_longitude,
nominal_altitude: initial_state.altitude,
nominal_velocity_north: initial_state.northward_velocity,
nominal_velocity_east: initial_state.eastward_velocity,
nominal_velocity_vertical: initial_state.vertical_velocity,
nominal_quaternion,
nominal_accel_bias,
nominal_gyro_bias,
error_state,
error_covariance,
process_noise,
is_enu: initial_state.is_enu,
}
}
fn inject_error_state(&mut self) {
let lat_deg = self.nominal_latitude.to_degrees();
let (r_n, r_e, _r_p) = crate::earth::principal_radii(&lat_deg, &self.nominal_altitude);
self.nominal_latitude += self.error_state[0] / r_n;
self.nominal_longitude += self.error_state[1] / (r_e * self.nominal_latitude.cos());
self.nominal_altitude += self.error_state[2];
self.nominal_velocity_north += self.error_state[3];
self.nominal_velocity_east += self.error_state[4];
self.nominal_velocity_vertical += self.error_state[5];
let delta_theta = Vector3::new(
self.error_state[6],
self.error_state[7],
self.error_state[8],
);
let delta_q = nalgebra::Vector4::new(
1.0,
delta_theta[0] * 0.5,
delta_theta[1] * 0.5,
delta_theta[2] * 0.5,
);
let w = self.nominal_quaternion[0];
let x = self.nominal_quaternion[1];
let y = self.nominal_quaternion[2];
let z = self.nominal_quaternion[3];
let dw = delta_q[0];
let dx = delta_q[1];
let dy = delta_q[2];
let dz = delta_q[3];
self.nominal_quaternion[0] = w * dw - x * dx - y * dy - z * dz;
self.nominal_quaternion[1] = w * dx + x * dw + y * dz - z * dy;
self.nominal_quaternion[2] = w * dy - x * dz + y * dw + z * dx;
self.nominal_quaternion[3] = w * dz + x * dy - y * dx + z * dw;
let norm = self.nominal_quaternion.norm();
self.nominal_quaternion /= norm;
self.nominal_accel_bias[0] += self.error_state[9];
self.nominal_accel_bias[1] += self.error_state[10];
self.nominal_accel_bias[2] += self.error_state[11];
self.nominal_gyro_bias[0] += self.error_state[12];
self.nominal_gyro_bias[1] += self.error_state[13];
self.nominal_gyro_bias[2] += self.error_state[14];
self.error_state.fill(0.0);
}
}
impl NavigationFilter for ErrorStateKalmanFilter {
fn predict<C: crate::InputModel>(&mut self, control_input: &C, dt: f64) {
let imu_data = control_input
.as_any()
.downcast_ref::<IMUData>()
.expect("ErrorStateKalmanFilter.predict expects an IMUData InputModel");
let corrected_accel = imu_data.accel - self.nominal_accel_bias;
let corrected_gyro = imu_data.gyro - self.nominal_gyro_bias;
let qw = self.nominal_quaternion[0];
let qx = self.nominal_quaternion[1];
let qy = self.nominal_quaternion[2];
let qz = self.nominal_quaternion[3];
let unit_quat = UnitQuaternion::from_quaternion(nalgebra::Quaternion::new(qw, qx, qy, qz));
let rotation =
Rotation3::from_matrix_unchecked(unit_quat.to_rotation_matrix().into_inner());
let mut nominal_state = StrapdownState {
latitude: self.nominal_latitude,
longitude: self.nominal_longitude,
altitude: self.nominal_altitude,
velocity_north: self.nominal_velocity_north,
velocity_east: self.nominal_velocity_east,
velocity_vertical: self.nominal_velocity_vertical,
attitude: rotation,
is_enu: self.is_enu,
};
let corrected_imu = IMUData {
accel: corrected_accel,
gyro: corrected_gyro,
};
forward(&mut nominal_state, corrected_imu, dt);
self.nominal_latitude = nominal_state.latitude;
self.nominal_longitude = nominal_state.longitude;
self.nominal_altitude = nominal_state.altitude;
self.nominal_velocity_north = nominal_state.velocity_north;
self.nominal_velocity_east = nominal_state.velocity_east;
self.nominal_velocity_vertical = nominal_state.velocity_vertical;
let unit_quat = UnitQuaternion::from_rotation_matrix(&nominal_state.attitude);
self.nominal_quaternion =
nalgebra::Vector4::new(unit_quat.w, unit_quat.i, unit_quat.j, unit_quat.k);
let norm = self.nominal_quaternion.norm();
self.nominal_quaternion /= norm;
let f_error = crate::linearize::error_state_transition_jacobian(
&nominal_state,
&corrected_accel,
&corrected_gyro,
dt,
);
self.error_covariance =
&f_error * &self.error_covariance * f_error.transpose() + &self.process_noise;
self.error_covariance = symmetrize(&self.error_covariance);
let eps = 1e-9;
for i in 0..15 {
self.error_covariance[(i, i)] += eps;
}
}
fn update<M: MeasurementModel + ?Sized>(&mut self, measurement: &M) {
let mut nominal_state_vec = DVector::zeros(9);
nominal_state_vec[0] = self.nominal_latitude;
nominal_state_vec[1] = self.nominal_longitude;
nominal_state_vec[2] = self.nominal_altitude;
nominal_state_vec[3] = self.nominal_velocity_north;
nominal_state_vec[4] = self.nominal_velocity_east;
nominal_state_vec[5] = self.nominal_velocity_vertical;
let quat = nalgebra::UnitQuaternion::from_quaternion(nalgebra::Quaternion::new(
self.nominal_quaternion[0],
self.nominal_quaternion[1],
self.nominal_quaternion[2],
self.nominal_quaternion[3],
));
let euler = quat.euler_angles();
nominal_state_vec[6] = euler.0; nominal_state_vec[7] = euler.1; nominal_state_vec[8] = euler.2;
let z_hat = measurement.get_expected_measurement(&nominal_state_vec);
use crate::measurements::{
GPSPositionAndVelocityMeasurement, GPSPositionMeasurement, GPSVelocityMeasurement,
MagnetometerYawMeasurement, RelativeAltitudeMeasurement,
};
let qw = self.nominal_quaternion[0];
let qx = self.nominal_quaternion[1];
let qy = self.nominal_quaternion[2];
let qz = self.nominal_quaternion[3];
let unit_quat = UnitQuaternion::from_quaternion(nalgebra::Quaternion::new(qw, qx, qy, qz));
let rotation =
Rotation3::from_matrix_unchecked(unit_quat.to_rotation_matrix().into_inner());
let nav_state = StrapdownState {
latitude: self.nominal_latitude,
longitude: self.nominal_longitude,
altitude: self.nominal_altitude,
velocity_north: self.nominal_velocity_north,
velocity_east: self.nominal_velocity_east,
velocity_vertical: self.nominal_velocity_vertical,
attitude: rotation,
is_enu: self.is_enu,
};
let h_9state = if measurement
.as_any()
.downcast_ref::<GPSPositionMeasurement>()
.is_some()
{
crate::linearize::gps_position_jacobian(&nav_state)
} else if measurement
.as_any()
.downcast_ref::<GPSVelocityMeasurement>()
.is_some()
{
crate::linearize::gps_velocity_jacobian(&nav_state)
} else if measurement
.as_any()
.downcast_ref::<GPSPositionAndVelocityMeasurement>()
.is_some()
{
crate::linearize::gps_position_velocity_jacobian(&nav_state)
} else if measurement
.as_any()
.downcast_ref::<RelativeAltitudeMeasurement>()
.is_some()
{
crate::linearize::relative_altitude_jacobian(&nav_state)
} else if let Some(mag_meas) = measurement
.as_any()
.downcast_ref::<MagnetometerYawMeasurement>()
{
crate::linearize::magnetometer_yaw_jacobian(
&nav_state,
mag_meas.mag_x,
mag_meas.mag_y,
mag_meas.mag_z,
)
} else {
crate::linearize::gps_position_jacobian(&nav_state)
};
let meas_dim = measurement.get_dimension();
let mut h_error = DMatrix::<f64>::zeros(meas_dim, 15);
h_error.view_mut((0, 0), (meas_dim, 9)).copy_from(&h_9state);
let s = &h_error * &self.error_covariance * h_error.transpose() + measurement.get_noise();
let k = self.error_covariance.clone()
* h_error.transpose()
* robust_spd_solve(&symmetrize(&s), &DMatrix::identity(s.nrows(), s.ncols()))
.transpose();
let innovation = measurement.get_measurement(&nominal_state_vec) - &z_hat;
self.error_state = &k * innovation;
self.inject_error_state();
let i_kh = DMatrix::identity(15, 15) - &k * &h_error;
let r = measurement.get_noise();
self.error_covariance =
&i_kh * &self.error_covariance * i_kh.transpose() + &k * r * k.transpose();
self.error_covariance = symmetrize(&self.error_covariance);
let eps = 1e-9;
for i in 0..15 {
self.error_covariance[(i, i)] += eps;
}
}
fn get_estimate(&self) -> DVector<f64> {
let mut state = DVector::zeros(15);
state[0] = self.nominal_latitude;
state[1] = self.nominal_longitude;
state[2] = self.nominal_altitude;
state[3] = self.nominal_velocity_north;
state[4] = self.nominal_velocity_east;
state[5] = self.nominal_velocity_vertical;
let quat = nalgebra::UnitQuaternion::from_quaternion(nalgebra::Quaternion::new(
self.nominal_quaternion[0],
self.nominal_quaternion[1],
self.nominal_quaternion[2],
self.nominal_quaternion[3],
));
let euler = quat.euler_angles();
state[6] = wrap_to_2pi(euler.0); state[7] = wrap_to_2pi(euler.1); state[8] = wrap_to_2pi(euler.2);
state[9] = self.nominal_accel_bias[0];
state[10] = self.nominal_accel_bias[1];
state[11] = self.nominal_accel_bias[2];
state[12] = self.nominal_gyro_bias[0];
state[13] = self.nominal_gyro_bias[1];
state[14] = self.nominal_gyro_bias[2];
state
}
fn get_certainty(&self) -> DMatrix<f64> {
self.error_covariance.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::earth;
use crate::measurements::{
GPSPositionAndVelocityMeasurement, GPSPositionMeasurement, GPSVelocityMeasurement,
RelativeAltitudeMeasurement,
};
use assert_approx_eq::assert_approx_eq;
use nalgebra::Vector3;
const IMU_BIASES: [f64; 6] = [0.0; 6];
const N: usize = 15;
const COVARIANCE_DIAGONAL: [f64; N] = [1e-9; N];
const PROCESS_NOISE_DIAGONAL: [f64; N] = [1e-9; N];
const ALPHA: f64 = 1e-3;
const BETA: f64 = 2.0;
const KAPPA: f64 = 0.0;
const UKF_PARAMS: InitialState = InitialState {
latitude: 0.0,
longitude: 0.0,
altitude: 0.0,
northward_velocity: 0.0,
eastward_velocity: 0.0,
vertical_velocity: 0.0,
roll: 0.0,
pitch: 0.0,
yaw: 0.0,
in_degrees: false,
is_enu: true,
};
#[test]
fn ukf_construction() {
let measurement_bias = vec![0.0; 3]; let ukf = UnscentedKalmanFilter::new(
UKF_PARAMS,
IMU_BIASES.to_vec(),
Some(measurement_bias.clone()),
vec![1e-3; 18],
DMatrix::from_diagonal(&DVector::from_vec(vec![1e-3; 18])),
ALPHA,
BETA,
KAPPA,
);
assert_eq!(ukf.mean_state.len(), 18);
let wms = ukf.weights_mean;
let wcs = ukf.weights_cov;
assert_eq!(wms.len(), (2 * ukf.state_size) + 1);
assert_eq!(wcs.len(), (2 * ukf.state_size) + 1);
let lambda = ALPHA.powi(2) * (18.0 + KAPPA) - 18.0;
assert_eq!(lambda, ukf.lambda);
let wm_0 = lambda / (18.0 + lambda);
let wc_0 = wm_0 + (1.0 - ALPHA.powi(2)) + BETA;
let w_i = 1.0 / (2.0 * (18.0 + lambda));
assert_approx_eq!(wms[0], wm_0, 1e-6);
assert_approx_eq!(wcs[0], wc_0, 1e-6);
for i in 1..wms.len() {
assert_approx_eq!(wms[i], w_i, 1e-6);
assert_approx_eq!(wcs[i], w_i, 1e-6);
}
}
#[test]
fn ukf_get_sigma_points() {
let ukf = UnscentedKalmanFilter::new(
UKF_PARAMS,
IMU_BIASES.to_vec(),
None,
COVARIANCE_DIAGONAL.to_vec(),
DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
ALPHA,
BETA,
KAPPA,
);
let sigma_points = ukf.get_sigma_points();
assert_eq!(sigma_points.ncols(), (2 * ukf.state_size) + 1);
let mu = ukf.get_sigma_points() * ukf.weights_mean;
assert_eq!(mu.nrows(), ukf.state_size);
assert_eq!(mu.ncols(), 1);
assert_approx_eq!(mu[0], 0.0, 1e-6);
assert_approx_eq!(mu[1], 0.0, 1e-6);
assert_approx_eq!(mu[2], 0.0, 1e-6);
assert_approx_eq!(mu[3], 0.0, 1e-6);
assert_approx_eq!(mu[4], 0.0, 1e-6);
assert_approx_eq!(mu[5], 0.0, 1e-6);
assert_approx_eq!(mu[6], 0.0, 1e-6);
assert_approx_eq!(mu[7], 0.0, 1e-6);
assert_approx_eq!(mu[8], 0.0, 1e-6);
}
#[test]
fn ukf_propagate() {
let mut ukf = UnscentedKalmanFilter::new(
UKF_PARAMS,
vec![0.0; 6],
None, vec![0.0; N], DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
1e-3,
2.0,
0.0,
);
let dt = 1.0;
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, earth::gravity(&0.0, &0.0)),
gyro: Vector3::new(0.0, 0.0, 0.0), };
ukf.predict(&imu_data, dt);
assert!(
ukf.mean_state.len() == 15 );
let measurement = GPSPositionMeasurement {
latitude: 0.0,
longitude: 0.0,
altitude: 0.0,
horizontal_noise_std: 1e-3,
vertical_noise_std: 1e-3,
};
ukf.update(&measurement);
assert_approx_eq!(ukf.mean_state[0], 0.0, 1e-3);
assert_approx_eq!(ukf.mean_state[1], 0.0, 1e-3);
assert_approx_eq!(ukf.mean_state[2], 0.0, 0.1);
assert_approx_eq!(ukf.mean_state[3], 0.0, 0.1);
assert_approx_eq!(ukf.mean_state[4], 0.0, 0.1);
assert_approx_eq!(ukf.mean_state[5], 0.0, 0.1);
}
#[test]
fn ukf_debug_display() {
let ukf = UnscentedKalmanFilter::new(
UKF_PARAMS,
IMU_BIASES.to_vec(),
None,
COVARIANCE_DIAGONAL.to_vec(),
DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
ALPHA,
BETA,
KAPPA,
);
let debug_str = format!("{:?}", ukf);
assert!(debug_str.contains("UKF"));
assert!(debug_str.contains("mean_state"));
let display_str = format!("{}", ukf);
assert!(display_str.contains("UnscentedKalmanFilter"));
assert!(display_str.contains("covariance"));
}
#[test]
fn ukf_predict_with_biases() {
let mut ukf = UnscentedKalmanFilter::new(
UKF_PARAMS,
vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6], None,
COVARIANCE_DIAGONAL.to_vec(),
DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
ALPHA,
BETA,
KAPPA,
);
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, -9.81),
gyro: Vector3::new(0.0, 0.0, 0.0),
};
ukf.predict(&imu_data, 0.1);
assert_eq!(ukf.mean_state.len(), 15);
}
#[test]
fn ukf_update_with_cross_covariance() {
let mut ukf = UnscentedKalmanFilter::new(
UKF_PARAMS,
IMU_BIASES.to_vec(),
None,
COVARIANCE_DIAGONAL.to_vec(),
DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
ALPHA,
BETA,
KAPPA,
);
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, -9.81),
gyro: Vector3::new(0.0, 0.0, 0.0),
};
ukf.predict(&imu_data, 0.1);
let measurement = GPSPositionMeasurement {
latitude: 0.001,
longitude: 0.001,
altitude: 10.0,
horizontal_noise_std: 5.0,
vertical_noise_std: 2.0,
};
ukf.update(&measurement);
assert!(!ukf.mean_state.is_empty());
}
#[test]
fn ukf_with_additional_states() {
let measurement_bias = vec![1.0, 2.0, 3.0];
let total_states = 15 + measurement_bias.len();
let ukf = UnscentedKalmanFilter::new(
UKF_PARAMS,
IMU_BIASES.to_vec(),
Some(measurement_bias),
vec![1e-6; total_states],
DMatrix::from_diagonal(&DVector::from_vec(vec![1e-9; total_states])),
ALPHA,
BETA,
KAPPA,
);
assert_eq!(ukf.state_size, total_states);
assert_eq!(ukf.mean_state.len(), total_states);
}
#[test]
fn ukf_with_velocity_measurement() {
let mut ukf = UnscentedKalmanFilter::new(
UKF_PARAMS,
IMU_BIASES.to_vec(),
None,
COVARIANCE_DIAGONAL.to_vec(),
DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
ALPHA,
BETA,
KAPPA,
);
let vel_meas = GPSVelocityMeasurement {
northward_velocity: 0.0,
eastward_velocity: 0.0,
vertical_velocity: 0.0,
horizontal_noise_std: 0.5,
vertical_noise_std: 0.5,
};
ukf.update(&vel_meas);
assert_eq!(ukf.mean_state.len(), 15);
}
#[test]
fn ukf_with_position_velocity_measurement() {
let mut ukf = UnscentedKalmanFilter::new(
UKF_PARAMS,
IMU_BIASES.to_vec(),
None,
COVARIANCE_DIAGONAL.to_vec(),
DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
ALPHA,
BETA,
KAPPA,
);
let meas = GPSPositionAndVelocityMeasurement {
latitude: 0.0,
longitude: 0.0,
altitude: 0.0,
northward_velocity: 0.0,
eastward_velocity: 0.0,
horizontal_noise_std: 5.0,
vertical_noise_std: 2.0,
velocity_noise_std: 0.5,
};
ukf.update(&meas);
assert_eq!(ukf.mean_state.len(), 15);
}
#[test]
fn ukf_with_altitude_measurement() {
let initial_state = InitialState {
latitude: 0.0,
longitude: 0.0,
altitude: 100.0,
northward_velocity: 0.0,
eastward_velocity: 0.0,
vertical_velocity: 0.0,
roll: 0.0,
pitch: 0.0,
yaw: 0.0,
in_degrees: false,
is_enu: true,
};
let mut ukf = UnscentedKalmanFilter::new(
initial_state,
IMU_BIASES.to_vec(),
None,
COVARIANCE_DIAGONAL.to_vec(),
DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
ALPHA,
BETA,
KAPPA,
);
let alt_meas = RelativeAltitudeMeasurement {
relative_altitude: 5.0,
reference_altitude: 95.0,
};
ukf.update(&alt_meas);
assert!(ukf.mean_state[2] > 90.0 && ukf.mean_state[2] < 110.0);
}
#[test]
fn ukf_free_fall_motion() {
let initial_state = InitialState {
latitude: 0.0,
longitude: 0.0,
altitude: 100.0,
northward_velocity: 0.0,
eastward_velocity: 0.0,
vertical_velocity: 0.0,
roll: 0.0,
pitch: 0.0,
yaw: 0.0,
in_degrees: false,
is_enu: true,
};
let mut ukf = UnscentedKalmanFilter::new(
initial_state,
IMU_BIASES.to_vec(),
None,
vec![
1e-6, 1e-6, 1.0, 0.1, 0.1, 0.1, 1e-4, 1e-4, 1e-4, 1e-6, 1e-6, 1e-6, 1e-8, 1e-8,
1e-8,
],
DMatrix::from_diagonal(&DVector::from_vec(vec![
1e-9, 1e-9, 1e-6, 1e-6, 1e-6, 1e-6, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9,
1e-9,
])),
ALPHA,
BETA,
KAPPA,
);
let dt = 0.1;
let num_steps = 10;
for _ in 0..num_steps {
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, 0.0), gyro: Vector3::new(0.0, 0.0, 0.0),
};
ukf.predict(&imu_data, dt);
}
let final_vd = ukf.mean_state[5];
assert!(
final_vd < -5.0,
"Expected significant vertical velocity, got {}",
final_vd
);
let final_altitude = ukf.mean_state[2];
assert!(
final_altitude < 100.0,
"Expected altitude decrease, got {}",
final_altitude
);
let measurement = GPSPositionMeasurement {
latitude: 0.0,
longitude: 0.0,
altitude: final_altitude,
horizontal_noise_std: 5.0,
vertical_noise_std: 2.0,
};
ukf.update(&measurement);
assert_approx_eq!(ukf.mean_state[2], final_altitude, 5.0);
}
#[test]
fn ukf_hover_motion() {
let initial_state = InitialState {
latitude: 0.0,
longitude: 0.0,
altitude: 100.0,
northward_velocity: 0.0,
eastward_velocity: 0.0,
vertical_velocity: 0.0,
roll: 0.0,
pitch: 0.0,
yaw: 0.0,
in_degrees: false,
is_enu: true,
};
let mut ukf = UnscentedKalmanFilter::new(
initial_state,
IMU_BIASES.to_vec(),
None,
vec![
1e-6, 1e-6, 1.0, 0.1, 0.1, 0.1, 1e-4, 1e-4, 1e-4, 1e-6, 1e-6, 1e-6, 1e-8, 1e-8,
1e-8,
],
DMatrix::from_diagonal(&DVector::from_vec(vec![
1e-9, 1e-9, 1e-6, 1e-6, 1e-6, 1e-6, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9,
1e-9,
])),
ALPHA,
BETA,
KAPPA,
);
let dt = 0.1;
let num_steps = 10;
for _ in 0..num_steps {
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, earth::gravity(&0.0, &0.0)),
gyro: Vector3::new(0.0, 0.0, 0.0),
};
ukf.predict(&imu_data, dt);
}
let final_vn = ukf.mean_state[3];
let final_ve = ukf.mean_state[4];
let final_vd = ukf.mean_state[5];
assert_approx_eq!(final_vn, 0.0, 0.5);
assert_approx_eq!(final_ve, 0.0, 0.5);
assert_approx_eq!(final_vd, 0.0, 0.5);
let final_altitude = ukf.mean_state[2];
assert_approx_eq!(final_altitude, 100.0, 1.0);
let vel_measurement = GPSVelocityMeasurement {
northward_velocity: 0.0,
eastward_velocity: 0.0,
vertical_velocity: 0.0,
horizontal_noise_std: 0.5,
vertical_noise_std: 0.5,
};
ukf.update(&vel_measurement);
assert_approx_eq!(ukf.mean_state[3], 0.0, 0.5);
assert_approx_eq!(ukf.mean_state[4], 0.0, 0.5);
assert_approx_eq!(ukf.mean_state[5], 0.0, 0.5);
}
#[test]
fn ukf_northward_motion() {
let initial_state = InitialState {
latitude: 0.0,
longitude: 0.0,
altitude: 100.0,
northward_velocity: 10.0, eastward_velocity: 0.0,
vertical_velocity: 0.0,
roll: 0.0,
pitch: 0.0,
yaw: 0.0,
in_degrees: false,
is_enu: true,
};
let mut ukf = UnscentedKalmanFilter::new(
initial_state,
IMU_BIASES.to_vec(),
None,
vec![
1e-6, 1e-6, 1.0, 0.1, 0.1, 0.1, 1e-4, 1e-4, 1e-4, 1e-6, 1e-6, 1e-6, 1e-8, 1e-8,
1e-8,
],
DMatrix::from_diagonal(&DVector::from_vec(vec![
1e-9, 1e-9, 1e-6, 1e-6, 1e-6, 1e-6, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9,
1e-9,
])),
ALPHA,
BETA,
KAPPA,
);
let dt = 0.1;
let num_steps = 10;
let initial_lat = ukf.mean_state[0];
for _ in 0..num_steps {
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, earth::gravity(&0.0, &0.0)),
gyro: Vector3::new(0.0, 0.0, 0.0),
};
ukf.predict(&imu_data, dt);
}
let final_lat = ukf.mean_state[0];
assert!(
final_lat > initial_lat,
"Expected latitude increase, got initial: {} final: {}",
initial_lat,
final_lat
);
let final_vn = ukf.mean_state[3];
assert_approx_eq!(final_vn, 10.0, 2.0);
let final_ve = ukf.mean_state[4];
assert_approx_eq!(final_ve, 0.0, 0.5);
let meas = GPSPositionAndVelocityMeasurement {
latitude: final_lat.to_degrees(),
longitude: 0.0,
altitude: 100.0,
northward_velocity: 10.0,
eastward_velocity: 0.0,
horizontal_noise_std: 5.0,
vertical_noise_std: 2.0,
velocity_noise_std: 0.5,
};
ukf.update(&meas);
assert_approx_eq!(ukf.mean_state[3], 10.0, 1.0);
assert_approx_eq!(ukf.mean_state[4], 0.0, 0.5);
}
#[test]
fn ukf_eastward_motion() {
let initial_state = InitialState {
latitude: 0.0,
longitude: 0.0,
altitude: 100.0,
northward_velocity: 0.0,
eastward_velocity: 15.0, vertical_velocity: 0.0,
roll: 0.0,
pitch: 0.0,
yaw: 0.0,
in_degrees: false,
is_enu: true,
};
let mut ukf = UnscentedKalmanFilter::new(
initial_state,
IMU_BIASES.to_vec(),
None,
vec![
1e-6, 1e-6, 1.0, 0.1, 0.1, 0.1, 1e-4, 1e-4, 1e-4, 1e-6, 1e-6, 1e-6, 1e-8, 1e-8,
1e-8,
],
DMatrix::from_diagonal(&DVector::from_vec(vec![
1e-9, 1e-9, 1e-6, 1e-6, 1e-6, 1e-6, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9,
1e-9,
])),
ALPHA,
BETA,
KAPPA,
);
let dt = 0.1;
let num_steps = 10;
let initial_lon = ukf.mean_state[1];
for _ in 0..num_steps {
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, earth::gravity(&0.0, &0.0)),
gyro: Vector3::new(0.0, 0.0, 0.0),
};
ukf.predict(&imu_data, dt);
}
let final_lon = ukf.mean_state[1];
assert!(
final_lon > initial_lon,
"Expected longitude increase, got initial: {} final: {}",
initial_lon,
final_lon
);
let final_ve = ukf.mean_state[4];
assert_approx_eq!(final_ve, 15.0, 2.0);
let final_vn = ukf.mean_state[3];
assert_approx_eq!(final_vn, 0.0, 0.5);
let final_vd = ukf.mean_state[5];
assert_approx_eq!(final_vd, 0.0, 0.5);
let pos_meas = GPSPositionMeasurement {
latitude: 0.0,
longitude: final_lon.to_degrees(),
altitude: 100.0,
horizontal_noise_std: 5.0,
vertical_noise_std: 2.0,
};
ukf.update(&pos_meas);
assert_approx_eq!(ukf.mean_state[1], final_lon, 0.01);
assert_approx_eq!(ukf.mean_state[2], 100.0, 5.0);
let vel_meas = GPSVelocityMeasurement {
northward_velocity: 0.0,
eastward_velocity: 15.0,
vertical_velocity: 0.0,
horizontal_noise_std: 0.5,
vertical_noise_std: 0.5,
};
ukf.update(&vel_meas);
assert_approx_eq!(ukf.mean_state[3], 0.0, 0.5);
assert_approx_eq!(ukf.mean_state[4], 15.0, 1.0);
assert_approx_eq!(ukf.mean_state[5], 0.0, 0.5);
}
#[test]
fn ukf_combined_horizontal_motion() {
let initial_state = InitialState {
latitude: 0.0,
longitude: 0.0,
altitude: 100.0,
northward_velocity: 10.0,
eastward_velocity: 10.0,
vertical_velocity: 0.0,
roll: 0.0,
pitch: 0.0,
yaw: 0.0,
in_degrees: false,
is_enu: true,
};
let mut ukf = UnscentedKalmanFilter::new(
initial_state,
IMU_BIASES.to_vec(),
None,
vec![
1e-6, 1e-6, 1.0, 0.1, 0.1, 0.1, 1e-4, 1e-4, 1e-4, 1e-6, 1e-6, 1e-6, 1e-8, 1e-8,
1e-8,
],
DMatrix::from_diagonal(&DVector::from_vec(vec![
1e-9, 1e-9, 1e-6, 1e-6, 1e-6, 1e-6, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9,
1e-9,
])),
ALPHA,
BETA,
KAPPA,
);
let dt = 0.1;
let num_steps = 10;
let initial_lat = ukf.mean_state[0];
let initial_lon = ukf.mean_state[1];
for _ in 0..num_steps {
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, earth::gravity(&0.0, &0.0)),
gyro: Vector3::new(0.0, 0.0, 0.0),
};
ukf.predict(&imu_data, dt);
}
let final_lat = ukf.mean_state[0];
let final_lon = ukf.mean_state[1];
assert!(final_lat > initial_lat, "Expected latitude increase");
assert!(final_lon > initial_lon, "Expected longitude increase");
assert_approx_eq!(ukf.mean_state[3], 10.0, 2.0);
assert_approx_eq!(ukf.mean_state[4], 10.0, 2.0);
let meas = GPSPositionAndVelocityMeasurement {
latitude: final_lat.to_degrees(),
longitude: final_lon.to_degrees(),
altitude: 100.0,
northward_velocity: 10.0,
eastward_velocity: 10.0,
horizontal_noise_std: 5.0,
vertical_noise_std: 2.0,
velocity_noise_std: 0.5,
};
ukf.update(&meas);
assert_approx_eq!(ukf.mean_state[3], 10.0, 1.0);
assert_approx_eq!(ukf.mean_state[4], 10.0, 1.0);
assert_approx_eq!(ukf.mean_state[2], 100.0, 5.0);
}
#[test]
fn ekf_construction_9state() {
let ekf = ExtendedKalmanFilter::new(
UKF_PARAMS,
vec![0.0; 6], vec![1e-3; 9],
DMatrix::from_diagonal(&DVector::from_vec(vec![1e-3; 9])),
false, );
assert_eq!(ekf.mean_state.len(), 9);
assert_eq!(ekf.state_size, 9);
assert!(!ekf.use_biases);
}
#[test]
fn ekf_construction_15state() {
let ekf = ExtendedKalmanFilter::new(
UKF_PARAMS,
IMU_BIASES.to_vec(),
COVARIANCE_DIAGONAL.to_vec(),
DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
true, );
assert_eq!(ekf.mean_state.len(), 15);
assert_eq!(ekf.state_size, 15);
assert!(ekf.use_biases);
}
#[test]
fn ekf_debug_display() {
let ekf = ExtendedKalmanFilter::new(
UKF_PARAMS,
IMU_BIASES.to_vec(),
COVARIANCE_DIAGONAL.to_vec(),
DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
true,
);
let debug_str = format!("{:?}", ekf);
assert!(debug_str.contains("EKF"));
assert!(debug_str.contains("mean_state"));
let display_str = format!("{}", ekf);
assert!(display_str.contains("ExtendedKalmanFilter"));
assert!(display_str.contains("covariance"));
}
#[test]
fn ekf_propagate_9state() {
let mut ekf = ExtendedKalmanFilter::new(
UKF_PARAMS,
vec![0.0; 6],
vec![0.0; 9], DMatrix::from_diagonal(&DVector::from_vec(vec![1e-9; 9])),
false, );
let dt = 1.0;
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, earth::gravity(&0.0, &0.0)),
gyro: Vector3::new(0.0, 0.0, 0.0),
};
ekf.predict(&imu_data, dt);
assert_eq!(ekf.mean_state.len(), 9);
let measurement = GPSPositionMeasurement {
latitude: 0.0,
longitude: 0.0,
altitude: 0.0,
horizontal_noise_std: 1e-3,
vertical_noise_std: 1e-3,
};
ekf.update(&measurement);
assert_approx_eq!(ekf.mean_state[0], 0.0, 1e-3);
assert_approx_eq!(ekf.mean_state[1], 0.0, 1e-3);
assert_approx_eq!(ekf.mean_state[2], 0.0, 0.1);
assert_approx_eq!(ekf.mean_state[3], 0.0, 0.1);
assert_approx_eq!(ekf.mean_state[4], 0.0, 0.1);
assert_approx_eq!(ekf.mean_state[5], 0.0, 0.1);
}
#[test]
fn ekf_propagate_15state() {
let mut ekf = ExtendedKalmanFilter::new(
UKF_PARAMS,
vec![0.0; 6],
vec![0.0; 15], DMatrix::from_diagonal(&DVector::from_vec(vec![1e-9; 15])),
true, );
let dt = 1.0;
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, earth::gravity(&0.0, &0.0)),
gyro: Vector3::new(0.0, 0.0, 0.0),
};
ekf.predict(&imu_data, dt);
assert_eq!(ekf.mean_state.len(), 15);
let measurement = GPSPositionMeasurement {
latitude: 0.0,
longitude: 0.0,
altitude: 0.0,
horizontal_noise_std: 1e-3,
vertical_noise_std: 1e-3,
};
ekf.update(&measurement);
assert_approx_eq!(ekf.mean_state[0], 0.0, 1e-3);
assert_approx_eq!(ekf.mean_state[1], 0.0, 1e-3);
assert_approx_eq!(ekf.mean_state[2], 0.0, 0.1);
}
#[test]
fn ekf_predict_with_nonzero_biases() {
let mut ekf = ExtendedKalmanFilter::new(
UKF_PARAMS,
vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6], COVARIANCE_DIAGONAL.to_vec(),
DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
true,
);
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, -9.81),
gyro: Vector3::new(0.0, 0.0, 0.0),
};
ekf.predict(&imu_data, 0.1);
assert_eq!(ekf.mean_state.len(), 15);
assert_approx_eq!(ekf.mean_state[9], 0.1, 1e-6);
assert_approx_eq!(ekf.mean_state[12], 0.4, 1e-6);
}
#[test]
fn ekf_with_velocity_measurement() {
let mut ekf = ExtendedKalmanFilter::new(
UKF_PARAMS,
IMU_BIASES.to_vec(),
COVARIANCE_DIAGONAL.to_vec(),
DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
true,
);
let vel_meas = GPSVelocityMeasurement {
northward_velocity: 0.0,
eastward_velocity: 0.0,
vertical_velocity: 0.0,
horizontal_noise_std: 0.5,
vertical_noise_std: 0.5,
};
ekf.update(&vel_meas);
assert_eq!(ekf.mean_state.len(), 15);
}
#[test]
fn ekf_with_position_velocity_measurement() {
let mut ekf = ExtendedKalmanFilter::new(
UKF_PARAMS,
IMU_BIASES.to_vec(),
COVARIANCE_DIAGONAL.to_vec(),
DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
true,
);
let meas = GPSPositionAndVelocityMeasurement {
latitude: 0.0,
longitude: 0.0,
altitude: 0.0,
northward_velocity: 0.0,
eastward_velocity: 0.0,
horizontal_noise_std: 5.0,
vertical_noise_std: 2.0,
velocity_noise_std: 0.5,
};
ekf.update(&meas);
assert_eq!(ekf.mean_state.len(), 15);
}
#[test]
fn ekf_with_altitude_measurement() {
let initial_state = InitialState {
latitude: 0.0,
longitude: 0.0,
altitude: 100.0,
northward_velocity: 0.0,
eastward_velocity: 0.0,
vertical_velocity: 0.0,
roll: 0.0,
pitch: 0.0,
yaw: 0.0,
in_degrees: false,
is_enu: true,
};
let mut ekf = ExtendedKalmanFilter::new(
initial_state,
IMU_BIASES.to_vec(),
COVARIANCE_DIAGONAL.to_vec(),
DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
true,
);
let alt_meas = RelativeAltitudeMeasurement {
relative_altitude: 5.0,
reference_altitude: 95.0,
};
ekf.update(&alt_meas);
assert!(ekf.mean_state[2] > 90.0 && ekf.mean_state[2] < 110.0);
}
#[test]
fn ekf_free_fall_motion() {
let initial_state = InitialState {
latitude: 0.0,
longitude: 0.0,
altitude: 100.0,
northward_velocity: 0.0,
eastward_velocity: 0.0,
vertical_velocity: 0.0,
roll: 0.0,
pitch: 0.0,
yaw: 0.0,
in_degrees: false,
is_enu: true,
};
let mut ekf = ExtendedKalmanFilter::new(
initial_state,
IMU_BIASES.to_vec(),
vec![
1e-6, 1e-6, 1.0, 0.1, 0.1, 0.1, 1e-4, 1e-4, 1e-4, 1e-6, 1e-6, 1e-6, 1e-8, 1e-8,
1e-8,
],
DMatrix::from_diagonal(&DVector::from_vec(vec![
1e-9, 1e-9, 1e-6, 1e-6, 1e-6, 1e-6, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9,
1e-9,
])),
true,
);
let dt = 0.1;
let num_steps = 10;
for _ in 0..num_steps {
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, 0.0), gyro: Vector3::new(0.0, 0.0, 0.0),
};
ekf.predict(&imu_data, dt);
}
let final_vd = ekf.mean_state[5];
assert!(
final_vd < -5.0,
"Expected significant vertical velocity, got {}",
final_vd
);
let final_altitude = ekf.mean_state[2];
assert!(
final_altitude < 100.0,
"Expected altitude decrease, got {}",
final_altitude
);
let measurement = GPSPositionMeasurement {
latitude: 0.0,
longitude: 0.0,
altitude: final_altitude,
horizontal_noise_std: 5.0,
vertical_noise_std: 2.0,
};
ekf.update(&measurement);
assert_approx_eq!(ekf.mean_state[2], final_altitude, 5.0);
}
#[test]
fn ekf_hover_motion() {
let initial_state = InitialState {
latitude: 0.0,
longitude: 0.0,
altitude: 100.0,
northward_velocity: 0.0,
eastward_velocity: 0.0,
vertical_velocity: 0.0,
roll: 0.0,
pitch: 0.0,
yaw: 0.0,
in_degrees: false,
is_enu: true,
};
let mut ekf = ExtendedKalmanFilter::new(
initial_state,
IMU_BIASES.to_vec(),
vec![
1e-6, 1e-6, 1.0, 0.1, 0.1, 0.1, 1e-4, 1e-4, 1e-4, 1e-6, 1e-6, 1e-6, 1e-8, 1e-8,
1e-8,
],
DMatrix::from_diagonal(&DVector::from_vec(vec![
1e-9, 1e-9, 1e-6, 1e-6, 1e-6, 1e-6, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9,
1e-9,
])),
true,
);
let dt = 0.1;
let num_steps = 10;
for _ in 0..num_steps {
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, earth::gravity(&0.0, &0.0)),
gyro: Vector3::new(0.0, 0.0, 0.0),
};
ekf.predict(&imu_data, dt);
}
let final_vn = ekf.mean_state[3];
let final_ve = ekf.mean_state[4];
let final_vd = ekf.mean_state[5];
assert_approx_eq!(final_vn, 0.0, 0.5);
assert_approx_eq!(final_ve, 0.0, 0.5);
assert_approx_eq!(final_vd, 0.0, 0.5);
let final_altitude = ekf.mean_state[2];
assert_approx_eq!(final_altitude, 100.0, 1.0);
let vel_measurement = GPSVelocityMeasurement {
northward_velocity: 0.0,
eastward_velocity: 0.0,
vertical_velocity: 0.0,
horizontal_noise_std: 0.5,
vertical_noise_std: 0.5,
};
ekf.update(&vel_measurement);
assert_approx_eq!(ekf.mean_state[3], 0.0, 0.5);
assert_approx_eq!(ekf.mean_state[4], 0.0, 0.5);
assert_approx_eq!(ekf.mean_state[5], 0.0, 0.5);
}
#[test]
fn ekf_northward_motion() {
let initial_state = InitialState {
latitude: 0.0,
longitude: 0.0,
altitude: 100.0,
northward_velocity: 10.0, eastward_velocity: 0.0,
vertical_velocity: 0.0,
roll: 0.0,
pitch: 0.0,
yaw: 0.0,
in_degrees: false,
is_enu: true,
};
let mut ekf = ExtendedKalmanFilter::new(
initial_state,
IMU_BIASES.to_vec(),
vec![
1e-6, 1e-6, 1.0, 0.1, 0.1, 0.1, 1e-4, 1e-4, 1e-4, 1e-6, 1e-6, 1e-6, 1e-8, 1e-8,
1e-8,
],
DMatrix::from_diagonal(&DVector::from_vec(vec![
1e-9, 1e-9, 1e-6, 1e-6, 1e-6, 1e-6, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9,
1e-9,
])),
true,
);
let dt = 0.1;
let num_steps = 10;
let initial_lat = ekf.mean_state[0];
for _ in 0..num_steps {
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, earth::gravity(&0.0, &0.0)),
gyro: Vector3::new(0.0, 0.0, 0.0),
};
ekf.predict(&imu_data, dt);
}
let final_lat = ekf.mean_state[0];
assert!(
final_lat > initial_lat,
"Expected latitude increase, got initial: {} final: {}",
initial_lat,
final_lat
);
let final_vn = ekf.mean_state[3];
assert_approx_eq!(final_vn, 10.0, 2.0);
let final_ve = ekf.mean_state[4];
assert_approx_eq!(final_ve, 0.0, 0.5);
let meas = GPSPositionAndVelocityMeasurement {
latitude: final_lat.to_degrees(),
longitude: 0.0,
altitude: 100.0,
northward_velocity: 10.0,
eastward_velocity: 0.0,
horizontal_noise_std: 5.0,
vertical_noise_std: 2.0,
velocity_noise_std: 0.5,
};
ekf.update(&meas);
assert_approx_eq!(ekf.mean_state[3], 10.0, 1.0);
assert_approx_eq!(ekf.mean_state[4], 0.0, 0.5);
}
#[test]
fn ekf_eastward_motion() {
let initial_state = InitialState {
latitude: 0.0,
longitude: 0.0,
altitude: 100.0,
northward_velocity: 0.0,
eastward_velocity: 15.0, vertical_velocity: 0.0,
roll: 0.0,
pitch: 0.0,
yaw: 0.0,
in_degrees: false,
is_enu: true,
};
let mut ekf = ExtendedKalmanFilter::new(
initial_state,
IMU_BIASES.to_vec(),
vec![
1e-6, 1e-6, 1.0, 0.1, 0.1, 0.1, 1e-4, 1e-4, 1e-4, 1e-6, 1e-6, 1e-6, 1e-8, 1e-8,
1e-8,
],
DMatrix::from_diagonal(&DVector::from_vec(vec![
1e-9, 1e-9, 1e-6, 1e-6, 1e-6, 1e-6, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9,
1e-9,
])),
true,
);
let dt = 0.1;
let num_steps = 10;
let initial_lon = ekf.mean_state[1];
for _ in 0..num_steps {
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, earth::gravity(&0.0, &0.0)),
gyro: Vector3::new(0.0, 0.0, 0.0),
};
ekf.predict(&imu_data, dt);
}
let final_lon = ekf.mean_state[1];
assert!(
final_lon > initial_lon,
"Expected longitude increase, got initial: {} final: {}",
initial_lon,
final_lon
);
let final_ve = ekf.mean_state[4];
assert_approx_eq!(final_ve, 15.0, 2.0);
let final_vn = ekf.mean_state[3];
assert_approx_eq!(final_vn, 0.0, 0.5);
let final_vd = ekf.mean_state[5];
assert_approx_eq!(final_vd, 0.0, 0.5);
let pos_meas = GPSPositionMeasurement {
latitude: 0.0,
longitude: final_lon.to_degrees(),
altitude: 100.0,
horizontal_noise_std: 5.0,
vertical_noise_std: 2.0,
};
ekf.update(&pos_meas);
assert_approx_eq!(ekf.mean_state[1], final_lon, 0.01);
assert_approx_eq!(ekf.mean_state[2], 100.0, 5.0);
let vel_meas = GPSVelocityMeasurement {
northward_velocity: 0.0,
eastward_velocity: 15.0,
vertical_velocity: 0.0,
horizontal_noise_std: 0.5,
vertical_noise_std: 0.5,
};
ekf.update(&vel_meas);
assert_approx_eq!(ekf.mean_state[3], 0.0, 0.5);
assert_approx_eq!(ekf.mean_state[4], 15.0, 1.0);
assert_approx_eq!(ekf.mean_state[5], 0.0, 0.5);
}
#[test]
fn ekf_combined_horizontal_motion() {
let initial_state = InitialState {
latitude: 0.0,
longitude: 0.0,
altitude: 100.0,
northward_velocity: 10.0,
eastward_velocity: 10.0,
vertical_velocity: 0.0,
roll: 0.0,
pitch: 0.0,
yaw: 0.0,
in_degrees: false,
is_enu: true,
};
let mut ekf = ExtendedKalmanFilter::new(
initial_state,
IMU_BIASES.to_vec(),
vec![
1e-6, 1e-6, 1.0, 0.1, 0.1, 0.1, 1e-4, 1e-4, 1e-4, 1e-6, 1e-6, 1e-6, 1e-8, 1e-8,
1e-8,
],
DMatrix::from_diagonal(&DVector::from_vec(vec![
1e-9, 1e-9, 1e-6, 1e-6, 1e-6, 1e-6, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9,
1e-9,
])),
true,
);
let dt = 0.1;
let num_steps = 10;
let initial_lat = ekf.mean_state[0];
let initial_lon = ekf.mean_state[1];
for _ in 0..num_steps {
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, earth::gravity(&0.0, &0.0)),
gyro: Vector3::new(0.0, 0.0, 0.0),
};
ekf.predict(&imu_data, dt);
}
let final_lat = ekf.mean_state[0];
let final_lon = ekf.mean_state[1];
assert!(final_lat > initial_lat, "Expected latitude increase");
assert!(final_lon > initial_lon, "Expected longitude increase");
assert_approx_eq!(ekf.mean_state[3], 10.0, 2.0);
assert_approx_eq!(ekf.mean_state[4], 10.0, 2.0);
let meas = GPSPositionAndVelocityMeasurement {
latitude: final_lat.to_degrees(),
longitude: final_lon.to_degrees(),
altitude: 100.0,
northward_velocity: 10.0,
eastward_velocity: 10.0,
horizontal_noise_std: 5.0,
vertical_noise_std: 2.0,
velocity_noise_std: 0.5,
};
ekf.update(&meas);
assert_approx_eq!(ekf.mean_state[3], 10.0, 1.0);
assert_approx_eq!(ekf.mean_state[4], 10.0, 1.0);
assert_approx_eq!(ekf.mean_state[2], 100.0, 5.0);
}
#[test]
fn ekf_covariance_reduction() {
let mut ekf = ExtendedKalmanFilter::new(
UKF_PARAMS,
IMU_BIASES.to_vec(),
vec![1.0; 15], DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
true,
);
let initial_trace: f64 = (0..15).map(|i| ekf.covariance[(i, i)]).sum();
let measurement = GPSPositionMeasurement {
latitude: 0.0,
longitude: 0.0,
altitude: 0.0,
horizontal_noise_std: 1.0,
vertical_noise_std: 1.0,
};
ekf.update(&measurement);
let final_trace: f64 = (0..15).map(|i| ekf.covariance[(i, i)]).sum();
assert!(
final_trace < initial_trace,
"Covariance should decrease after measurement update: {} >= {}",
final_trace,
initial_trace
);
}
#[test]
fn ekf_angle_wrapping() {
let initial_state = InitialState {
latitude: 0.0,
longitude: 0.0,
altitude: 100.0,
northward_velocity: 0.0,
eastward_velocity: 0.0,
vertical_velocity: 0.0,
roll: 3.0, pitch: 3.0,
yaw: 3.0,
in_degrees: false,
is_enu: true,
};
let mut ekf = ExtendedKalmanFilter::new(
initial_state,
IMU_BIASES.to_vec(),
COVARIANCE_DIAGONAL.to_vec(),
DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
true,
);
let measurement = GPSPositionMeasurement {
latitude: 0.0,
longitude: 0.0,
altitude: 100.0,
horizontal_noise_std: 5.0,
vertical_noise_std: 2.0,
};
ekf.update(&measurement);
assert!(ekf.mean_state[6] >= 0.0 && ekf.mean_state[6] <= 2.0 * std::f64::consts::PI);
assert!(ekf.mean_state[7] >= 0.0 && ekf.mean_state[7] <= 2.0 * std::f64::consts::PI);
assert!(ekf.mean_state[8] >= 0.0 && ekf.mean_state[8] <= 2.0 * std::f64::consts::PI);
}
#[test]
fn eskf_construction() {
let eskf = ErrorStateKalmanFilter::new(
UKF_PARAMS,
IMU_BIASES.to_vec(),
COVARIANCE_DIAGONAL.to_vec(),
DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
);
let state = eskf.get_estimate();
assert_eq!(state.len(), 15);
assert_eq!(eskf.error_state.len(), 15);
for i in 0..15 {
assert_approx_eq!(eskf.error_state[i], 0.0, 1e-10);
}
let quat_norm = eskf.nominal_quaternion.norm();
assert_approx_eq!(quat_norm, 1.0, 1e-6);
}
#[test]
fn eskf_debug_display() {
let eskf = ErrorStateKalmanFilter::new(
UKF_PARAMS,
IMU_BIASES.to_vec(),
COVARIANCE_DIAGONAL.to_vec(),
DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
);
let debug_str = format!("{:?}", eskf);
assert!(debug_str.contains("ESKF"));
assert!(debug_str.contains("nominal_position"));
let display_str = format!("{}", eskf);
assert!(display_str.contains("ErrorStateKalmanFilter"));
assert!(display_str.contains("nominal_quaternion"));
}
#[test]
fn eskf_quaternion_normalization() {
let mut eskf = ErrorStateKalmanFilter::new(
UKF_PARAMS,
IMU_BIASES.to_vec(),
COVARIANCE_DIAGONAL.to_vec(),
DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
);
for _ in 0..10 {
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, earth::gravity(&0.0, &0.0)),
gyro: Vector3::new(0.01, 0.01, 0.01), };
eskf.predict(&imu_data, 0.01);
let measurement = GPSPositionMeasurement {
latitude: 0.0,
longitude: 0.0,
altitude: 0.0,
horizontal_noise_std: 5.0,
vertical_noise_std: 2.0,
};
eskf.update(&measurement);
}
let quat_norm = eskf.nominal_quaternion.norm();
assert_approx_eq!(quat_norm, 1.0, 1e-6);
}
#[test]
fn eskf_error_reset_after_update() {
let mut eskf = ErrorStateKalmanFilter::new(
UKF_PARAMS,
IMU_BIASES.to_vec(),
vec![1e-3; 15], DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
);
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, earth::gravity(&0.0, &0.0)),
gyro: Vector3::zeros(),
};
eskf.predict(&imu_data, 1.0);
let measurement = GPSPositionMeasurement {
latitude: 0.001, longitude: 0.001,
altitude: 1.0,
horizontal_noise_std: 5.0,
vertical_noise_std: 2.0,
};
eskf.update(&measurement);
for i in 0..15 {
assert_approx_eq!(eskf.error_state[i], 0.0, 1e-10);
}
}
#[test]
fn eskf_bias_estimation() {
let initial_state = InitialState {
latitude: 0.0,
longitude: 0.0,
altitude: 100.0,
northward_velocity: 0.0,
eastward_velocity: 0.0,
vertical_velocity: 0.0,
roll: 0.0,
pitch: 0.0,
yaw: 0.0,
in_degrees: false,
is_enu: true,
};
let true_accel_bias = Vector3::new(0.1, 0.05, 0.08);
let true_gyro_bias = Vector3::new(0.01, 0.015, 0.02);
let mut eskf = ErrorStateKalmanFilter::new(
initial_state,
vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0], vec![1e-6; 15],
DMatrix::from_diagonal(&DVector::from_vec(vec![
1e-9, 1e-9, 1e-6, 1e-6, 1e-6, 1e-6, 1e-9, 1e-9, 1e-9, 1e-6, 1e-6,
1e-6, 1e-8, 1e-8, 1e-8,
])),
);
for _ in 0..20 {
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, earth::gravity(&0.0, &0.0)) + true_accel_bias,
gyro: true_gyro_bias,
};
eskf.predict(&imu_data, 0.1);
let measurement = GPSPositionMeasurement {
latitude: 0.0,
longitude: 0.0,
altitude: 100.0,
horizontal_noise_std: 1.0,
vertical_noise_std: 0.5,
};
eskf.update(&measurement);
}
let state = eskf.get_estimate();
assert!(state[9].abs() < 1.0); assert!(state[10].abs() < 1.0); assert!(state[11].abs() < 1.0); assert!(state[12].abs() < 0.5); assert!(state[13].abs() < 0.5); assert!(state[14].abs() < 0.5); }
#[test]
fn eskf_hover_motion() {
let initial_state = InitialState {
latitude: 0.0,
longitude: 0.0,
altitude: 100.0,
northward_velocity: 0.0,
eastward_velocity: 0.0,
vertical_velocity: 0.0,
roll: 0.0,
pitch: 0.0,
yaw: 0.0,
in_degrees: false,
is_enu: true,
};
let mut eskf = ErrorStateKalmanFilter::new(
initial_state,
IMU_BIASES.to_vec(),
vec![
1e-6, 1e-6, 1.0, 0.1, 0.1, 0.1, 1e-4, 1e-4, 1e-4, 1e-6, 1e-6, 1e-6, 1e-8, 1e-8,
1e-8,
],
DMatrix::from_diagonal(&DVector::from_vec(vec![
1e-9, 1e-9, 1e-6, 1e-6, 1e-6, 1e-6, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9,
1e-9,
])),
);
let dt = 0.1;
let num_steps = 10;
for _ in 0..num_steps {
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, earth::gravity(&0.0, &0.0)),
gyro: Vector3::zeros(),
};
eskf.predict(&imu_data, dt);
}
let state = eskf.get_estimate();
assert_approx_eq!(state[0], 0.0, 0.001); assert_approx_eq!(state[1], 0.0, 0.001); assert_approx_eq!(state[2], 100.0, 1.0); assert_approx_eq!(state[3], 0.0, 0.5); assert_approx_eq!(state[4], 0.0, 0.5); assert_approx_eq!(state[5], 0.0, 0.5); }
#[test]
fn eskf_with_velocity_measurement() {
let mut eskf = ErrorStateKalmanFilter::new(
UKF_PARAMS,
IMU_BIASES.to_vec(),
COVARIANCE_DIAGONAL.to_vec(),
DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
);
let vel_meas = GPSVelocityMeasurement {
northward_velocity: 0.0,
eastward_velocity: 0.0,
vertical_velocity: 0.0,
horizontal_noise_std: 0.5,
vertical_noise_std: 0.5,
};
eskf.update(&vel_meas);
let state = eskf.get_estimate();
assert_eq!(state.len(), 15);
for i in 0..15 {
assert_approx_eq!(eskf.error_state[i], 0.0, 1e-10);
}
}
#[test]
fn eskf_covariance_reduction() {
let mut eskf = ErrorStateKalmanFilter::new(
UKF_PARAMS,
IMU_BIASES.to_vec(),
vec![1.0; 15], DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
);
let initial_trace: f64 = (0..15).map(|i| eskf.error_covariance[(i, i)]).sum();
let measurement = GPSPositionMeasurement {
latitude: 0.0,
longitude: 0.0,
altitude: 0.0,
horizontal_noise_std: 1.0,
vertical_noise_std: 1.0,
};
eskf.update(&measurement);
let final_trace: f64 = (0..15).map(|i| eskf.error_covariance[(i, i)]).sum();
assert!(
final_trace < initial_trace,
"Covariance should decrease after measurement update: {} >= {}",
final_trace,
initial_trace
);
}
#[test]
fn eskf_angle_wrapping() {
let initial_state = InitialState {
latitude: 0.0,
longitude: 0.0,
altitude: 100.0,
northward_velocity: 0.0,
eastward_velocity: 0.0,
vertical_velocity: 0.0,
roll: 3.0, pitch: 3.0,
yaw: 3.0,
in_degrees: false,
is_enu: true,
};
let mut eskf = ErrorStateKalmanFilter::new(
initial_state,
IMU_BIASES.to_vec(),
COVARIANCE_DIAGONAL.to_vec(),
DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
);
let measurement = GPSPositionMeasurement {
latitude: 0.0,
longitude: 0.0,
altitude: 100.0,
horizontal_noise_std: 5.0,
vertical_noise_std: 2.0,
};
eskf.update(&measurement);
let state = eskf.get_estimate();
assert!(state[6] >= 0.0 && state[6] <= 2.0 * std::f64::consts::PI);
assert!(state[7] >= 0.0 && state[7] <= 2.0 * std::f64::consts::PI);
assert!(state[8] >= 0.0 && state[8] <= 2.0 * std::f64::consts::PI);
}
#[test]
fn eskf_no_singularities() {
let initial_state = InitialState {
latitude: 45.0,
longitude: -122.0,
altitude: 100.0,
northward_velocity: 10.0,
eastward_velocity: 5.0,
vertical_velocity: 0.0,
roll: std::f64::consts::FRAC_PI_2 - 0.1, pitch: 0.0,
yaw: 0.0,
in_degrees: false,
is_enu: true,
};
let mut eskf = ErrorStateKalmanFilter::new(
initial_state,
IMU_BIASES.to_vec(),
COVARIANCE_DIAGONAL.to_vec(),
DMatrix::from_diagonal(&DVector::from_vec(PROCESS_NOISE_DIAGONAL.to_vec())),
);
for _ in 0..10 {
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, earth::gravity(&0.785, &100.0)),
gyro: Vector3::new(0.5, 0.5, 0.5), };
eskf.predict(&imu_data, 0.01);
}
let quat_norm = eskf.nominal_quaternion.norm();
assert_approx_eq!(quat_norm, 1.0, 1e-6);
let state = eskf.get_estimate();
assert!(state[0].is_finite()); assert!(state[1].is_finite()); assert!(state[2].is_finite()); for i in 6..9 {
assert!(state[i].is_finite()); }
}
}