use crate::earth::METERS_TO_DEGREES;
use std::any::Any;
use std::fmt::{self, Debug, Display};
use nalgebra::{DMatrix, DVector, Rotation3, Vector3};
use world_magnetic_model::GeomagneticField;
use world_magnetic_model::time::Date;
use world_magnetic_model::uom::si::angle::degree;
use world_magnetic_model::uom::si::f32::{Angle, Length};
use world_magnetic_model::uom::si::length::meter;
pub const MAG_YAW_NOISE: f64 = 0.2;
pub trait MeasurementModel: Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn get_dimension(&self) -> usize;
fn get_measurement(&self, state: &DVector<f64>) -> DVector<f64>;
fn get_noise(&self) -> DMatrix<f64>;
fn get_expected_measurement(&self, state: &DVector<f64>) -> DVector<f64>;
fn get_jacobian(&self, state: &DVector<f64>) -> DMatrix<f64>;
}
#[derive(Clone, Debug, Default)]
pub struct GPSPositionMeasurement {
pub latitude: f64,
pub longitude: f64,
pub altitude: f64,
pub horizontal_noise_std: f64,
pub vertical_noise_std: f64,
}
impl Display for GPSPositionMeasurement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"GPSPositionMeasurement(lat: {}, lon: {}, alt: {}, horiz_noise: {}, vert_noise: {})",
self.latitude,
self.longitude,
self.altitude,
self.horizontal_noise_std,
self.vertical_noise_std
)
}
}
impl MeasurementModel for GPSPositionMeasurement {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn get_dimension(&self) -> usize {
3
}
fn get_measurement(&self, _state: &DVector<f64>) -> DVector<f64> {
DVector::from_vec(vec![
self.latitude.to_radians(),
self.longitude.to_radians(),
self.altitude,
])
}
fn get_noise(&self) -> DMatrix<f64> {
let horizontal_noise_rad = (self.horizontal_noise_std * METERS_TO_DEGREES).to_radians();
DMatrix::from_diagonal(&DVector::from_vec(vec![
horizontal_noise_rad.powi(2),
horizontal_noise_rad.powi(2),
self.vertical_noise_std.powi(2),
]))
}
fn get_expected_measurement(&self, state: &DVector<f64>) -> DVector<f64> {
DVector::from_vec(vec![state[0], state[1], state[2]])
}
fn get_jacobian(&self, state: &DVector<f64>) -> DMatrix<f64> {
let nav_state = crate::StrapdownState::try_from(&state.as_slice()[..9]).unwrap();
crate::linearize::gps_position_jacobian(&nav_state)
}
}
#[derive(Clone, Debug, Default)]
pub struct GPSVelocityMeasurement {
pub northward_velocity: f64,
pub eastward_velocity: f64,
pub vertical_velocity: f64,
pub horizontal_noise_std: f64,
pub vertical_noise_std: f64,
}
impl Display for GPSVelocityMeasurement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"GPSVelocityMeasurement(north: {}, east: {}, down: {}, horiz_noise: {}, vert_noise: {})",
self.northward_velocity,
self.eastward_velocity,
self.vertical_velocity,
self.horizontal_noise_std,
self.vertical_noise_std
)
}
}
impl MeasurementModel for GPSVelocityMeasurement {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn get_dimension(&self) -> usize {
3
}
fn get_measurement(&self, _state: &DVector<f64>) -> DVector<f64> {
DVector::from_vec(vec![
self.northward_velocity,
self.eastward_velocity,
self.vertical_velocity,
])
}
fn get_noise(&self) -> DMatrix<f64> {
DMatrix::from_diagonal(&DVector::from_vec(vec![
self.horizontal_noise_std.powi(2),
self.horizontal_noise_std.powi(2),
self.vertical_noise_std.powi(2),
]))
}
fn get_expected_measurement(&self, state: &DVector<f64>) -> DVector<f64> {
DVector::from_vec(vec![state[3], state[4], state[5]])
}
fn get_jacobian(&self, state: &DVector<f64>) -> DMatrix<f64> {
let nav_state = crate::StrapdownState::try_from(&state.as_slice()[..9]).unwrap();
crate::linearize::gps_velocity_jacobian(&nav_state)
}
}
#[derive(Clone, Debug, Default)]
pub struct GPSPositionAndVelocityMeasurement {
pub latitude: f64,
pub longitude: f64,
pub altitude: f64,
pub northward_velocity: f64,
pub eastward_velocity: f64,
pub horizontal_noise_std: f64,
pub vertical_noise_std: f64,
pub velocity_noise_std: f64,
}
impl MeasurementModel for GPSPositionAndVelocityMeasurement {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn get_dimension(&self) -> usize {
5
}
fn get_measurement(&self, _state: &DVector<f64>) -> DVector<f64> {
DVector::from_vec(vec![
self.latitude.to_radians(),
self.longitude.to_radians(),
self.altitude,
self.northward_velocity,
self.eastward_velocity,
])
}
fn get_noise(&self) -> DMatrix<f64> {
let horizontal_noise_rad = (self.horizontal_noise_std * METERS_TO_DEGREES).to_radians();
DMatrix::from_diagonal(&DVector::from_vec(vec![
horizontal_noise_rad.powi(2),
horizontal_noise_rad.powi(2),
self.vertical_noise_std.powi(2),
self.velocity_noise_std.powi(2),
self.velocity_noise_std.powi(2),
]))
}
fn get_expected_measurement(&self, state: &DVector<f64>) -> DVector<f64> {
DVector::from_vec(vec![state[0], state[1], state[2], state[3], state[4]])
}
fn get_jacobian(&self, state: &DVector<f64>) -> DMatrix<f64> {
let nav_state = crate::StrapdownState::try_from(&state.as_slice()[..9]).unwrap();
crate::linearize::gps_position_velocity_jacobian(&nav_state)
}
}
#[derive(Clone, Debug, Default)]
pub struct RelativeAltitudeMeasurement {
pub relative_altitude: f64,
pub reference_altitude: f64,
}
impl Display for RelativeAltitudeMeasurement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"RelativeAltitudeMeasurement(rel_alt: {}, ref_alt: {})",
self.relative_altitude, self.reference_altitude
)
}
}
impl MeasurementModel for RelativeAltitudeMeasurement {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn get_dimension(&self) -> usize {
1
}
fn get_measurement(&self, _state: &DVector<f64>) -> DVector<f64> {
DVector::from_vec(vec![self.relative_altitude + self.reference_altitude])
}
fn get_noise(&self) -> DMatrix<f64> {
DMatrix::from_diagonal(&DVector::from_vec(vec![5.0]))
}
fn get_expected_measurement(&self, state: &DVector<f64>) -> DVector<f64> {
DVector::from_vec(vec![state[2]])
}
fn get_jacobian(&self, state: &DVector<f64>) -> DMatrix<f64> {
let nav_state = crate::StrapdownState::try_from(&state.as_slice()[..9]).unwrap();
crate::linearize::relative_altitude_jacobian(&nav_state)
}
}
#[derive(Clone, Debug)]
pub struct MagnetometerYawMeasurement {
pub mag_x: f64,
pub mag_y: f64,
pub mag_z: f64,
pub noise_std: f64,
pub apply_declination: bool,
pub year: i32,
pub day_of_year: u16,
}
impl Default for MagnetometerYawMeasurement {
fn default() -> Self {
Self {
mag_x: 0.0,
mag_y: 0.0,
mag_z: 0.0,
noise_std: 0.05, apply_declination: false,
year: 2025,
day_of_year: 1,
}
}
}
impl Display for MagnetometerYawMeasurement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"MagnetometerYawMeasurement(mag: [{:.2}, {:.2}, {:.2}] µT, noise: {:.4} rad, decl: {})",
self.mag_x, self.mag_y, self.mag_z, self.noise_std, self.apply_declination
)
}
}
impl MagnetometerYawMeasurement {
pub fn get_declination(&self, lat_deg: f64, lon_deg: f64, alt_m: f64) -> f64 {
let date = Date::from_ordinal_date(self.year, self.day_of_year)
.unwrap_or_else(|_| Date::from_ordinal_date(2025, 1).unwrap());
let field = GeomagneticField::new(
Length::new::<meter>(alt_m as f32),
Angle::new::<degree>(lat_deg as f32),
Angle::new::<degree>(lon_deg as f32),
date,
);
match field {
Ok(f) => f.declination().get::<degree>() as f64 * std::f64::consts::PI / 180.0,
Err(_) => 0.0, }
}
}
impl MeasurementModel for MagnetometerYawMeasurement {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn get_dimension(&self) -> usize {
1 }
fn get_measurement(&self, state: &DVector<f64>) -> DVector<f64> {
let roll = if state.len() > 6 { state[6] } else { 0.0 };
let pitch = if state.len() > 7 { state[7] } else { 0.0 };
let yaw = if state.len() > 8 { state[8] } else { 0.0 };
let attitude = Rotation3::from_euler_angles(roll, pitch, yaw);
let mag_vector = attitude * Vector3::new(self.mag_x, self.mag_y, self.mag_z);
let mut heading = mag_vector.y.atan2(mag_vector.x);
if self.apply_declination && state.len() >= 3 {
let lat_deg = state[0].to_degrees();
let lon_deg = state[1].to_degrees();
let alt_m = state[2];
let declination = self.get_declination(lat_deg, lon_deg, alt_m);
heading += declination;
heading = heading.rem_euclid(2.0 * std::f64::consts::PI);
}
DVector::from_vec(vec![heading])
}
fn get_noise(&self) -> DMatrix<f64> {
DMatrix::from_diagonal(&DVector::from_vec(vec![self.noise_std.powi(2)]))
}
fn get_expected_measurement(&self, state: &DVector<f64>) -> DVector<f64> {
let yaw = if state.len() > 8 { state[8] } else { 0.0 };
DVector::from_vec(vec![yaw])
}
fn get_jacobian(&self, state: &DVector<f64>) -> DMatrix<f64> {
let nav_state = crate::StrapdownState::try_from(&state.as_slice()[..9]).unwrap();
crate::linearize::magnetometer_yaw_jacobian(&nav_state, self.mag_x, self.mag_y, self.mag_z)
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_approx_eq::assert_approx_eq;
const EPS: f64 = 1e-12;
#[test]
fn gps_position_vector_noise_and_sigma_points() {
let meas = GPSPositionMeasurement {
latitude: 37.0,
longitude: -122.0,
altitude: 12.34,
horizontal_noise_std: 3.0,
vertical_noise_std: 2.0,
};
let dummy_state = DVector::from_vec(vec![0.0; 9]);
let vec = meas.get_measurement(&dummy_state);
assert_eq!(vec.len(), 3);
assert!((vec[0] - 37.0_f64.to_radians()).abs() < EPS);
assert!((vec[1] - (-122.0_f64).to_radians()).abs() < EPS);
assert!((vec[2] - 12.34).abs() < EPS);
let noise = meas.get_noise();
let expected_h = (3.0 * METERS_TO_DEGREES).to_radians().powi(2);
let expected_v = 2.0_f64.powi(2);
assert_eq!(noise.nrows(), 3);
assert!((noise[(0, 0)] - expected_h).abs() < EPS);
assert!((noise[(1, 1)] - expected_h).abs() < EPS);
assert!((noise[(2, 2)] - expected_v).abs() < EPS);
let state_sigma: DVector<f64> = DVector::from_vec(vec![
0.1, 1.1, 2.1, 3.0, 4.0, 5.0, ]);
let z = meas.get_expected_measurement(&state_sigma);
assert_eq!(z.len(), 3);
assert_approx_eq!(z[0], 0.1, EPS);
assert_approx_eq!(z[1], 1.1, EPS);
assert_approx_eq!(z[2], 2.1, EPS);
}
#[test]
fn gps_velocity_vector_noise_and_sigma_points() {
let meas = GPSVelocityMeasurement {
northward_velocity: 1.5,
eastward_velocity: -0.5,
vertical_velocity: 0.25,
horizontal_noise_std: 0.2,
vertical_noise_std: 0.1,
};
let dummy_state = DVector::from_vec(vec![0.0; 9]);
let vec = meas.get_measurement(&dummy_state);
assert_eq!(vec.len(), 3);
assert!((vec[0] - 1.5).abs() < EPS);
assert!((vec[1] - (-0.5)).abs() < EPS);
assert!((vec[2] - 0.25).abs() < EPS);
let noise = meas.get_noise();
assert!((noise[(0, 0)] - 0.2_f64.powi(2)).abs() < EPS);
assert!((noise[(2, 2)] - 0.1_f64.powi(2)).abs() < EPS);
let state_sigma: DVector<f64> = DVector::from_vec(vec![
0.1, 1.1, 2.1, 3.0, 4.0, 5.0, ]);
let z = meas.get_expected_measurement(&state_sigma);
assert_eq!(z.len(), 3);
assert_approx_eq!(z[0], 3.0, EPS);
assert_approx_eq!(z[1], 4.0, EPS);
assert_approx_eq!(z[2], 5.0, EPS);
}
#[test]
fn position_and_velocity_measurement_behaviour() {
let meas = GPSPositionAndVelocityMeasurement {
latitude: 10.0,
longitude: 20.0,
altitude: 100.0,
northward_velocity: 2.0,
eastward_velocity: -1.0,
horizontal_noise_std: 1.0,
vertical_noise_std: 4.0,
velocity_noise_std: 0.5,
};
let dummy_state = DVector::from_vec(vec![0.0; 9]);
let vec = meas.get_measurement(&dummy_state);
assert_eq!(vec.len(), 5);
assert!((vec[0] - 10.0_f64.to_radians()).abs() < EPS);
assert!((vec[3] - 2.0).abs() < EPS);
let noise = meas.get_noise();
assert_eq!(noise.nrows(), 5);
let state_sigma: DVector<f64> = DVector::from_vec(vec![
0.1, 1.1, 2.1, 3.0, 4.0, 5.0, ]);
let z = meas.get_expected_measurement(&state_sigma);
assert_eq!(z.len(), 5);
assert_approx_eq!(z[0], 0.1, EPS);
assert_approx_eq!(z[1], 1.1, EPS);
assert_approx_eq!(z[2], 2.1, EPS);
assert_approx_eq!(z[3], 3.0, EPS);
assert_approx_eq!(z[4], 4.0, EPS);
}
#[test]
fn relative_altitude_measurement_and_display_and_sigma() {
let meas = RelativeAltitudeMeasurement {
relative_altitude: -5.0,
reference_altitude: 100.0,
};
let dummy_state = DVector::from_vec(vec![0.0; 9]);
let vec = meas.get_measurement(&dummy_state);
assert_eq!(vec.len(), 1);
assert!((vec[0] - 95.0).abs() < EPS);
let noise = meas.get_noise();
assert_eq!(noise.nrows(), 1);
assert!((noise[(0, 0)] - 5.0).abs() < EPS);
let state_sigma: DVector<f64> = DVector::from_vec(vec![
0.1, 1.1, 50.0, 3.0, 4.0, 5.0, ]);
let z = meas.get_expected_measurement(&state_sigma);
assert_eq!(z.len(), 1);
assert_approx_eq!(z[0], 50.0, EPS);
let s = format!("{}", meas);
assert!(s.contains("rel_alt") && s.contains("ref_alt"));
}
#[test]
fn downcast_trait_object_and_display() {
let pos = GPSPositionMeasurement::default();
let boxed: Box<dyn MeasurementModel> = Box::new(pos.clone());
let any = boxed.as_any();
let down = any
.downcast_ref::<GPSPositionMeasurement>()
.expect("downcast failed");
assert!((down.latitude).abs() < EPS);
let s = format!("{}", down);
assert!(s.contains("GPSPositionMeasurement"));
}
#[test]
fn negative_and_zero_std_are_handled() {
let meas = GPSVelocityMeasurement {
northward_velocity: 0.0,
eastward_velocity: 0.0,
vertical_velocity: 0.0,
horizontal_noise_std: -2.0,
vertical_noise_std: 0.0,
};
let noise = meas.get_noise();
assert!((noise[(0, 0)] - 4.0).abs() < EPS);
assert!((noise[(2, 2)] - 0.0).abs() < EPS);
}
#[test]
fn magnetometer_yaw_measurement_level_attitude() {
let meas = MagnetometerYawMeasurement {
mag_x: 20.0, mag_y: 0.0,
mag_z: -45.0, noise_std: 0.05,
apply_declination: false,
year: 2025,
day_of_year: 1,
};
let state = DVector::from_vec(vec![
std::f64::consts::FRAC_PI_4, -2.1293, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ]);
let z = meas.get_measurement(&state);
assert_eq!(z.len(), 1);
assert!(z[0].abs() < 0.01, "Expected heading near 0, got {}", z[0]);
}
#[test]
fn magnetometer_yaw_measurement_east_heading() {
let meas = MagnetometerYawMeasurement {
mag_x: 0.0,
mag_y: 20.0, mag_z: -45.0,
noise_std: 0.05,
apply_declination: false,
year: 2025,
day_of_year: 1,
};
let state = DVector::from_vec(vec![
std::f64::consts::FRAC_PI_4, -2.1293, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ]);
let z = meas.get_measurement(&state);
let expected = std::f64::consts::FRAC_PI_2;
assert!(
(z[0] - expected).abs() < 0.01,
"Expected heading near π/2, got {}",
z[0]
);
}
#[test]
fn magnetometer_yaw_tilt_compensation() {
let meas = MagnetometerYawMeasurement {
mag_x: 20.0,
mag_y: 5.0,
mag_z: -45.0,
noise_std: 0.05,
apply_declination: false,
year: 2025,
day_of_year: 1,
};
let level_state =
DVector::from_vec(vec![std::f64::consts::FRAC_PI_4, -2.1293, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
let tilted_state = DVector::from_vec(vec![
std::f64::consts::FRAC_PI_4, -2.1293, 100.0, 0.0, 0.0, 0.0, 0.1745, 0.0, 0.0, ]);
let z_level = meas.get_measurement(&level_state);
let z_tilted = meas.get_measurement(&tilted_state);
assert!(
(z_level[0] - z_tilted[0]).abs() > 0.001,
"Tilt compensation should change heading"
);
}
#[test]
fn magnetometer_yaw_expected_measurement() {
let meas = MagnetometerYawMeasurement::default();
let state = DVector::from_vec(vec![
std::f64::consts::FRAC_PI_4, -2.1293, 100.0, 0.0, 0.0, 0.0, 0.1, 0.2, std::f64::consts::FRAC_PI_2, ]);
let expected = meas.get_expected_measurement(&state);
assert_eq!(expected.len(), 1);
assert!(
(expected[0] - std::f64::consts::FRAC_PI_2).abs() < EPS,
"Expected measurement should be state yaw"
);
}
#[test]
fn magnetometer_yaw_noise_matrix() {
let meas = MagnetometerYawMeasurement {
noise_std: 0.1,
..Default::default()
};
let noise = meas.get_noise();
assert_eq!(noise.nrows(), 1);
assert_eq!(noise.ncols(), 1);
assert!(
(noise[(0, 0)] - 0.01).abs() < EPS,
"Noise variance should be 0.1^2 = 0.01"
);
}
#[test]
fn magnetometer_yaw_display() {
let meas = MagnetometerYawMeasurement {
mag_x: 20.0,
mag_y: 5.0,
mag_z: -45.0,
noise_std: 0.05,
apply_declination: true,
year: 2025,
day_of_year: 1,
};
let s = format!("{}", meas);
assert!(s.contains("MagnetometerYawMeasurement"));
assert!(s.contains("20.00"));
assert!(s.contains("true")); }
#[test]
fn magnetometer_yaw_downcast() {
let meas = MagnetometerYawMeasurement::default();
let boxed: Box<dyn MeasurementModel> = Box::new(meas);
let any = boxed.as_any();
let down = any.downcast_ref::<MagnetometerYawMeasurement>();
assert!(
down.is_some(),
"Should be able to downcast MagnetometerYawMeasurement"
);
}
}