strapdown/strapdown.rs
1//! Strapdown navigation toolbox for various navigation filters
2//!
3//! This crate provides a set of tools for implementing navigation filters in Rust. The filters are implemented
4//! as structs that can be initialized and updated with new sensor data. The filters are designed to be used in
5//! a strapdown navigation system, where the orientation of the sensor is known and the sensor data can be used
6//! to estimate the position and velocity of the sensor. While utilities exist for IMU data, this crate does
7//! not currently support IMU output directly and should not be thought of as a full inertial navigation system
8//! (INS). This crate is designed to be used to test the filters that would be used in an INS. It does not
9//! provide utilities for reading raw output from the IMU or act as IMU firmware or driver. As such the IMU data
10//! is assumed to be pre-filtered and contain the total accelerations and relative rotations.
11//!
12//! This crate is primarily built off of three additional dependencies:
13//! - [`nav-types`](https://crates.io/crates/nav-types): Provides basic coordinate types and conversions.
14//! - [`nalgebra`](https://crates.io/crates/nalgebra): Provides the linear algebra tools for the filters.
15//! - [`rand`](https://crates.io/crates/rand) and [`rand_distr`](https://crates.io/crates/rand_distr): Provides random number generation for noise and simulation (primarily for particle filter methods).
16//!
17//! All other functionality is built on top of these crates or is auxiliary functionality (e.g. I/O). The primary
18//! reference text is _Principles of GNSS, Inertial, and Multisensor Integrated Navigation Systems, 2nd Edition_
19//! by Paul D. Groves. Where applicable, calculations will be referenced by the appropriate equation number tied
20//! to the book. In general, variables will be named according to the quantity they represent and not the symbol
21//! used in the book. For example, the Earth's equatorial radius is named `EQUATORIAL_RADIUS` instead of `a`.
22//! This style is sometimes relaxed within the body of a given function, but the general rule is to use descriptive
23//! names for variables and not mathematical symbols.
24//!
25//! ## Strapdown mechanization data and equations
26//!
27//! This crate contains the implementation details for the strapdown navigation equations implemented in the Local
28//! Navigation Frame. The equations are based on the book _Principles of GNSS, Inertial, and Multisensor Integrated
29//! Navigation Systems, Second Edition_ by Paul D. Groves. This file corresponds to Chapter 5.4 and 5.5 of the book.
30//! Effort has been made to reproduce most of the equations following the notation from the book. However, variable
31//! and constants should generally been named for the quantity they represent rather than the symbol used in the book.
32//!
33//! ## Coordinate and state definitions
34//! The typical nine-state NED/ENU navigation state vector is used in this implementation. The state vector is defined as:
35//!
36//! $$
37//! x = [p_n, p_e, p_d, v_n, v_e, v_d, \phi, \theta, \psi]
38//! $$
39//!
40//! Where:
41//! - $p_n$, $p_e$, and $p_d$ are the WGS84 geodetic positions (degrees latitude, degrees longitude, meters relative to the ellipsoid).
42//! - $v_n$, $v_e$, and $v_d$ are the local level frame (NED/ENU) velocities (m/s) along the north axis, east axis, and vertical axis.
43//! - $\phi$, $\theta$, and $\psi$ are the Euler angles (radians) representing the orientation of the body frame relative to the local level frame (XYZ Euler rotation).
44//!
45//! The coordinate convention and order is in NED.
46//!
47//! ### Strapdown equations in the Local-Level Frame
48//!
49//! This module implements the strapdown mechanization equations in the Local-Level Frame. These equations form the basis
50//! of the forward propagation step (motion/system/state-transition model) of all the filters implemented in this crate.
51//! The rational for this was to design and test it once, then re-use it on the various filters which really only need to
52//! act on the given probability distribution and are largely ambivalent to the actual function and use generic representations
53//! in their mathematics.
54//!
55//! The equations are based on the book _Principles of GNSS, Inertial, and Multisensor Integrated Navigation Systems, Second Edition_
56//! by Paul D. Groves. Below is a summary of the equations implemented in Chapter 5.4 implemented by this module.
57//!
58//! #### Skew-Symmetric notation
59//!
60//! Groves uses a direction cosine matrix representation of orientation (attitude, rotation). As such, to make the matrix math
61//! work out, rotational quantities need to also be represented using matrices. Groves' convention is to use a lower-case
62//! letter for vector quantities (arrays of shape (N,) Python-style, or (N,1) nalgebra/Matlab style) and capital letters for the
63//! skew-symmetric matrix representation of the same vector.
64//!
65//! $$
66//! x = \begin{bmatrix} a \\\\ b \\\\ c \end{bmatrix} \rightarrow X = \begin{bmatrix} 0 & -c & b \\\\ c & 0 & -a \\\\ -b & a & 0 \end{bmatrix}
67//! $$
68//!
69//! #### Attitude update
70//!
71//! Given a direction-cosine matrix $C_b^n$ representing the orientation (attitude, rotation) of the platform's body frame ($b$)
72//! with respect to the local level frame ($n$), the transport rate $\Omega_{en}^n$ representing the rotation of the local level frame
73//! with respect to the Earth-fixed frame ($e$), the Earth's rotation rate $\Omega_{ie}^e$, and the angular rate $\Omega_{ib}^b$
74//! representing the rotation of the body frame with respect to the inertial frame ($i$), the attitude update equation is given by:
75//!
76//! $$
77//! C_b^n(+) \approx C_b^n(-) \left( I + \Omega_{ib}^b t \right) - \left( \Omega_{ie}^e - \Omega_{en}^n \right) C_b^n(-) t
78//! $$
79//!
80//! where $t$ is the time differential and $C(-)$ is the prior attitude. These attitude matrices are then used to transform the
81//! specific forces from the IMU:
82//!
83//! $$
84//! f_{ib}^n \approx \frac{1}{2} \left( C_b^n(+) + C_b^n(-) \right) f_{ib}^b
85//! $$
86//!
87//! #### Velocity Update
88//!
89//! The velocity update equation is given by:
90//!
91//! $$
92//! v(+) \approx v(-) + \left( f_{ib}^n + g_{b}^n - \left( \Omega_{en}^n - \Omega_{ie}^e \right) v(-) \right) t
93//! $$
94//!
95//! #### Position update
96//!
97//! Finally, we update the base position states in three steps. First we update the altitude:
98//!
99//! $$
100//! p_d(+) = p_d(-) + \frac{1}{2} \left( v_d(-) + v_d(+) \right) t
101//! $$
102//!
103//! Next we update the latitude:
104//!
105//! $$
106//! p_n(+) = p_n(-) + \frac{1}{2} \left( \frac{v_n(-)}{R_n + p_d(-)} + \frac{v_n(+)}{R_n + p_d(+) } \right) t
107//! $$
108//!
109//! Finally, we update the longitude:
110//!
111//! $$
112//! p_e = p_e(-) + \frac{1}{2} \left( \frac{v_e(-)}{R_e + p_d(-) \cos(p_n(-))} + \frac{v_e(+)}{R_e + p_d(+) \cos(p_n(+))} \right) t
113//! $$
114//!
115//! This top-level module provides a public API for each step of the forward mechanization equations, allowing users to
116//! easily pass data in and out.
117pub mod earth;
118pub mod filter;
119pub mod linalg;
120pub mod messages;
121pub mod sim;
122
123use nalgebra::{DVector, Matrix3, Rotation3, Vector3};
124
125use std::convert::{From, Into, TryFrom};
126use std::fmt::{Debug, Display};
127
128/// Basic structure for holding IMU data in the form of acceleration and angular rate vectors.
129///
130/// The vectors are the body frame of the vehicle and represent relative movement. This structure and library is not intended
131/// to be a hardware driver for an IMU, thus the data is assumed to be pre-processed and ready for use in the
132/// mechanization equations (the IMU processing has already filtered out gravitational acceleration).
133#[derive(Clone, Copy, Debug, Default)]
134pub struct IMUData {
135 pub accel: Vector3<f64>, // Acceleration in m/s^2, body frame x, y, z axis
136 pub gyro: Vector3<f64>, // Angular rate in rad/s, body frame x, y, z axis
137}
138impl Display for IMUData {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 write!(
141 f,
142 "IMUData {{ accel: [{:.4}, {:.4}, {:.4}], gyro: [{:.4}, {:.4}, {:.4}] }}",
143 self.accel[0], self.accel[1], self.accel[2], self.gyro[0], self.gyro[1], self.gyro[2]
144 )
145 }
146}
147impl From<Vec<f64>> for IMUData {
148 fn from(vec: Vec<f64>) -> Self {
149 if vec.len() != 6 {
150 panic!(
151 "IMUData must be initialized with a vector of length 6 (3 for accel, 3 for gyro)"
152 );
153 }
154 IMUData {
155 accel: Vector3::new(vec[0], vec[1], vec[2]),
156 gyro: Vector3::new(vec[3], vec[4], vec[5]),
157 }
158 }
159}
160impl Into<Vec<f64>> for IMUData {
161 fn into(self) -> Vec<f64> {
162 vec![
163 self.accel[0],
164 self.accel[1],
165 self.accel[2],
166 self.gyro[0],
167 self.gyro[1],
168 self.gyro[2],
169 ]
170 }
171}
172/// Basic structure for holding the strapdown mechanization state in the form of position, velocity, and attitude.
173///
174/// Attitude is stored in matrix form (rotation or direction cosine matrix) and position and velocity are stored as
175/// vectors. The order or the states depends on the coordinate system used. The struct does not care, but the
176/// coordinate system used will determine which functions you should use. Default is NED but nonetheless must be
177/// assigned. For computational simplicity, latitude and longitude are stored as radians.
178#[derive(Clone, Copy)]
179pub struct StrapdownState {
180 /// Latitude in radians
181 pub latitude: f64,
182 /// Longitude in radians
183 pub longitude: f64,
184 /// Altitude in meters
185 pub altitude: f64,
186 /// Velocity north in m/s (NED frame)
187 pub velocity_north: f64,
188 /// Velocity east in m/s (NED frame)
189 pub velocity_east: f64,
190 /// Velocity down in m/s (NED frame)
191 pub velocity_down: f64,
192 /// Attitude as a rotation matrix (unchanged)
193 pub attitude: Rotation3<f64>,
194 /// Coordinate convention used for the state vector (NED or ENU; NED is true by default)
195 pub coordinate_convention: bool, // true for NED, false for ENU
196}
197
198impl Debug for StrapdownState {
199 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200 let (roll, pitch, yaw) = self.attitude.euler_angles();
201 write!(
202 f,
203 "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] }}",
204 self.latitude.to_degrees(),
205 self.longitude.to_degrees(),
206 self.altitude,
207 self.velocity_north,
208 self.velocity_east,
209 self.velocity_down,
210 roll.to_degrees(),
211 pitch.to_degrees(),
212 yaw.to_degrees()
213 )
214 }
215}
216impl Default for StrapdownState {
217 fn default() -> Self {
218 StrapdownState {
219 latitude: 0.0,
220 longitude: 0.0,
221 altitude: 0.0,
222 velocity_north: 0.0,
223 velocity_east: 0.0,
224 velocity_down: 0.0,
225 attitude: Rotation3::identity(),
226 coordinate_convention: true, // NED by default
227 }
228 }
229}
230impl StrapdownState {
231 /// Create a new StrapdownState from explicit position and velocity components, and attitude
232 ///
233 /// # Arguments
234 /// * `latitude` - Latitude in radians or degrees (see `in_degrees`).
235 /// * `longitude` - Longitude in radians or degrees (see `in_degrees`).
236 /// * `altitude` - Altitude in meters.
237 /// * `velocity_north` - North velocity in m/s.
238 /// * `velocity_east` - East velocity in m/s.
239 /// * `velocity_down` - Down velocity in m/s.
240 /// * `attitude` - Rotation3<f64> attitude matrix.
241 /// * `in_degrees` - If true, angles are provided in degrees and will be converted to radians.
242 /// * `ned` - If true, the coordinate convention is NED (North, East, Down), otherwise ENU (East, North, Up).
243 pub fn new(
244 latitude: f64,
245 longitude: f64,
246 altitude: f64,
247 velocity_north: f64,
248 velocity_east: f64,
249 velocity_down: f64,
250 attitude: Rotation3<f64>,
251 in_degrees: bool,
252 ned: bool,
253 ) -> StrapdownState {
254 let latitude = if in_degrees {
255 latitude.to_radians()
256 } else {
257 latitude
258 };
259 let longitude = if in_degrees {
260 longitude.to_radians()
261 } else {
262 longitude
263 };
264 assert!(
265 latitude >= -std::f64::consts::PI && latitude <= std::f64::consts::PI,
266 "Latitude must be in the range [-π, π]"
267 );
268 assert!(
269 longitude >= -std::f64::consts::PI && longitude <= std::f64::consts::PI,
270 "Longitude must be in the range [-π, π]"
271 );
272 assert!(
273 altitude >= -10_000.0,
274 "Altitude must be greater than -10,000 meters (to avoid unrealistic values)"
275 );
276
277 StrapdownState {
278 latitude,
279 longitude,
280 altitude,
281 velocity_north,
282 velocity_east,
283 velocity_down,
284 attitude,
285 coordinate_convention: ned,
286 }
287 }
288 // --- From/Into trait implementations for StrapdownState <-> Vec<f64> and &[f64] ---
289}
290impl From<StrapdownState> for Vec<f64> {
291 /// Converts a StrapdownState to a Vec<f64> in NED order, angles in radians.
292 fn from(state: StrapdownState) -> Self {
293 let (roll, pitch, yaw) = state.attitude.euler_angles();
294 vec![
295 state.latitude,
296 state.longitude,
297 state.altitude,
298 state.velocity_north,
299 state.velocity_east,
300 state.velocity_down,
301 roll,
302 pitch,
303 yaw,
304 ]
305 }
306}
307impl From<&StrapdownState> for Vec<f64> {
308 /// Converts a reference to StrapdownState to a Vec<f64> in NED order, angles in radians.
309 fn from(state: &StrapdownState) -> Self {
310 let (roll, pitch, yaw) = state.attitude.euler_angles();
311 vec![
312 state.latitude,
313 state.longitude,
314 state.altitude,
315 state.velocity_north,
316 state.velocity_east,
317 state.velocity_down,
318 roll,
319 pitch,
320 yaw,
321 ]
322 }
323}
324impl TryFrom<&[f64]> for StrapdownState {
325 type Error = &'static str;
326 /// Attempts to create a StrapdownState from a slice of 9 elements (NED order, radians).
327 fn try_from(slice: &[f64]) -> Result<Self, Self::Error> {
328 if slice.len() != 9 {
329 return Err("Slice must have length 9 for StrapdownState");
330 }
331 let attitude = Rotation3::from_euler_angles(slice[6], slice[7], slice[8]);
332 Ok(StrapdownState::new(
333 slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], attitude,
334 false, // angles are in radians
335 true, // NED convention
336 ))
337 }
338}
339impl TryFrom<Vec<f64>> for StrapdownState {
340 type Error = &'static str;
341 /// Attempts to create a StrapdownState from a Vec<f64> of length 9 (NED order, radians).
342 fn try_from(vec: Vec<f64>) -> Result<Self, Self::Error> {
343 Self::try_from(vec.as_slice())
344 }
345}
346impl From<StrapdownState> for DVector<f64> {
347 /// Converts a StrapdownState to a DVector<f64> in NED order, angles in radians.
348 fn from(state: StrapdownState) -> Self {
349 DVector::from_vec(state.into())
350 }
351}
352impl From<&StrapdownState> for DVector<f64> {
353 /// Converts a reference to StrapdownState to a DVector<f64> in NED order, angles in radians.
354 fn from(state: &StrapdownState) -> Self {
355 DVector::from_vec(state.into())
356 }
357}
358
359/// NED form of the forward kinematics equations. Corresponds to section 5.4 Local-Navigation Frame Equations
360/// from the book _Principles of GNSS, Inertial, and Multisensor Integrated Navigation Systems, Second Edition_
361/// by Paul D. Groves; Second Edition.
362///
363/// This function implements the forward kinematics equations for the strapdown navigation system. It takes
364/// the IMU data and the time step as inputs and updates the position, velocity, and attitude of the system.
365/// The IMU data is assumed to be pre-processed and ready for use in the mechanization equations (i.e. the
366/// gravity vector has already been filtered out and the data represents relative motion).
367///
368/// # Arguments
369/// * `imu_data` - A reference to an IMUData instance containing the acceleration and gyro data in the body frame.
370/// * `dt` - A f64 representing the time step in seconds.
371///
372/// # Example
373/// ```rust
374/// use strapdown::{StrapdownState, IMUData, forward};
375/// use nalgebra::Vector3;
376/// let mut state = StrapdownState::default();
377/// let imu_data = IMUData {
378/// accel: Vector3::new(0.0, 0.0, -9.81), // free fall acceleration in m/s^2
379/// gyro: Vector3::new(0.0, 0.0, 0.0) // No rotation
380/// };
381/// let dt = 0.1; // Example time step in seconds
382/// forward(&mut state, imu_data, dt);
383/// ```
384pub fn forward(state: &mut StrapdownState, imu_data: IMUData, dt: f64) {
385 // Extract the attitude matrix from the current state
386 let c_0: Rotation3<f64> = state.attitude;
387 // Attitude update; Equation 5.46
388 let c_1: Matrix3<f64> = attitude_update(state, imu_data.gyro, dt);
389 // Specific force transformation; Equation 5.47
390 let f: Vector3<f64> = 0.5 * (c_0.matrix() + c_1) * imu_data.accel;
391 // Velocity update; Equation 5.54
392 let velocity = velocity_update(state, f, dt);
393 // Position update; Equation 5.56
394 let (lat_1, lon_1, alt_1) = position_update(state, velocity, dt);
395 // Save updated attitude as rotation
396 state.attitude = Rotation3::from_matrix(&c_1);
397 // Save update velocity
398 state.velocity_north = velocity[0];
399 state.velocity_east = velocity[1];
400 state.velocity_down = velocity[2];
401 // Save updated position
402 state.latitude = lat_1;
403 state.longitude = lon_1;
404 state.altitude = alt_1;
405}
406/// NED Attitude update equation
407///
408/// This function implements the attitude update equation for the strapdown navigation system. It takes the gyroscope
409/// data and the time step as inputs and returns the updated attitude matrix. The attitude update equation is based
410/// on the book _Principles of GNSS, Inertial, and Multisensor Integrated Navigation Systems, Second Edition_ by Paul D. Groves.
411///
412/// # Arguments
413/// * `gyros` - A Vector3 representing the gyroscope data in rad/s in the body frame x, y, z axis.
414/// * `dt` - A f64 representing the time step in seconds.
415///
416/// # Returns
417/// * A Matrix3 representing the updated attitude matrix in the NED frame.
418fn attitude_update(state: &StrapdownState, gyros: Vector3<f64>, dt: f64) -> Matrix3<f64> {
419 let transport_rate: Matrix3<f64> = earth::vector_to_skew_symmetric(&earth::transport_rate(
420 &state.latitude.to_degrees(),
421 &state.altitude,
422 &Vector3::from_vec(vec![
423 state.velocity_north,
424 state.velocity_east,
425 state.velocity_down,
426 ]),
427 ));
428 let rotation_rate: Matrix3<f64> =
429 earth::vector_to_skew_symmetric(&earth::earth_rate_lla(&state.latitude.to_degrees()));
430 let omega_ib: Matrix3<f64> = earth::vector_to_skew_symmetric(&gyros);
431 let c_1: Matrix3<f64> = state.attitude * (Matrix3::identity() + omega_ib * dt)
432 - (rotation_rate + transport_rate) * state.attitude * dt;
433 c_1
434}
435/// Velocity update in NED
436///
437/// This function implements the velocity update equation for the strapdown navigation system. It takes the specific force
438/// vector and the time step as inputs and returns the updated velocity vector. The velocity update equation is based
439/// on the book _Principles of GNSS, Inertial, and Multisensor Integrated Navigation Systems, Second Edition_ by Paul D. Groves.
440///
441/// # Arguments
442/// * `f` - A Vector3 representing the specific force vector in m/s^2 in the NED frame.
443/// * `dt` - A f64 representing the time step in seconds.
444///
445/// # Returns
446/// * A Vector3 representing the updated velocity vector in the NED frame.
447fn velocity_update(state: &StrapdownState, specific_force: Vector3<f64>, dt: f64) -> Vector3<f64> {
448 let transport_rate: Matrix3<f64> = earth::vector_to_skew_symmetric(&earth::transport_rate(
449 &state.latitude.to_degrees(),
450 &state.altitude,
451 &Vector3::from_vec(vec![
452 state.velocity_north,
453 state.velocity_east,
454 state.velocity_down,
455 ]),
456 ));
457 let rotation_rate: Matrix3<f64> =
458 earth::vector_to_skew_symmetric(&earth::earth_rate_lla(&state.latitude.to_degrees()));
459 let r = earth::ecef_to_lla(&state.latitude.to_degrees(), &state.longitude.to_degrees());
460 let velocity: Vector3<f64> = Vector3::new(
461 state.velocity_north,
462 state.velocity_east,
463 state.velocity_down,
464 );
465 let gravity = Vector3::new(
466 0.0,
467 0.0,
468 earth::gravity(&state.latitude.to_degrees(), &state.altitude),
469 );
470 velocity
471 + (specific_force - gravity - r * (transport_rate + 2.0 * rotation_rate) * velocity) * dt
472}
473/// Position update in NED
474///
475/// This function implements the position update equation for the strapdown navigation system. It takes the current state,
476/// the velocity vector, and the time step as inputs and returns the updated position (latitude, longitude, altitude).
477///
478/// # Arguments
479/// * `state` - A reference to the current StrapdownState containing the position and velocity.
480/// * `velocity` - A Vector3 representing the velocity vector in m/s in the NED frame.
481/// * `dt` - A f64 representing the time step in seconds.
482///
483/// # Returns
484/// * A tuple (latitude, longitude, altitude) representing the updated position in radians and meters.
485pub fn position_update(state: &StrapdownState, velocity: Vector3<f64>, dt: f64) -> (f64, f64, f64) {
486 let (r_n, r_e_0, _) = earth::principal_radii(&state.latitude, &state.altitude);
487 let lat_0 = state.latitude;
488 let alt_0 = state.altitude;
489 // Altitude update
490 let alt_1 = alt_0 + 0.5 * (state.velocity_down + velocity[2]) * dt;
491 // Latitude update
492 let lat_1: f64 = state.latitude
493 + 0.5 * (state.velocity_north / (r_n + alt_0) + velocity[0] / (r_n + state.altitude)) * dt;
494 // Longitude update
495 let (_, r_e_1, _) = earth::principal_radii(&lat_1, &state.altitude);
496 let cos_lat0 = lat_0.cos().max(1e-6); // Guard against cos(lat) --> 0 near poles
497 let cos_lat1 = lat_1.cos().max(1e-6);
498 let lon_1: f64 = state.longitude
499 + 0.5
500 * (state.velocity_east / ((r_e_0 + alt_0) * cos_lat0)
501 + velocity[1] / ((r_e_1 + state.altitude) * cos_lat1))
502 * dt;
503 // Save updated position
504 (
505 wrap_latitude(lat_1.to_degrees()).to_radians(),
506 wrap_to_pi(lon_1),
507 alt_1,
508 )
509}
510// --- Miscellaneous functions for wrapping angles ---
511/// Wrap an angle to the range -180 to 180 degrees
512///
513/// This function is generic and can be used with any type that implements the necessary traits.
514///
515/// # Arguments
516/// * `angle` - The angle to be wrapped, which can be of any type that implements the necessary traits.
517/// # Returns
518/// * The wrapped angle, which will be in the range -180 to 180 degrees.
519/// # Example
520/// ```rust
521/// use strapdown::wrap_to_180;
522/// let angle = 190.0;
523/// let wrapped_angle = wrap_to_180(angle);
524/// assert_eq!(wrapped_angle, -170.0); // 190 degrees wrapped to -170 degrees
525/// ```
526pub fn wrap_to_180<T>(angle: T) -> T
527where
528 T: PartialOrd + Copy + std::ops::SubAssign + std::ops::AddAssign + From<f64>,
529{
530 let mut wrapped: T = angle;
531 while wrapped > T::from(180.0) {
532 wrapped -= T::from(360.0);
533 }
534 while wrapped < T::from(-180.0) {
535 wrapped += T::from(360.0);
536 }
537 wrapped
538}
539/// Wrap an angle to the range 0 to 360 degrees
540///
541/// This function is generic and can be used with any type that implements the necessary traits.
542///
543/// # Arguments
544/// * `angle` - The angle to be wrapped, which can be of any type that implements the necessary traits.
545/// # Returns
546/// * The wrapped angle, which will be in the range 0 to 360 degrees.
547/// # Example
548/// ```rust
549/// use strapdown::wrap_to_360;
550/// let angle = 370.0;
551/// let wrapped_angle = wrap_to_360(angle);
552/// assert_eq!(wrapped_angle, 10.0); // 370 degrees wrapped to 10 degrees
553/// ```
554pub fn wrap_to_360<T>(angle: T) -> T
555where
556 T: PartialOrd + Copy + std::ops::SubAssign + std::ops::AddAssign + From<f64>,
557{
558 let mut wrapped: T = angle;
559 while wrapped > T::from(360.0) {
560 wrapped -= T::from(360.0);
561 }
562 while wrapped < T::from(0.0) {
563 wrapped += T::from(360.0);
564 }
565 wrapped
566}
567/// Wrap an angle to the range 0 to $\pm\pi$ radians
568///
569/// This function is generic and can be used with any type that implements the necessary traits.
570///
571/// # Arguments
572/// * `angle` - The angle to be wrapped, which can be of any type that implements the necessary traits.
573/// # Returns
574/// * The wrapped angle, which will be in the range -π to π radians.
575/// # Example
576/// ```rust
577/// use strapdown::wrap_to_pi;
578/// use std::f64::consts::PI;
579/// let angle = 3.0 * PI / 2.0; // radians
580/// let wrapped_angle = wrap_to_pi(angle);
581/// assert_eq!(wrapped_angle, -PI / 2.0); // 3π/4 radians wrapped to -π/4 radians
582/// ```
583pub fn wrap_to_pi<T>(angle: T) -> T
584where
585 T: PartialOrd + Copy + std::ops::SubAssign + std::ops::AddAssign + From<f64>,
586{
587 let mut wrapped: T = angle;
588 while wrapped > T::from(std::f64::consts::PI) {
589 wrapped -= T::from(2.0 * std::f64::consts::PI);
590 }
591 while wrapped < T::from(-std::f64::consts::PI) {
592 wrapped += T::from(2.0 * std::f64::consts::PI);
593 }
594 wrapped
595}
596/// Wrap an angle to the range 0 to $2 \pi$ radians
597///
598/// This function is generic and can be used with any type that implements the necessary traits.
599///
600/// # Arguments
601/// * `angle` - The angle to be wrapped, which can be of any type that implements the necessary traits.
602/// # Returns
603/// * The wrapped angle, which will be in the range -π to π radians.
604/// # Example
605/// ```rust
606/// use strapdown::wrap_to_2pi;
607/// use std::f64::consts::PI;
608/// let angle = 5.0 * PI; // radians
609/// let wrapped_angle = wrap_to_2pi(angle);
610/// assert_eq!(wrapped_angle, PI); // 5π radians wrapped to π radians
611/// ```
612pub fn wrap_to_2pi<T>(angle: T) -> T
613where
614 T: PartialOrd + Copy + std::ops::SubAssign + std::ops::AddAssign + From<f64>,
615 T: PartialOrd + Copy + std::ops::SubAssign + std::ops::AddAssign + From<i32>,
616{
617 let mut wrapped: T = angle;
618 while wrapped > T::from(2.0 * std::f64::consts::PI) {
619 wrapped -= T::from(2.0 * std::f64::consts::PI);
620 }
621 while wrapped < T::from(0.0) {
622 wrapped += T::from(2.0 * std::f64::consts::PI);
623 }
624 wrapped
625}
626/// Wrap latitude to the range -90 to 90 degrees
627///
628/// This function is generic and can be used with any type that implements the necessary traits.
629/// This function is useful for ensuring that latitude values remain within the valid range for
630/// WGS84 coordinates. Keep in mind that the local level frame (NED/ENU) is typically used for
631/// navigation and positioning in middling latitudes.
632///
633/// # Arguments
634/// * `latitude` - The latitude to be wrapped, which can be of any type that implements the necessary traits.
635/// # Returns
636/// * The wrapped latitude, which will be in the range -90 to 90 degrees.
637/// # Example
638/// ```rust
639/// use strapdown::wrap_latitude;
640/// let latitude = 95.0; // degrees
641/// let wrapped_latitude = wrap_latitude(latitude);
642/// assert_eq!(wrapped_latitude, -85.0); // 95 degrees wrapped to -85 degrees
643/// ```
644pub fn wrap_latitude<T>(latitude: T) -> T
645where
646 T: PartialOrd + Copy + std::ops::SubAssign + std::ops::AddAssign + From<f64>,
647{
648 let mut wrapped: T = latitude;
649 while wrapped > T::from(90.0) {
650 wrapped -= T::from(180.0);
651 }
652 while wrapped < T::from(-90.0) {
653 wrapped += T::from(180.0);
654 }
655 wrapped
656}
657// Note: nalgebra does not yet have a well developed testing framework for directly comparing
658// nalgebra data structures. Rather than directly comparing, check the individual items.
659#[cfg(test)]
660mod tests {
661 use super::*;
662 use assert_approx_eq::assert_approx_eq;
663
664 #[test]
665 fn test_strapdown_state_new() {
666 let state = StrapdownState::default();
667 assert_eq!(state.latitude, 0.0);
668 assert_eq!(state.longitude, 0.0);
669 assert_eq!(state.altitude, 0.0);
670 assert_eq!(state.velocity_north, 0.0);
671 assert_eq!(state.velocity_east, 0.0);
672 assert_eq!(state.velocity_down, 0.0);
673 assert_eq!(state.attitude, Rotation3::identity());
674 }
675 #[test]
676 fn test_to_vector_zeros() {
677 let state = StrapdownState::default();
678 let state_vector: Vec<f64> = state.into();
679 let zeros = vec![0.0; 9];
680 assert_eq!(state_vector, zeros);
681 }
682 #[test]
683 fn test_new_from_vector() {
684 let roll: f64 = 15.0;
685 let pitch: f64 = 45.0;
686 let yaw: f64 = 90.0;
687 let state_vector = vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0, roll, pitch, yaw];
688 let state = StrapdownState::try_from(state_vector).unwrap();
689 assert_eq!(state.latitude, 0.0);
690 assert_eq!(state.longitude, 0.0);
691 assert_eq!(state.altitude, 0.0);
692 assert_eq!(state.velocity_north, 0.0);
693 }
694 #[test]
695 fn test_dcm_to_vector() {
696 let state = StrapdownState::default();
697 let state_vector: Vec<f64> = (&state).into();
698 assert_eq!(state_vector.len(), 9);
699 assert_eq!(state_vector, vec![0.0; 9]);
700 }
701 #[test]
702 fn test_attitude_matrix_euler_consistency() {
703 let state = StrapdownState::default();
704 let (roll, pitch, yaw) = state.attitude.euler_angles();
705 let state_vector: Vec<f64> = state.into();
706 assert_eq!(state_vector[6], roll);
707 assert_eq!(state_vector[7], pitch);
708 assert_eq!(state_vector[8], yaw);
709 }
710
711 #[test]
712 // Test the forward mechanization (basic structure, not full dynamics)
713 fn test_forward_freefall_stub() {
714 let attitude = Rotation3::identity();
715 let state = StrapdownState::new(
716 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, attitude, false, true, // NED convention
717 );
718 // This is a stub: actual forward propagation logic should be tested in integration with the mechanization equations.
719 assert_eq!(state.latitude, 0.0);
720 assert_eq!(state.longitude, 0.0);
721 assert_eq!(state.altitude, 0.0);
722 assert_eq!(state.velocity_north, 0.0);
723 assert_eq!(state.velocity_east, 0.0);
724 assert_eq!(state.velocity_down, 0.0);
725 assert_eq!(state.attitude, Rotation3::identity());
726 }
727 #[test]
728 fn rest() {
729 // Test the forward mechanization with a state at rest
730 let attitude = Rotation3::identity();
731 let mut state = StrapdownState::new(
732 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, attitude, false, true, // NED convention
733 );
734 assert_eq!(state.velocity_north, 0.0);
735 assert_eq!(state.velocity_east, 0.0);
736 assert_eq!(state.velocity_down, 0.0);
737 let imu_data = IMUData {
738 accel: Vector3::new(0.0, 0.0, earth::gravity(&0.0, &0.0)),
739 gyro: Vector3::new(0.0, 0.0, 0.0), // No rotation
740 };
741 let dt = 1.0; // Example time step in seconds
742 forward(&mut state, imu_data, dt);
743 // After a forward step, the state should still be at rest
744 assert_approx_eq!(state.latitude, 0.0, 1e-6);
745 assert_approx_eq!(state.longitude, 0.0, 1e-6);
746 assert_approx_eq!(state.altitude, 0.0, 0.1);
747 assert_approx_eq!(state.velocity_north, 0.0, 1e-3);
748 assert_approx_eq!(state.velocity_east, 0.0, 1e-3);
749 assert_approx_eq!(state.velocity_down, 0.0, 0.1);
750 //assert_approx_eq!(state.attitude, Rotation3::identity(), 1e-3);
751 let attitude = state.attitude.matrix() - Rotation3::identity().matrix();
752 assert_approx_eq!(attitude[(0, 0)], 0.0, 1e-3);
753 assert_approx_eq!(attitude[(0, 1)], 0.0, 1e-3);
754 assert_approx_eq!(attitude[(0, 2)], 0.0, 1e-3);
755 assert_approx_eq!(attitude[(1, 0)], 0.0, 1e-3);
756 assert_approx_eq!(attitude[(1, 1)], 0.0, 1e-3);
757 assert_approx_eq!(attitude[(1, 2)], 0.0, 1e-3);
758 assert_approx_eq!(attitude[(2, 0)], 0.0, 1e-3);
759 assert_approx_eq!(attitude[(2, 1)], 0.0, 1e-3);
760 assert_approx_eq!(attitude[(2, 2)], 0.0, 1e-3);
761 }
762 #[test]
763 fn yawing() {
764 // Testing the forward mechanization with a state that is yawing
765 let attitude = Rotation3::from_euler_angles(0.0, 0.0, 0.1); // 0.1 rad yaw
766 let state = StrapdownState::new(
767 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, attitude, false, // angles provided in radians
768 true, // NED convention
769 );
770 assert_approx_eq!(state.attitude.euler_angles().2, 0.1, 1e-6); // Check initial yaw
771 let gyros = Vector3::new(0.0, 0.0, 0.1); // Gyro data for yawing
772 let dt = 1.0; // Example time step in seconds
773 let new_attitude = Rotation3::from_matrix(&attitude_update(&state, gyros, dt));
774 // Check if the yaw has changed
775 let new_yaw = new_attitude.euler_angles().2;
776 assert_approx_eq!(new_yaw, 0.1 + 0.1, 1e-3); // 0.1 rad initial + 0.1 rad
777 }
778 #[test]
779 fn rolling() {
780 // Testing the forward mechanization with a state that is yawing
781 let attitude = Rotation3::from_euler_angles(0.1, 0.0, 0.0); // 0.1 rad yaw
782 let state = StrapdownState::new(
783 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, attitude, false, // angles provided in radians
784 true, // NED convention
785 );
786 assert_approx_eq!(state.attitude.euler_angles().0, 0.1, 1e-6); // Check initial roll
787 let gyros = Vector3::new(0.10, 0.0, 0.0); // Gyro data for yawing
788 let dt = 1.0; // Example time step in seconds
789 let new_attitude = Rotation3::from_matrix(&attitude_update(&state, gyros, dt));
790 // Check if the yaw has changed
791 let new_roll = new_attitude.euler_angles().0;
792 assert_approx_eq!(new_roll, 0.1 + 0.1, 1e-3); // 0.1 rad initial + 0.1 rad
793 }
794 #[test]
795 fn pitching() {
796 // Testing the forward mechanization with a state that is yawing
797 let attitude = Rotation3::from_euler_angles(0.0, 0.1, 0.0); // 0.1 rad yaw
798 let state = StrapdownState::new(
799 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, attitude, false, // angles provided in radians
800 true, // NED convention
801 );
802 assert_approx_eq!(state.attitude.euler_angles().1, 0.1, 1e-6); // Check initial yaw
803 let gyros = Vector3::new(0.0, 0.1, 0.0); // Gyro data for yawing
804 let dt = 1.0; // Example time step in seconds
805 let new_attitude = Rotation3::from_matrix(&attitude_update(&state, gyros, dt));
806 // Check if the yaw has changed
807 let new_pitch = new_attitude.euler_angles().1;
808 assert_approx_eq!(new_pitch, 0.1 + 0.1, 1e-3); // 0.1 rad initial + 0.1 rad
809 }
810 #[test]
811 fn test_wrap_to_180() {
812 assert_eq!(super::wrap_to_180(190.0), -170.0);
813 assert_eq!(super::wrap_to_180(-190.0), 170.0);
814 assert_eq!(super::wrap_to_180(0.0), 0.0);
815 assert_eq!(super::wrap_to_180(180.0), 180.0);
816 assert_eq!(super::wrap_to_180(-180.0), -180.0);
817 }
818 #[test]
819 fn test_wrap_to_360() {
820 assert_eq!(super::wrap_to_360(370.0), 10.0);
821 assert_eq!(super::wrap_to_360(-10.0), 350.0);
822 assert_eq!(super::wrap_to_360(0.0), 0.0);
823 }
824 #[test]
825 fn test_wrap_to_pi() {
826 assert_eq!(
827 super::wrap_to_pi(3.0 * std::f64::consts::PI),
828 std::f64::consts::PI
829 );
830 assert_eq!(
831 super::wrap_to_pi(-3.0 * std::f64::consts::PI),
832 -std::f64::consts::PI
833 );
834 assert_eq!(super::wrap_to_pi(0.0), 0.0);
835 assert_eq!(
836 super::wrap_to_pi(std::f64::consts::PI),
837 std::f64::consts::PI
838 );
839 assert_eq!(
840 super::wrap_to_pi(-std::f64::consts::PI),
841 -std::f64::consts::PI
842 );
843 }
844 #[test]
845 fn test_wrap_to_2pi() {
846 assert_eq!(
847 super::wrap_to_2pi(7.0 * std::f64::consts::PI),
848 std::f64::consts::PI
849 );
850 assert_eq!(
851 super::wrap_to_2pi(-5.0 * std::f64::consts::PI),
852 std::f64::consts::PI
853 );
854 assert_eq!(super::wrap_to_2pi(0.0), 0.0);
855 assert_eq!(
856 super::wrap_to_2pi(std::f64::consts::PI),
857 std::f64::consts::PI
858 );
859 assert_eq!(
860 super::wrap_to_2pi(-std::f64::consts::PI),
861 std::f64::consts::PI
862 );
863 }
864 #[test]
865 fn test_velocity_update_zero_force() {
866 // Zero specific force, velocity should remain unchanged
867 let state = StrapdownState::default();
868 let f = nalgebra::Vector3::new(
869 0.0,
870 0.0,
871 earth::gravity(&0.0, &0.0), // Gravity vector in NED
872 );
873 let dt = 1.0;
874 let v_new = velocity_update(&state, f, dt);
875 assert_eq!(v_new[0], 0.0);
876 assert_eq!(v_new[1], 0.0);
877 assert_eq!(v_new[2], 0.0);
878 }
879 #[test]
880 fn test_velocity_update_constant_force() {
881 // Constant specific force in north direction, expect velocity to increase linearly
882 let state = StrapdownState::default();
883 let f = nalgebra::Vector3::new(1.0, 0.0, earth::gravity(&0.0, &0.0)); // 1 m/s^2 north
884 let dt = 2.0;
885 let v_new = velocity_update(&state, f, dt);
886 // Should be v = a * dt
887 assert!((v_new[0] - 2.0).abs() < 1e-6);
888 assert!((v_new[1]).abs() < 1e-6);
889 assert!((v_new[2]).abs() < 1e-6);
890 }
891 #[test]
892 fn test_velocity_update_initial_velocity() {
893 // Initial velocity, zero force, should remain unchanged
894 let mut state = StrapdownState::default();
895 state.velocity_north = 5.0;
896 state.velocity_east = -3.0;
897 state.velocity_down = 2.0;
898 let f = Vector3::from_vec(vec![0.0, 0.0, earth::gravity(&0.0, &0.0)]);
899 let dt = 1.0;
900 let v_new = velocity_update(&state, f, dt);
901 assert_approx_eq!(v_new[0], 5.0, 1e-3);
902 assert_approx_eq!(v_new[1], -3.0, 1e-3);
903 assert_approx_eq!(v_new[2], 2.0, 1e-3);
904 }
905 #[test]
906 fn vertical_acceleration() {
907 // Test vertical acceleration
908 let mut state = StrapdownState::default();
909 state.velocity_north = 0.0;
910 state.velocity_east = 0.0;
911 state.velocity_down = 0.0;
912 let f = Vector3::from_vec(vec![0.0, 0.0, 2.0 * earth::gravity(&0.0, &0.0)]); // Downward acceleration
913 let dt = 1.0;
914 let v_new = velocity_update(&state, f, dt);
915 assert_approx_eq!(v_new[2], earth::gravity(&0.0, &0.0), 1e-3);
916 }
917 #[test]
918 fn test_forward_yawing() {
919 // Yaw rate only, expect yaw to increase by gyro_z * dt
920 let attitude = nalgebra::Rotation3::identity();
921 let mut state = StrapdownState::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, attitude, false, true);
922 let imu_data = IMUData {
923 accel: Vector3::new(0.0, 0.0, 0.0),
924 gyro: Vector3::new(0.0, 0.0, 0.1), // Gyro data for yawing
925 };
926 let dt = 1.0;
927 forward(&mut state, imu_data, dt);
928 let (_, _, yaw) = state.attitude.euler_angles();
929 assert!((yaw - 0.1).abs() < 1e-3);
930 }
931
932 #[test]
933 fn test_forward_rolling() {
934 // Roll rate only, expect roll to increase by gyro_x * dt
935 let attitude = nalgebra::Rotation3::identity();
936 let mut state = StrapdownState::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, attitude, false, true);
937 let imu_data = IMUData {
938 accel: Vector3::new(0.0, 0.0, 0.0),
939 gyro: Vector3::new(0.1, 0.0, 0.0), // Gyro data for rolling
940 };
941 let dt = 1.0;
942 forward(&mut state, imu_data, dt);
943
944 //let (roll, _, _) = state.attitude.euler_angles();
945 let roll = state.attitude.euler_angles().0;
946 assert_approx_eq!(roll, 0.1, 1e-3);
947 }
948
949 #[test]
950 fn test_forward_pitching() {
951 // Pitch rate only, expect pitch to increase by gyro_y * dt
952 let attitude = nalgebra::Rotation3::identity();
953 let mut state = StrapdownState::new(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, attitude, false, true);
954 let imu_data = IMUData {
955 accel: Vector3::new(0.0, 0.0, 0.0),
956 gyro: Vector3::new(0.0, 0.1, 0.0), // Gyro data for pitching
957 };
958 let dt = 1.0;
959 forward(&mut state, imu_data, dt);
960 let (_, pitch, _) = state.attitude.euler_angles();
961 assert_approx_eq!(pitch, 0.1, 1e-3); // 0.1 rad initial + 0.1 rad
962 }
963}