pub mod earth;
pub mod kalman;
pub mod linalg;
pub mod linearize;
pub mod measurements;
pub mod messages;
pub mod particle;
pub mod rbpf;
pub mod sim;
use nalgebra::{DMatrix, DVector, Matrix3, Rotation3, Vector3, Vector6};
use std::any::Any;
use std::convert::{From, Into, TryFrom};
use std::fmt::{self, Debug, Display};
#[cfg(test)]
use crate::measurements::GPSPositionMeasurement;
use crate::measurements::MeasurementModel;
pub trait NavigationFilter {
fn predict<C: InputModel>(&mut self, control_input: &C, dt: f64);
fn update<M: MeasurementModel + ?Sized>(&mut self, measurement: &M);
fn get_estimate(&self) -> DVector<f64>;
fn get_certainty(&self) -> DMatrix<f64>;
}
pub trait InputModel {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn get_dimension(&self) -> usize;
fn get_vector(&self) -> DVector<f64>;
}
#[derive(Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
pub enum IMUQuality {
#[default]
Consumer,
Industrial,
Tactical,
Navigation,
Strategic,
}
impl IMUQuality {
pub fn gyro_bias_instability_dph(&self) -> f64 {
match self {
IMUQuality::Consumer => 100.0_f64.to_radians(),
IMUQuality::Industrial => 50.0_f64.to_radians(),
IMUQuality::Tactical => 1.0_f64.to_radians(),
IMUQuality::Navigation => 0.01_f64.to_radians(),
IMUQuality::Strategic => 0.0001_f64.to_radians(),
}
}
pub fn gyro_angle_random_walk(&self) -> f64 {
match self {
IMUQuality::Consumer => 1.0_f64.to_radians(),
IMUQuality::Industrial => 0.1_f64.to_radians(),
IMUQuality::Tactical => 0.01_f64.to_radians(),
IMUQuality::Navigation => 0.005_f64.to_radians(),
IMUQuality::Strategic => 0.0005_f64.to_radians(),
}
}
pub fn accel_bias_instability_mps2(&self) -> f64 {
match self {
IMUQuality::Consumer => 0.1,
IMUQuality::Industrial => 0.05,
IMUQuality::Tactical => 0.001,
IMUQuality::Navigation => 0.0001,
IMUQuality::Strategic => 0.00001,
}
}
pub fn accel_velocity_random_walk(&self) -> f64 {
match self {
IMUQuality::Consumer => 0.1,
IMUQuality::Industrial => 0.03,
IMUQuality::Tactical => 0.01,
IMUQuality::Navigation => 0.005,
IMUQuality::Strategic => 0.0001,
}
}
pub fn gyro_process_noise(&self) -> Matrix3<f64> {
Matrix3::<f64>::identity() * self.gyro_bias_instability_dph().powi(2)
}
pub fn accel_process_noise(&self) -> Matrix3<f64> {
Matrix3::<f64>::identity() * self.accel_bias_instability_mps2().powi(2)
}
pub fn velocity_process_noise(&self) -> Matrix3<f64> {
Matrix3::<f64>::identity() * self.accel_velocity_random_walk().powi(2)
}
pub fn attitude_process_noise(&self) -> Matrix3<f64> {
Matrix3::<f64>::identity() * self.gyro_angle_random_walk().powi(2)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct IMUData {
pub accel: Vector3<f64>,
pub gyro: Vector3<f64>,
}
impl Display for IMUData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"IMUData {{ accel: [{:.4}, {:.4}, {:.4}], gyro: [{:.4}, {:.4}, {:.4}] }}",
self.accel[0], self.accel[1], self.accel[2], self.gyro[0], self.gyro[1], self.gyro[2]
)
}
}
impl From<Vec<f64>> for IMUData {
fn from(vec: Vec<f64>) -> Self {
if vec.len() != 6 {
panic!(
"IMUData must be initialized with a vector of length 6 (3 for accel, 3 for gyro)"
);
}
IMUData {
accel: Vector3::new(vec[0], vec[1], vec[2]),
gyro: Vector3::new(vec[3], vec[4], vec[5]),
}
}
}
impl From<IMUData> for Vec<f64> {
fn from(data: IMUData) -> Self {
vec![
data.accel[0],
data.accel[1],
data.accel[2],
data.gyro[0],
data.gyro[1],
data.gyro[2],
]
}
}
impl InputModel for IMUData {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn get_dimension(&self) -> usize {
6
}
fn get_vector(&self) -> DVector<f64> {
DVector::from_vec(self.accel.iter().chain(self.gyro.iter()).cloned().collect())
}
}
#[derive(Copy, Clone, Debug, Default)]
pub struct VelocityData {
pub linear: Vector3<f64>,
pub angular: Vector3<f64>,
}
impl From<Vec<f64>> for VelocityData {
fn from(data: Vec<f64>) -> Self {
if data.len() != 6 {
panic!(
"VelocityData must be initialized with a vector of length 6 (3 for linear, 3 for angular)"
);
}
VelocityData {
linear: Vector3::new(data[0], data[1], data[2]),
angular: Vector3::new(data[3], data[4], data[5]),
}
}
}
impl From<(Vector3<f64>, Vector3<f64>)> for VelocityData {
fn from(data: (Vector3<f64>, Vector3<f64>)) -> Self {
VelocityData {
linear: data.0,
angular: data.1,
}
}
}
impl From<Vector6<f64>> for VelocityData {
fn from(data: Vector6<f64>) -> Self {
VelocityData {
linear: Vector3::new(data[0], data[1], data[2]),
angular: Vector3::new(data[3], data[4], data[5]),
}
}
}
impl InputModel for VelocityData {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn get_dimension(&self) -> usize {
6
}
fn get_vector(&self) -> DVector<f64> {
DVector::from_vec(
self.linear
.iter()
.chain(self.angular.iter())
.cloned()
.collect(),
)
}
}
#[derive(Clone, Copy)]
pub struct StrapdownState {
pub latitude: f64,
pub longitude: f64,
pub altitude: f64,
pub velocity_north: f64,
pub velocity_east: f64,
pub velocity_vertical: f64,
pub attitude: Rotation3<f64>,
pub is_enu: bool,
}
impl Debug for StrapdownState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (roll, pitch, yaw) = self.attitude.euler_angles();
f.debug_struct("StrapdownState")
.field("latitude (deg)", &self.latitude.to_degrees())
.field("longitude (deg)", &self.longitude.to_degrees())
.field("altitude (m)", &self.altitude)
.field("velocity_north (m/s)", &self.velocity_north)
.field("velocity_east (m/s)", &self.velocity_east)
.field("velocity_vertical (m/s)", &self.velocity_vertical)
.field(
"attitude (roll, pitch, yaw in deg)",
&format_args!(
"[{:.2}, {:.2}, {:.2}]",
roll.to_degrees(),
pitch.to_degrees(),
yaw.to_degrees()
),
)
.finish()
}
}
impl Display for StrapdownState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (roll, pitch, yaw) = self.attitude.euler_angles();
write!(
f,
"StrapdownState {{ lat: {:.4} deg, lon: {:.4} deg, alt: {:.2} m, v_n: {:.3} m/s, v_e: {:.3} m/s, v_d: {:.3} m/s, attitude: [{:.2} deg, {:.2} deg, {:.2} deg] }}",
self.latitude.to_degrees(),
self.longitude.to_degrees(),
self.altitude,
self.velocity_north,
self.velocity_east,
self.velocity_vertical,
roll.to_degrees(),
pitch.to_degrees(),
yaw.to_degrees()
)
}
}
impl Default for StrapdownState {
fn default() -> Self {
StrapdownState {
latitude: 0.0,
longitude: 0.0,
altitude: 0.0,
velocity_north: 0.0,
velocity_east: 0.0,
velocity_vertical: 0.0,
attitude: Rotation3::identity(),
is_enu: true,
}
}
}
impl StrapdownState {
#[allow(clippy::too_many_arguments)]
pub fn new(
latitude: f64,
longitude: f64,
altitude: f64,
velocity_north: f64,
velocity_east: f64,
velocity_vertical: f64,
attitude: Rotation3<f64>,
in_degrees: bool,
is_enu: Option<bool>,
) -> StrapdownState {
let latitude = if in_degrees {
latitude.to_radians()
} else {
latitude
};
let longitude = if in_degrees {
longitude.to_radians()
} else {
longitude
};
assert!(
(-std::f64::consts::PI..=std::f64::consts::PI).contains(&latitude),
"Latitude must be in the range [-π, π]"
);
assert!(
(-std::f64::consts::PI..=std::f64::consts::PI).contains(&longitude),
"Longitude must be in the range [-π, π]"
);
assert!(
(-30_000.0..=30_000.0).contains(&altitude),
"Strapdown equations and the local level frame are only valid within 30 km above mean sea level and maximum ocean depth is ~11 km. Given altitude: {} m, please check your input and sign conventions.",
altitude
);
StrapdownState {
latitude,
longitude,
altitude,
velocity_north,
velocity_east,
velocity_vertical,
attitude,
is_enu: is_enu.unwrap_or(true),
}
}
}
impl From<StrapdownState> for Vec<f64> {
fn from(state: StrapdownState) -> Self {
let (roll, pitch, yaw) = state.attitude.euler_angles();
vec![
state.latitude,
state.longitude,
state.altitude,
state.velocity_north,
state.velocity_east,
state.velocity_vertical,
roll,
pitch,
yaw,
]
}
}
impl From<&StrapdownState> for Vec<f64> {
fn from(state: &StrapdownState) -> Self {
let (roll, pitch, yaw) = state.attitude.euler_angles();
vec![
state.latitude,
state.longitude,
state.altitude,
state.velocity_north,
state.velocity_east,
state.velocity_vertical,
roll,
pitch,
yaw,
]
}
}
impl TryFrom<&[f64]> for StrapdownState {
type Error = &'static str;
fn try_from(slice: &[f64]) -> Result<Self, Self::Error> {
if slice.len() != 9 {
return Err("Slice must have length 9 for StrapdownState");
}
let attitude = Rotation3::from_euler_angles(slice[6], slice[7], slice[8]);
Ok(StrapdownState::new(
slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], attitude,
false, None,
))
}
}
impl TryFrom<Vec<f64>> for StrapdownState {
type Error = &'static str;
fn try_from(vec: Vec<f64>) -> Result<Self, Self::Error> {
Self::try_from(vec.as_slice())
}
}
impl From<StrapdownState> for DVector<f64> {
fn from(state: StrapdownState) -> Self {
DVector::from_vec(state.into())
}
}
impl From<&StrapdownState> for DVector<f64> {
fn from(state: &StrapdownState) -> Self {
DVector::from_vec(state.into())
}
}
pub fn forward(state: &mut StrapdownState, imu_data: IMUData, dt: f64) {
let c_0: Rotation3<f64> = state.attitude;
let c_1: Matrix3<f64> = attitude_update(state, imu_data.gyro, dt);
let f: Vector3<f64> = 0.5 * (c_0.matrix() + c_1) * imu_data.accel;
let velocity = velocity_update(state, f, dt);
let (lat_1, lon_1, alt_1) = position_update(state, velocity, dt);
state.attitude = Rotation3::from_matrix(&c_1);
state.velocity_north = velocity[0];
state.velocity_east = velocity[1];
state.velocity_vertical = velocity[2];
state.latitude = lat_1;
state.longitude = lon_1;
state.altitude = alt_1;
}
pub fn attitude_update(state: &StrapdownState, gyros: Vector3<f64>, dt: f64) -> Matrix3<f64> {
let transport_rate: Matrix3<f64> = earth::vector_to_skew_symmetric(&earth::transport_rate(
&state.latitude.to_degrees(),
&state.altitude,
&Vector3::from_vec(vec![
state.velocity_north,
state.velocity_east,
state.velocity_vertical,
]),
));
let rotation_rate: Matrix3<f64> =
earth::vector_to_skew_symmetric(&earth::earth_rate_lla(&state.latitude.to_degrees()));
let omega_ib: Matrix3<f64> = earth::vector_to_skew_symmetric(&gyros);
let c_1: Matrix3<f64> = state.attitude * (Matrix3::identity() + omega_ib * dt)
- (rotation_rate + transport_rate) * state.attitude * dt;
c_1
}
fn velocity_update(state: &StrapdownState, specific_force: Vector3<f64>, dt: f64) -> Vector3<f64> {
let transport_rate: Matrix3<f64> = earth::vector_to_skew_symmetric(&earth::transport_rate(
&state.latitude.to_degrees(),
&state.altitude,
&Vector3::from_vec(vec![
state.velocity_north,
state.velocity_east,
state.velocity_vertical,
]),
));
let rotation_rate: Matrix3<f64> =
earth::vector_to_skew_symmetric(&earth::earth_rate_lla(&state.latitude.to_degrees()));
let r = earth::ecef_to_lla(&state.latitude.to_degrees(), &state.longitude.to_degrees());
let velocity: Vector3<f64> = Vector3::new(
state.velocity_north,
state.velocity_east,
state.velocity_vertical,
);
let gravity = Vector3::new(
0.0,
0.0,
earth::gravity(&state.latitude.to_degrees(), &state.altitude),
);
let gravity = if state.is_enu { -gravity } else { gravity };
velocity
+ (specific_force + gravity - r * (transport_rate + 2.0 * rotation_rate) * velocity) * dt
}
pub fn position_update(state: &StrapdownState, velocity: Vector3<f64>, dt: f64) -> (f64, f64, f64) {
let (r_n, r_e_0, _) = earth::principal_radii(&state.latitude, &state.altitude);
let lat_0 = state.latitude;
let alt_0 = state.altitude;
let alt_1 = alt_0 + 0.5 * (state.velocity_vertical + velocity[2]) * dt;
let lat_1: f64 = state.latitude
+ 0.5 * (state.velocity_north / (r_n + state.altitude) + velocity[0] / (r_n + alt_1)) * dt;
let (_, r_e_1, _) = earth::principal_radii(&lat_1, &alt_1);
let cos_lat0 = lat_0.cos().max(1e-6); let cos_lat1 = lat_1.cos().max(1e-6);
let lon_1: f64 = state.longitude
+ 0.5
* (state.velocity_east / ((r_e_0 + alt_0) * cos_lat0)
+ velocity[1] / ((r_e_1 + alt_1) * cos_lat1))
* dt;
(
wrap_latitude(lat_1.to_degrees()).to_radians(),
wrap_to_pi(lon_1),
alt_1,
)
}
pub fn wrap_to_180<T>(angle: T) -> T
where
T: PartialOrd + Copy + std::ops::SubAssign + std::ops::AddAssign + From<f64>,
{
let mut wrapped: T = angle;
while wrapped > T::from(180.0) {
wrapped -= T::from(360.0);
}
while wrapped < T::from(-180.0) {
wrapped += T::from(360.0);
}
wrapped
}
pub fn wrap_to_360<T>(angle: T) -> T
where
T: PartialOrd + Copy + std::ops::SubAssign + std::ops::AddAssign + From<f64>,
{
let mut wrapped: T = angle;
while wrapped > T::from(360.0) {
wrapped -= T::from(360.0);
}
while wrapped < T::from(0.0) {
wrapped += T::from(360.0);
}
wrapped
}
pub fn wrap_to_pi<T>(angle: T) -> T
where
T: PartialOrd + Copy + std::ops::SubAssign + std::ops::AddAssign + From<f64>,
{
let mut wrapped: T = angle;
while wrapped > T::from(std::f64::consts::PI) {
wrapped -= T::from(2.0 * std::f64::consts::PI);
}
while wrapped < T::from(-std::f64::consts::PI) {
wrapped += T::from(2.0 * std::f64::consts::PI);
}
wrapped
}
pub fn wrap_to_2pi<T>(angle: T) -> T
where
T: PartialOrd + Copy + std::ops::SubAssign + std::ops::AddAssign + From<f64>,
T: PartialOrd + Copy + std::ops::SubAssign + std::ops::AddAssign + From<i32>,
{
let mut wrapped: T = angle;
while wrapped > T::from(2.0 * std::f64::consts::PI) {
wrapped -= T::from(2.0 * std::f64::consts::PI);
}
while wrapped < T::from(0.0) {
wrapped += T::from(2.0 * std::f64::consts::PI);
}
wrapped
}
pub fn wrap_latitude<T>(latitude: T) -> T
where
T: PartialOrd + Copy + std::ops::SubAssign + std::ops::AddAssign + From<f64>,
{
let mut wrapped: T = latitude;
while wrapped > T::from(90.0) {
wrapped -= T::from(180.0);
}
while wrapped < T::from(-90.0) {
wrapped += T::from(180.0);
}
wrapped
}
#[cfg(test)]
pub(crate) fn calculate_constant_velocity_acceleration(
state: &StrapdownState,
target_velocity: Vector3<f64>,
) -> Vector3<f64> {
let transport_rate = earth::vector_to_skew_symmetric(&earth::transport_rate(
&state.latitude.to_degrees(),
&state.altitude,
&target_velocity,
));
let rotation_rate =
earth::vector_to_skew_symmetric(&earth::earth_rate_lla(&state.latitude.to_degrees()));
let r = earth::ecef_to_lla(&state.latitude.to_degrees(), &state.longitude.to_degrees());
let mut gravity = Vector3::new(
0.0,
0.0,
earth::gravity(&state.latitude.to_degrees(), &state.altitude),
);
gravity = if state.is_enu { -gravity } else { gravity };
r * (transport_rate + 2.0 * rotation_rate) * target_velocity - gravity
}
#[cfg(test)]
#[allow(clippy::too_many_arguments)]
pub fn generate_scenario_data(
initial_state: StrapdownState,
duration_seconds: usize,
sample_rate_hz: usize,
accel_body: Vector3<f64>,
gyro_body: Vector3<f64>,
geosynchronous: bool,
constant_velocity: bool,
_coords_in_degrees: bool, ) -> (
Vec<IMUData>,
Vec<GPSPositionMeasurement>,
Vec<StrapdownState>,
) {
let num_samples = duration_seconds * sample_rate_hz;
let dt = 1.0 / sample_rate_hz as f64;
let mut imu_data = Vec::with_capacity(num_samples);
let mut gps_measurements = Vec::with_capacity(num_samples);
let mut true_states = Vec::with_capacity(num_samples);
let mut current_state = initial_state;
for i in 0..num_samples {
true_states.push(current_state);
let gps_meas = GPSPositionMeasurement {
latitude: current_state.latitude.to_degrees(),
longitude: current_state.longitude.to_degrees(),
altitude: current_state.altitude,
horizontal_noise_std: 5.0 * earth::METERS_TO_DEGREES,
vertical_noise_std: 2.0,
};
gps_measurements.push(gps_meas.clone());
let accel_nav = if constant_velocity {
let target_velocity = Vector3::new(
initial_state.velocity_north,
initial_state.velocity_east,
initial_state.velocity_vertical,
);
calculate_constant_velocity_acceleration(¤t_state, target_velocity)
} else {
current_state.attitude * accel_body
};
let accel_body_actual = current_state.attitude.inverse() * accel_nav;
let gyro_total = if geosynchronous {
let earth_rate = earth::earth_rate_lla(¤t_state.latitude.to_degrees());
let earth_rate_body = current_state.attitude.inverse() * earth_rate;
gyro_body + earth_rate_body
} else {
gyro_body
};
let imu = IMUData {
accel: accel_body_actual,
gyro: gyro_total,
};
imu_data.push(imu);
let c_0 = current_state.attitude;
let c_1 = attitude_update(¤t_state, gyro_total, dt);
let f = 0.5 * (c_0.matrix() + c_1) * accel_body_actual;
let velocity = velocity_update(¤t_state, f, dt);
let (lat_1, lon_1, alt_1) = position_update(¤t_state, velocity, dt);
if i % (60 * sample_rate_hz) == 0 {
println!(
"Time: {}s, IMU Accel: ({:.4}, {:.4}, {:.4}) m/s² | Gyro: ({:.4}, {:.4}, {:.4}) rad/s | GPS Pos: ({:.3}°, {:.3}°, {:.1}m) | Velocities: N: {:.3} m/s, E: {:.3} m/s, V: {:.3} m/s",
i / sample_rate_hz,
imu.accel[0],
imu.accel[1],
imu.accel[2],
imu.gyro[0],
imu.gyro[1],
imu.gyro[2],
gps_meas.latitude,
gps_meas.longitude,
gps_meas.altitude,
velocity[0],
velocity[1],
velocity[2]
);
}
current_state.latitude = lat_1;
current_state.longitude = lon_1;
current_state.altitude = alt_1;
current_state.velocity_north = velocity[0];
current_state.velocity_east = velocity[1];
current_state.velocity_vertical = velocity[2];
current_state.attitude = Rotation3::from_matrix(&c_1);
}
(imu_data, gps_measurements, true_states)
}
#[cfg(test)]
mod tests {
use super::*;
use assert_approx_eq::assert_approx_eq;
#[test]
fn test_wrap_to_180() {
assert_eq!(super::wrap_to_180(190.0), -170.0);
assert_eq!(super::wrap_to_180(-190.0), 170.0);
assert_eq!(super::wrap_to_180(0.0), 0.0);
assert_eq!(super::wrap_to_180(180.0), 180.0);
assert_eq!(super::wrap_to_180(-180.0), -180.0);
}
#[test]
fn test_wrap_to_360() {
assert_eq!(super::wrap_to_360(370.0), 10.0);
assert_eq!(super::wrap_to_360(-10.0), 350.0);
assert_eq!(super::wrap_to_360(0.0), 0.0);
}
#[test]
fn test_wrap_to_pi() {
assert_eq!(
super::wrap_to_pi(3.0 * std::f64::consts::PI),
std::f64::consts::PI
);
assert_eq!(
super::wrap_to_pi(-3.0 * std::f64::consts::PI),
-std::f64::consts::PI
);
assert_eq!(super::wrap_to_pi(0.0), 0.0);
assert_eq!(
super::wrap_to_pi(std::f64::consts::PI),
std::f64::consts::PI
);
assert_eq!(
super::wrap_to_pi(-std::f64::consts::PI),
-std::f64::consts::PI
);
}
#[test]
fn test_wrap_to_2pi() {
assert_eq!(
super::wrap_to_2pi(7.0 * std::f64::consts::PI),
std::f64::consts::PI
);
assert_eq!(
super::wrap_to_2pi(-5.0 * std::f64::consts::PI),
std::f64::consts::PI
);
assert_eq!(super::wrap_to_2pi(0.0), 0.0);
assert_eq!(
super::wrap_to_2pi(std::f64::consts::PI),
std::f64::consts::PI
);
assert_eq!(
super::wrap_to_2pi(-std::f64::consts::PI),
std::f64::consts::PI
);
}
#[test]
fn test_strapdown_state_new() {
let state = StrapdownState::default();
assert_eq!(state.latitude, 0.0);
assert_eq!(state.longitude, 0.0);
assert_eq!(state.altitude, 0.0);
assert_eq!(state.velocity_north, 0.0);
assert_eq!(state.velocity_east, 0.0);
assert_eq!(state.velocity_vertical, 0.0);
assert_eq!(state.attitude, Rotation3::identity());
}
#[test]
fn test_to_vector_zeros() {
let state = StrapdownState::default();
let state_vector: Vec<f64> = state.into();
let zeros = vec![0.0; 9];
assert_eq!(state_vector, zeros);
}
#[test]
fn test_new_from_vector() {
let roll: f64 = 15.0;
let pitch: f64 = 45.0;
let yaw: f64 = 90.0;
let state_vector = vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0, roll, pitch, yaw];
let state = StrapdownState::try_from(state_vector).unwrap();
assert_eq!(state.latitude, 0.0);
assert_eq!(state.longitude, 0.0);
assert_eq!(state.altitude, 0.0);
assert_eq!(state.velocity_north, 0.0);
}
#[test]
fn test_dcm_to_vector() {
let state = StrapdownState::default();
let state_vector: Vec<f64> = (&state).into();
assert_eq!(state_vector.len(), 9);
assert_eq!(state_vector, vec![0.0; 9]);
}
#[test]
fn test_attitude_matrix_euler_consistency() {
let state = StrapdownState::default();
let (roll, pitch, yaw) = state.attitude.euler_angles();
let state_vector: Vec<f64> = state.into();
assert_eq!(state_vector[6], roll);
assert_eq!(state_vector[7], pitch);
assert_eq!(state_vector[8], yaw);
}
#[test]
fn rest() {
let attitude = Rotation3::identity();
let mut state = StrapdownState::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, attitude, false, None);
assert_eq!(state.velocity_north, 0.0);
assert_eq!(state.velocity_east, 0.0);
assert_eq!(state.velocity_vertical, 0.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), };
let dt = 1.0; forward(&mut state, imu_data, dt);
assert_approx_eq!(state.latitude, 0.0, 1e-6);
assert_approx_eq!(state.longitude, 0.0, 1e-6);
assert_approx_eq!(state.altitude, 0.0, 0.1);
assert_approx_eq!(state.velocity_north, 0.0, 1e-3);
assert_approx_eq!(state.velocity_east, 0.0, 1e-3);
assert_approx_eq!(state.velocity_vertical, 0.0, 0.1);
let attitude = state.attitude.matrix() - Rotation3::identity().matrix();
assert_approx_eq!(attitude[(0, 0)], 0.0, 1e-3);
assert_approx_eq!(attitude[(0, 1)], 0.0, 1e-3);
assert_approx_eq!(attitude[(0, 2)], 0.0, 1e-3);
assert_approx_eq!(attitude[(1, 0)], 0.0, 1e-3);
assert_approx_eq!(attitude[(1, 1)], 0.0, 1e-3);
assert_approx_eq!(attitude[(1, 2)], 0.0, 1e-3);
assert_approx_eq!(attitude[(2, 0)], 0.0, 1e-3);
assert_approx_eq!(attitude[(2, 1)], 0.0, 1e-3);
assert_approx_eq!(attitude[(2, 2)], 0.0, 1e-3);
}
#[test]
fn yawing() {
let attitude = Rotation3::from_euler_angles(0.0, 0.0, 0.1); let state = StrapdownState::new(
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, attitude, false, None, );
assert_approx_eq!(state.attitude.euler_angles().2, 0.1, 1e-6); let gyros = Vector3::new(0.0, 0.0, 0.1); let dt = 1.0; let new_attitude = Rotation3::from_matrix(&attitude_update(&state, gyros, dt));
let new_yaw = new_attitude.euler_angles().2;
assert_approx_eq!(new_yaw, 0.1 + 0.1, 1e-3); }
#[test]
fn rolling() {
let attitude = Rotation3::from_euler_angles(0.1, 0.0, 0.0); let state = StrapdownState::new(
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, attitude, false, None, );
assert_approx_eq!(state.attitude.euler_angles().0, 0.1, 1e-6); let gyros = Vector3::new(0.10, 0.0, 0.0); let dt = 1.0; let new_attitude = Rotation3::from_matrix(&attitude_update(&state, gyros, dt));
let new_roll = new_attitude.euler_angles().0;
assert_approx_eq!(new_roll, 0.1 + 0.1, 1e-3); }
#[test]
fn pitching() {
let attitude = Rotation3::from_euler_angles(0.0, 0.1, 0.0); let state = StrapdownState::new(
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, attitude, false, None, );
assert_approx_eq!(state.attitude.euler_angles().1, 0.1, 1e-6); let gyros = Vector3::new(0.0, 0.1, 0.0); let dt = 1.0; let new_attitude = Rotation3::from_matrix(&attitude_update(&state, gyros, dt));
let new_pitch = new_attitude.euler_angles().1;
assert_approx_eq!(new_pitch, 0.1 + 0.1, 1e-3); }
#[test]
fn test_velocity_update_zero_force() {
let state = StrapdownState::default();
let f = nalgebra::Vector3::new(
0.0,
0.0,
earth::gravity(&0.0, &0.0), );
let dt = 1.0;
let v_new = velocity_update(&state, f, dt);
assert_eq!(v_new[0], 0.0);
assert_eq!(v_new[1], 0.0);
assert_eq!(v_new[2], 0.0);
}
#[test]
fn test_velocity_update_constant_force() {
let state = StrapdownState::default();
let f = nalgebra::Vector3::new(1.0, 0.0, earth::gravity(&0.0, &0.0)); let dt = 2.0;
let v_new = velocity_update(&state, f, dt);
assert!((v_new[0] - 2.0).abs() < 1e-6);
assert!((v_new[1]).abs() < 1e-6);
assert!((v_new[2]).abs() < 1e-6);
}
#[test]
fn test_velocity_update_initial_velocity() {
let state = StrapdownState {
velocity_north: 5.0,
velocity_east: -3.0,
velocity_vertical: 2.0,
..Default::default()
};
let f = Vector3::from_vec(vec![0.0, 0.0, earth::gravity(&0.0, &0.0)]);
let dt = 1.0;
let v_new = velocity_update(&state, f, dt);
assert_approx_eq!(v_new[0], 5.0, 1e-3);
assert_approx_eq!(v_new[1], -3.0, 1e-3);
assert_approx_eq!(v_new[2], 2.0, 1e-3);
}
#[test]
fn test_freefall() {
let attitude = Rotation3::identity();
let state = StrapdownState::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, attitude, false, None);
assert_eq!(state.latitude, 0.0);
assert_eq!(state.longitude, 0.0);
assert_eq!(state.altitude, 0.0);
assert_eq!(state.velocity_north, 0.0);
assert_eq!(state.velocity_east, 0.0);
assert_eq!(state.velocity_vertical, 0.0);
assert_eq!(state.attitude, Rotation3::identity());
let f = Vector3::from_vec(vec![0.0, 0.0, 0.0]); let dt = 1.0;
let v_new = velocity_update(&state, f, dt);
assert_approx_eq!(v_new[0], 0.0, 1e-3);
assert_approx_eq!(v_new[1], 0.0, 1e-3);
assert_approx_eq!(v_new[2], -earth::gravity(&0.0, &0.0), 1e-3);
let p_new = position_update(&state, v_new, dt);
assert_approx_eq!(p_new.0, 0.0, 1e-3);
assert_approx_eq!(p_new.1, 0.0, 1e-3);
assert_approx_eq!(p_new.2, -0.5 * earth::gravity(&0.0, &0.0), 1e-3); }
#[test]
fn vertical_acceleration() {
let state = StrapdownState::default();
let f = Vector3::from_vec(vec![0.0, 0.0, 2.0 * earth::gravity(&0.0, &0.0)]); let dt = 1.0;
let v_new = velocity_update(&state, f, dt);
assert_approx_eq!(v_new[2], earth::gravity(&0.0, &0.0), 1e-3);
let p_new = position_update(&state, v_new, dt);
assert_approx_eq!(p_new.2, 0.5 * earth::gravity(&0.0, &0.0), 1e-3); }
#[test]
fn test_forward_yawing() {
let attitude = nalgebra::Rotation3::identity();
let mut state = StrapdownState::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, attitude, false, None);
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, 0.0),
gyro: Vector3::new(0.0, 0.0, 0.1), };
let dt = 1.0;
forward(&mut state, imu_data, dt);
let (_, _, yaw) = state.attitude.euler_angles();
assert!((yaw - 0.1).abs() < 1e-3);
}
#[test]
fn test_forward_rolling() {
let attitude = nalgebra::Rotation3::identity();
let mut state = StrapdownState::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, attitude, false, None);
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, 0.0),
gyro: Vector3::new(0.1, 0.0, 0.0), };
let dt = 1.0;
forward(&mut state, imu_data, dt);
let roll = state.attitude.euler_angles().0;
assert_approx_eq!(roll, 0.1, 1e-3);
}
#[test]
fn test_forward_pitching() {
let attitude = nalgebra::Rotation3::identity();
let mut state = StrapdownState::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, attitude, false, None);
let imu_data = IMUData {
accel: Vector3::new(0.0, 0.0, 0.0),
gyro: Vector3::new(0.0, 0.1, 0.0), };
let dt = 1.0;
forward(&mut state, imu_data, dt);
let (_, pitch, _) = state.attitude.euler_angles();
assert_approx_eq!(pitch, 0.1, 1e-3); }
#[test]
fn test_imudata_display() {
let imu = IMUData {
accel: Vector3::new(1.0, 2.0, 3.0),
gyro: Vector3::new(0.1, 0.2, 0.3),
};
let display_str = format!("{}", imu);
assert!(display_str.contains("1.0000"));
assert!(display_str.contains("2.0000"));
assert!(display_str.contains("3.0000"));
assert!(display_str.contains("0.1000"));
}
#[test]
fn test_imudata_from_vec() {
let vec = vec![1.0, 2.0, 3.0, 0.1, 0.2, 0.3];
let imu: IMUData = vec.into();
assert_eq!(imu.accel[0], 1.0);
assert_eq!(imu.accel[1], 2.0);
assert_eq!(imu.accel[2], 3.0);
assert_eq!(imu.gyro[0], 0.1);
assert_eq!(imu.gyro[1], 0.2);
assert_eq!(imu.gyro[2], 0.3);
}
#[test]
#[should_panic(expected = "IMUData must be initialized with a vector of length 6")]
fn test_imudata_from_vec_wrong_length() {
let vec = vec![1.0, 2.0, 3.0];
let _imu: IMUData = vec.into();
}
#[test]
fn test_imudata_to_vec() {
let imu = IMUData {
accel: Vector3::new(1.0, 2.0, 3.0),
gyro: Vector3::new(0.1, 0.2, 0.3),
};
let vec: Vec<f64> = imu.into();
assert_eq!(vec, vec![1.0, 2.0, 3.0, 0.1, 0.2, 0.3]);
}
#[test]
fn test_strapdown_state_debug() {
let attitude = Rotation3::from_euler_angles(0.1, 0.2, 0.3);
let state = StrapdownState::new(45.0, -122.0, 100.0, 1.0, 2.0, 3.0, attitude, true, None);
let debug_str = format!("{:?}", state);
assert!(debug_str.contains("StrapdownState"));
assert!(debug_str.contains("latitude"));
assert!(debug_str.contains("45"));
}
#[test]
fn test_strapdown_state_display() {
let attitude = Rotation3::from_euler_angles(0.1, 0.2, 0.3);
let state = StrapdownState::new(45.0, -122.0, 100.0, 1.0, 2.0, 3.0, attitude, true, None);
let display_str = format!("{}", state);
assert!(display_str.contains("StrapdownState"));
assert!(display_str.contains("45"));
assert!(display_str.contains("lat"));
}
#[test]
#[should_panic(expected = "Latitude must be in the range")]
fn test_strapdown_state_new_invalid_latitude() {
let attitude = Rotation3::identity();
let _state = StrapdownState::new(200.0, 0.0, 0.0, 0.0, 0.0, 0.0, attitude, true, None);
}
#[test]
#[should_panic(expected = "Longitude must be in the range")]
fn test_strapdown_state_new_invalid_longitude() {
let attitude = Rotation3::identity();
let _state = StrapdownState::new(0.0, 200.0, 0.0, 0.0, 0.0, 0.0, attitude, true, None);
}
#[test]
#[should_panic(expected = "Strapdown equations and the local level frame are only valid")]
fn test_strapdown_state_new_invalid_altitude() {
let attitude = Rotation3::identity();
let _state = StrapdownState::new(0.0, 0.0, 50000.0, 0.0, 0.0, 0.0, attitude, true, None);
}
#[test]
fn test_strapdown_state_new_with_degrees() {
let attitude = Rotation3::identity();
let state = StrapdownState::new(45.0, -122.0, 100.0, 0.0, 0.0, 0.0, attitude, true, None);
assert_approx_eq!(state.latitude, 45.0_f64.to_radians(), 1e-6);
assert_approx_eq!(state.longitude, -122.0_f64.to_radians(), 1e-6);
}
#[test]
fn test_strapdown_state_new_with_radians() {
let attitude = Rotation3::identity();
let state = StrapdownState::new(1.0, -2.0, 100.0, 0.0, 0.0, 0.0, attitude, false, None);
assert_eq!(state.latitude, 1.0);
assert_eq!(state.longitude, -2.0);
}
#[test]
fn test_strapdown_state_try_from_slice() {
let data = vec![0.1, 0.2, 100.0, 1.0, 2.0, 3.0, 0.1, 0.2, 0.3];
let slice: &[f64] = &data;
let state = StrapdownState::try_from(slice).unwrap();
assert_eq!(state.latitude, 0.1);
assert_eq!(state.longitude, 0.2);
assert_eq!(state.altitude, 100.0);
}
#[test]
fn test_strapdown_state_try_from_slice_wrong_length() {
let data = vec![0.1, 0.2, 100.0];
let slice: &[f64] = &data;
let result = StrapdownState::try_from(slice);
assert!(result.is_err());
}
#[test]
fn test_strapdown_state_to_dvector() {
let state = StrapdownState::default();
let dvec: DVector<f64> = (&state).into();
assert_eq!(dvec.len(), 9);
}
#[test]
fn test_strapdown_state_to_dvector_owned() {
let state = StrapdownState::default();
let dvec: DVector<f64> = state.into();
assert_eq!(dvec.len(), 9);
}
#[test]
fn test_velocity_update_enu_vs_ned() {
let state_enu = StrapdownState::default(); let state_ned = StrapdownState { is_enu: false, ..Default::default() };
let f = Vector3::new(0.0, 0.0, earth::gravity(&0.0, &0.0));
let dt = 1.0;
let v_enu = velocity_update(&state_enu, f, dt);
let v_ned = velocity_update(&state_ned, f, dt);
assert!(
v_enu[2] != v_ned[2],
"ENU and NED should handle gravity differently"
);
}
#[test]
fn test_generate_scenario_straight_level_flight() {
let vel = 10.0;
let initial_state = StrapdownState {
latitude: 0.0_f64.to_radians(), longitude: 0.0_f64.to_radians(), altitude: 1000.0,
velocity_north: 0.0,
velocity_east: vel, velocity_vertical: 0.0,
attitude: Rotation3::identity(),
is_enu: true,
};
let g = earth::gravity(&0.0, &1000.0); let accel_body = Vector3::new(0.0, 0.0, g); let gyro_body = Vector3::new(0.0, 0.0, 0.0);
let duration_seconds = 3600; let sample_rate_hz = 100;
let (imu_data, gps_measurements, true_states) = generate_scenario_data(
initial_state,
duration_seconds,
sample_rate_hz,
accel_body,
gyro_body,
true, true, false,
);
assert_eq!(imu_data.len(), duration_seconds * sample_rate_hz);
assert_eq!(gps_measurements.len(), duration_seconds * sample_rate_hz);
assert_eq!(true_states.len(), duration_seconds * sample_rate_hz);
let final_state = true_states.last().unwrap();
let final_gps = gps_measurements.last().unwrap();
assert!(
final_gps.latitude.abs() < 90.0,
"GPS latitude should be in degrees"
);
assert!(
final_gps.longitude.abs() < 180.0,
"GPS longitude should be in degrees"
);
assert!(
final_state.latitude.abs() < std::f64::consts::PI,
"State latitude should be in radians"
);
assert!(
final_state.longitude.abs() < std::f64::consts::PI,
"State longitude should be in radians"
);
println!(
"Initial altitude: {:.2} m, Final altitude: {:.2} m (drift: {:.3} m)",
initial_state.altitude,
final_state.altitude,
final_state.altitude - initial_state.altitude
);
println!(
"Initial velocities: N={:.3} m/s, E={:.3} m/s, V={:.3} m/s",
initial_state.velocity_north,
initial_state.velocity_east,
initial_state.velocity_vertical
);
println!(
"Final velocities: N={:.3} m/s, E={:.3} m/s, V={:.3} m/s",
final_state.velocity_north, final_state.velocity_east, final_state.velocity_vertical
);
assert_approx_eq!(final_state.altitude, initial_state.altitude, 1.0);
assert_approx_eq!(final_state.velocity_east, initial_state.velocity_east, 0.01);
assert_approx_eq!(
final_state.velocity_north,
initial_state.velocity_north,
0.01
);
assert_approx_eq!(
final_state.velocity_vertical,
initial_state.velocity_vertical,
0.01
);
let distance_approx = vel * duration_seconds as f64; let lon_change_approx = distance_approx * earth::METERS_TO_DEGREES;
println!(
"Expected lon change: {:.6}°, Actual lon change: {:.6}°",
lon_change_approx,
(final_state.longitude - initial_state.longitude).to_degrees()
);
}
#[test]
fn test_generate_scenario_stationary() {
let initial_state = StrapdownState {
latitude: 40.0_f64.to_radians(), longitude: -105.0_f64.to_radians(), altitude: 1000.0,
velocity_north: 0.0,
velocity_east: 0.0,
velocity_vertical: 0.0,
attitude: Rotation3::identity(),
is_enu: true,
};
let g = earth::gravity(&40.0, &1000.0);
let accel_body = Vector3::new(0.0, 0.0, g); let gyro_body = Vector3::new(0.0, 0.0, 0.0);
let duration_seconds = 3600; let sample_rate_hz = 1;
let (_imu_data, _gps_measurements, true_states) = generate_scenario_data(
initial_state,
duration_seconds,
sample_rate_hz,
accel_body,
gyro_body,
true, true, false,
);
let final_state = true_states.last().unwrap();
let final_gps = _gps_measurements.last().unwrap();
assert!(
final_gps.latitude.abs() < 90.0,
"GPS latitude should be in degrees"
);
assert_approx_eq!(final_gps.latitude, 40.0, 0.01);
assert!(
final_state.latitude.abs() < std::f64::consts::PI,
"State latitude should be in radians"
);
assert_approx_eq!(final_state.latitude, 40.0_f64.to_radians(), 1e-6);
println!(
"Initial altitude: {:.2} m, Final altitude: {:.2} m",
initial_state.altitude, final_state.altitude
);
println!(
"Final velocities: N={:.4}, E={:.4}, V={:.4}",
final_state.velocity_north, final_state.velocity_east, final_state.velocity_vertical
);
assert_approx_eq!(final_state.altitude, initial_state.altitude, 0.1);
assert_approx_eq!(final_state.velocity_north, 0.0, 0.01);
assert_approx_eq!(final_state.velocity_east, 0.0, 0.01);
assert_approx_eq!(final_state.velocity_vertical, 0.0, 0.01);
assert_approx_eq!(final_state.latitude, initial_state.latitude, 1e-6);
assert_approx_eq!(final_state.longitude, initial_state.longitude, 1e-6);
}
}