strapdown_core/
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, but the raw IMU 
9//! output firmware. As such the IMU data is assumed to be _relative_ accelerations and rotations. Additional
10//! signals that can be derived using IMU data, such as gravity or magnetic vector and anomalies, should come
11//! from a separate IMU channel. In other words, to calculate the gravity vector the IMU output should be 
12//! parsed to seperately output the overall acceleration and rotation of the sensor whereas the navigation 
13//! filter will use the gravity and orientation corrected acceleration and rotation to estimate the position
14//! 
15//! Primarily built off of three crate dependencies:
16//! - nav-types: Provides basic coordinate types and conversions.
17//! - nalgebra: Provides the linear algebra tools for the filters.
18//! - haversine-rs: Provides the haversine formula for calculating distances between two points on the Earth's surface, which is the primary error metric.
19//! All other functionality is built on top of these crates. The primary reference text is _Prinicples of GNSS, 
20//! Inertial, and Multisensor Integrated Navigation Systems, 2nd Edition_ by Paul D. Groves. Where applicable, 
21//! calculations will be referenced by the appropriate equation number tied to the book. In general, variables 
22//! will be named according to the quantity they represent and not the symbol used in the book. For example, 
23//! the Earth's equitorial radius is named `EQUITORIAL_RADIUS` instead of `a`. This style is sometimes relaxed 
24//! within the body of a given function, but the general rule is to use descriptive names for variables and not 
25//! mathematical symbols.
26//////-------
27//! Strapdown mechanization data and equations
28//!
29//! This crate contains the implementation details for the strapdown navigation equations implemented in the Local
30//! Navigation Frame. The equations are based on the book "Principles of GNSS, Inertial, and Multisensor Integrated
31//! Navigation Systems, Second Edition" by Paul D. Groves. This file corresponds to Chapter 5.4 and 5.5 of the book.
32//! Effort has been made to reproduce most of the equations following the notation from the book. However, variable
33//! and constants should generally been named for the quatity they represent rather than the symbol used in the book.
34//!
35//! # Coordinate and state definitions
36//! The typical nine-state NED/ENU navigation state vector is used in this implementation. The state vector is defined as:
37//!
38//! ```text
39//! x = [pn, pe, pd, v_n, v_e, v_d, phi, theta, psi]
40//! ```
41//!
42//! Where:
43//! - `pn`, `pe`, and `pd` are the WGS84 geodetic positions (degrees latitude, degrees longitude, meters relative to the ellipsoid).
44//! - `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.
45//! - `phi`, `theta`, and `psi` are the Euler angles (radians) representing the orientation of the body frame relative to the local level frame.
46//!
47//! The coordinate convention and order is in NED. ENU implementations are to be added in the future.
48//! 
49//! 
50//! 
51
52pub mod earth;
53pub mod filter;
54
55use std::fmt::Debug;
56use angle::Deg; // might be overkill to need this entire create just for this
57use nalgebra::{Matrix3, Rotation3, SVector, Vector3};
58
59//use crate::earth;
60
61/// Basic structure for holding IMU data in the form of acceleration and angular rate vectors. The vectors are
62/// in the body frame of the vehicle and represent relative movement. This structure and library is not intended
63/// to be a hardware driver for an IMU, thus the data is assumed to be pre-processed and ready for use in the
64/// mechanization equations (the IMU processing has already filtered out gravitational acceleration).
65#[derive(Clone, Copy, Debug)]
66pub struct IMUData {
67    pub accel: Vector3<f64>, // Acceleration in m/s^2, body frame x, y, z axis
68    pub gyro: Vector3<f64>,  // Angular rate in rad/s, body frame x, y, z axis
69}
70impl IMUData {
71    // Create a new IMUData with all zeros
72    pub fn new() -> IMUData {
73        IMUData {
74            accel: Vector3::zeros(),
75            gyro: Vector3::zeros(),
76        }
77    }
78}
79
80/// Basic structure for holding the strapdown mechanization state in the form of position, velocity, and attitude.
81/// Attitude is stored in matrix form (rotation or direction cosine matrix) and position and velocity are stored as
82/// vectors. The order or the states depends on the coordinate system used. The struct does not care, but the
83/// coordinate system used will determine which functions you should use. Default is NED but nonetheless must be
84/// assigned. For compuational simplicity, latitude and logitude are stored as radians.
85#[derive(Clone, Copy)]
86pub struct StrapdownState {
87    pub position: Vector3<f64>, // latitude (rad), longitude (rad), altitude (m)
88    pub velocity: Vector3<f64>, // velocity in NED/ENU frame (m/s)
89    pub attitude: Rotation3<f64>, // attitude in the form of a rotation matrix (direction cosine matrix; roll-pitch-yaw Euler angles)
90//    ned: bool,                    // NED or ENU; true for NED, false for ENU
91}
92
93impl Debug for StrapdownState {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        let (roll, pitch, yaw) = self.attitude.euler_angles();
96        write!(
97            f,
98            "StrapdownState {{ position: [{:.4}, {:.4}, {:.4}], velocity: [{:.4}, {:.4}, {:.4}], attitude: [{:.4}, {:.4}, {:.4}] }}",
99            self.position[0].to_degrees(),
100            self.position[1].to_degrees(),
101            self.position[2],
102            self.velocity[0],
103            self.velocity[1],
104            self.velocity[2],
105            roll.to_degrees(),
106            pitch.to_degrees(),
107            yaw.to_degrees()
108        )
109    }
110}
111
112impl StrapdownState {
113    /// Create a new StrapdownState with all zeros
114    pub fn new() -> StrapdownState {
115        StrapdownState {
116            position: Vector3::zeros(),
117            velocity: Vector3::zeros(),
118            attitude: Rotation3::identity(),
119            //ned: true,
120        }
121    }
122    /// Create a new StrapdownState from a position, velocity, and attitude vector, where the attitude vector
123    /// is in the form of Euler angles in degrees
124    pub fn new_from(
125        position: Vector3<f64>,
126        velocity: Vector3<f64>,
127        attitude: Vector3<f64>,
128    ) -> StrapdownState {
129        StrapdownState {
130            attitude: Rotation3::from_euler_angles(
131                attitude[0].to_radians(),
132                attitude[1].to_radians(),
133                attitude[2].to_radians(),
134            ),
135            velocity: velocity,
136            position: position,
137            //ned: true,
138        }
139    }
140    /// Convert a one dimensional vector to a StrapdownState
141    ///
142    /// The vector is in the form of:
143    /// (pn, pe, pd, v_n, v_e, v_d, roll, pitch, yaw) where the angles for attitude, latitude (pn),
144    /// and longitude (pe) are in radians.
145    pub fn new_from_vector(state: SVector<f64, 9>) -> StrapdownState {
146        StrapdownState {
147            attitude: Rotation3::from_euler_angles(state[6], state[7], state[8]),
148            velocity: Vector3::new(state[3], state[4], state[5]),
149            position: Vector3::new(state[0], state[1], state[2]),
150            //ned: is_ned,
151        }
152    }
153    /// NED Attitude update equation
154    fn attitude_update(&self, gyros: &Vector3<f64>, dt: f64) -> Matrix3<f64> {
155        let transport_rate: Matrix3<f64> = earth::vector_to_skew_symmetric(&earth::transport_rate(
156            &self.position[0],
157            &self.position[2],
158            &self.velocity,
159        ));
160        let rotation_rate: Matrix3<f64> =
161            earth::vector_to_skew_symmetric(&earth::earth_rate_lla(&self.position[0]));
162        let omega_ib: Matrix3<f64> = earth::vector_to_skew_symmetric(&gyros);
163        let c_1: Matrix3<f64> = &self.attitude * (Matrix3::identity() + omega_ib * dt)
164            - (rotation_rate + transport_rate) * &self.attitude * dt;
165        return c_1;
166    }
167    /// Velocity update in NED
168    fn velocity_update(&self, f_1: &Vector3<f64>, dt: f64) -> Vector3<f64> {
169        let transport_rate: Matrix3<f64> = earth::vector_to_skew_symmetric(&earth::transport_rate(
170            &self.position[0],
171            &self.position[2],
172            &self.velocity,
173        ));
174        let rotation_rate: Matrix3<f64> =
175            earth::vector_to_skew_symmetric(&earth::earth_rate_lla(&self.position[0]));
176        let r = earth::ecef_to_lla(&self.position[0], &self.position[1]);
177        let grav: Vector3<f64> = earth::gravitation(&self.position[0], &self.position[1], &self.position[2]);
178        let v_1: Vector3<f64> = &self.velocity
179            + (f_1 + grav - r * (transport_rate + 2.0 * rotation_rate) * &self.velocity) * dt;
180        return v_1;
181    }
182
183    /// NED form of the forward kinematics equations. Corresponds to section 5.4 Local-Navigation Frame Equations.
184    pub fn forward(&mut self, imu_data: &IMUData, dt: f64) {
185        // Extract the attitude matrix from the current state
186        let c_0: Rotation3<f64> = self.attitude.clone();
187        // Attitude update; Equation 5.46
188        let c_1: Matrix3<f64> = self.attitude_update(&imu_data.gyro, dt);
189        // Specific force transformation; Equation 5.47
190        let f_1: Vector3<f64> = 0.5 * (c_0.matrix() + c_1) * imu_data.accel;
191        // Velocity update; Equation 5.54
192        let v_0: Vector3<f64> = self.velocity.clone();
193        let v_1: Vector3<f64> = self.velocity_update(&f_1, dt);
194        // Position update; Equation 5.56
195        let (r_n, r_e_0, _) = earth::principal_radii(&self.position[0], &self.position[2]);
196        let lat_0: f64 = self.position[0].clone().to_radians();
197        let alt_0: f64 = self.position[2].clone();
198        // Altitude update
199        self.position[2] += 0.5 * (v_0[2] + v_1[2]) * dt;
200        // Latitude update
201        let lat_1: f64 = &self.position[0].to_radians()
202            + 0.5 * (v_0[0] / (r_n + alt_0) + v_1[0] / (r_n + &self.position[2])) * dt;
203        // Longitude udpate
204        let (_, r_e_1, _) = earth::principal_radii(&lat_1, &self.position[2]);
205        let lon_1: f64 = &self.position[1].to_radians()
206            + 0.5
207                * (v_0[1] / ((r_e_0 + alt_0) * lat_0.cos())
208                    + v_1[1] / ((r_e_1 + &self.position[2]) * &lat_1.cos()))
209                * dt;
210        // Update position to degrees
211        self.position[0] = lat_1.to_degrees();
212        self.position[1] = lon_1.to_degrees();
213        // Update attitude to rotation
214        self.attitude = Rotation3::from_matrix(&c_1);
215        // Update velocity
216        self.velocity = v_1;
217    }
218
219    /// ENU form of the forward kinematics equations. Corresponds to section 5.4 Local-Navigation Frame Equations. Mathematrically identical to NED,
220    /// but the coordinate order in the data structures is different. Attitude is still specified by a roll-pitch-yaw Euler angle sequence.
221    // pub fn forward_enu(&mut self, imu_data: &IMUData, dt: f64) {
222    //
223    // }
224
225    /// Convert the StrapdownState to a one dimensional vector
226    /// The vector is in the form of:
227    /// [pn, pe, pd, v_n, v_e, v_d, phi, theta, psi]
228    pub fn to_vector(&self, in_degrees: bool) -> SVector<f64, 9> {
229        let mut state: SVector<f64, 9> = SVector::zeros();
230        state[2] = self.position[2];
231        state[3] = self.velocity[0];
232        state[4] = self.velocity[1];
233        state[5] = self.velocity[2];
234        let (roll, pitch, yaw) = &self.attitude.euler_angles();
235        if in_degrees {
236            state[0] = self.position[0].to_degrees();
237            state[1] = self.position[1].to_degrees();
238            state[6] = Deg(roll.to_degrees()).0;
239            state[7] = Deg(pitch.to_degrees()).0;
240            state[8] = Deg(yaw.to_degrees()).0;
241        } else {
242            state[6] = *roll;
243            state[7] = *pitch;
244            state[8] = *yaw;
245        }
246        return state;
247    }
248}
249
250// Miscellaneous functions for wrapping angles
251
252/// Wrap an angle to the range -180 to 180 degrees
253/// This function is generic and can be used with any type that implements the necessary traits.
254pub fn wrap_to_180<T>(angle: T) -> T
255where
256    T: PartialOrd + Copy + std::ops::SubAssign + std::ops::AddAssign + From<f64>,
257{
258    let mut wrapped: T = angle;
259    while wrapped > T::from(180.0) {
260        wrapped -= T::from(360.0);
261    }
262    while wrapped < T::from(-180.0) {
263        wrapped += T::from(360.0);
264    }
265    return wrapped;
266}
267
268/// Wrap an angle to the range 0 to 360 degrees
269/// This function is generic and can be used with any type that implements the necessary traits.
270pub fn wrap_to_360<T>(angle: T) -> T
271where
272    T: PartialOrd + Copy + std::ops::SubAssign + std::ops::AddAssign + From<f64>,
273{
274    let mut wrapped: T = angle;
275    while wrapped > T::from(360.0) {
276        wrapped -= T::from(360.0);
277    }
278    while wrapped < T::from(0.0) {
279        wrapped += T::from(360.0);
280    }
281    return wrapped;
282}
283/// Wrap an angle to the range 0 to 2*pi radians
284/// This function is generic and can be used with any type that implements the necessary traits.
285pub fn wrap_to_pi<T>(angle: T) -> T
286where
287    T: PartialOrd + Copy + std::ops::SubAssign + std::ops::AddAssign + From<f64>,
288{
289    let mut wrapped: T = angle;
290    while wrapped > T::from(std::f64::consts::PI) {
291        wrapped -= T::from(2.0 * std::f64::consts::PI);
292    }
293    while wrapped < T::from(-std::f64::consts::PI) {
294        wrapped += T::from(2.0 * std::f64::consts::PI);
295    }
296    return wrapped;
297}
298/// Wrap an angle to the range 0 to 2*pi radians
299/// This function is generic and can be used with any type that implements the necessary traits.
300pub fn wrap_to_2pi<T>(angle: T) -> T
301where
302    T: PartialOrd + Copy + std::ops::SubAssign + std::ops::AddAssign + From<f64>,
303    T: PartialOrd + Copy + std::ops::SubAssign + std::ops::AddAssign + From<i32>,
304{
305    let mut wrapped: T = angle;
306    while wrapped > T::from(2.0 * std::f64::consts::PI) {
307        wrapped -= T::from(2.0 * std::f64::consts::PI);
308    }
309    while wrapped < T::from(0.0) {
310        wrapped += T::from(2.0 * std::f64::consts::PI);
311    }
312    return wrapped;
313}
314
315// Note: nalgebra does not yet have a well developed testing framework for directly comparing
316// nalgebra data structures. Rather than directly comparing, check the individual items.
317#[cfg(test)]
318mod tests {
319    use super::*;
320    use assert_approx_eq::assert_approx_eq;
321
322    #[test]
323    fn test_strapdown_state_new() {
324        let state = StrapdownState::new();
325        assert_eq!(state.position, Vector3::zeros());
326        assert_eq!(state.velocity, Vector3::zeros());
327        assert_eq!(state.attitude, Rotation3::identity());
328    }
329    #[test]
330    fn test_to_vector_zeros() {
331        let state = StrapdownState::new();
332        let state_vector = state.to_vector(true);
333        let zeros: SVector<f64, 9> = SVector::zeros();
334        assert_eq!(state_vector, zeros);
335    }
336    #[test]
337    fn test_new_from_vector() {
338        let roll: f64 = 15.0;
339        let pitch: f64 = 45.0;
340        let yaw: f64 = 90.0;
341        let state_vector: SVector<f64, 9> = nalgebra::vector![0.0, 0.0, 0.0, 0.0, 0.0, 0.0, roll.to_radians(), pitch.to_radians(), yaw.to_radians()];
342        let state: StrapdownState = StrapdownState::new_from_vector(state_vector);
343        assert_eq!(state.position, Vector3::new(0.0, 0.0, 0.0));
344        assert_eq!(state.velocity, Vector3::new(0.0, 0.0, 0.0));
345        let eulers = state.attitude.euler_angles();
346        assert_approx_eq!(eulers.0.to_degrees(), roll, 1e-6);
347        assert_approx_eq!(eulers.1.to_degrees(), pitch, 1e-6);
348        assert_approx_eq!(eulers.2.to_degrees(), yaw, 1e-6);
349    }
350    #[test]
351    fn test_dcm_to_vector() {
352        let state: StrapdownState = StrapdownState::new_from(
353            Vector3::new(0.0, 1.0, 2.0),
354            Vector3::new(0.0, 15.0, 45.0),
355            Vector3::new(0.0, 0.0, 0.0),
356        );
357        let state_vector = state.to_vector(true);
358        assert_eq!(state_vector[0], 0.0);
359        assert_eq!(state_vector[1], (1.0_f64).to_degrees());
360        assert_eq!(state_vector[2], 2.0);
361        assert_eq!(state_vector[3], 0.0);
362        assert_eq!(state_vector[4], 15.0);
363        assert_eq!(state_vector[5], 45.0);
364        assert_eq!(state_vector[6], 0.0);
365        assert_eq!(state_vector[7], 0.0);
366        assert_eq!(state_vector[8], 0.0);
367    }
368    #[test]
369    // Test rotation
370    fn test_attitude_update_no_motion() {
371        let gyros = Vector3::new(0.0, 0.0, 0.0);
372        let state: StrapdownState = StrapdownState::new();
373        let dt = 1.0;
374        let new_attitude = state.attitude_update(&gyros, dt);
375        assert_approx_eq!(new_attitude[(0, 0)], 1.0, 1e-3);
376        assert_approx_eq!(new_attitude[(0, 1)], 0.0, 1e-3);
377        assert_approx_eq!(new_attitude[(0, 2)], 0.0, 1e-3);
378        assert_approx_eq!(new_attitude[(1, 0)], 0.0, 1e-3);
379        assert_approx_eq!(new_attitude[(1, 1)], 1.0, 1e-3);
380        assert_approx_eq!(new_attitude[(1, 2)], 0.0, 1e-3);
381        assert_approx_eq!(new_attitude[(2, 0)], 0.0, 1e-3);
382        assert_approx_eq!(new_attitude[(2, 1)], 0.0, 1e-3);
383        assert_approx_eq!(new_attitude[(2, 2)], 1.0, 1e-3);
384    }
385    #[test]
386    fn test_attitude_update_yawing() {
387        let gyros = Vector3::new(0.0, 0.0, 0.1);
388        let state: StrapdownState = StrapdownState::new();
389        let dt = 1.0;
390        let new_attitude = state.attitude_update(&gyros, dt);
391        let eulers = Rotation3::from_matrix(&new_attitude).euler_angles();
392        assert_approx_eq!(eulers.0, 0.0, 1e-3);
393        assert_approx_eq!(eulers.1, 0.0, 1e-3);
394        assert_approx_eq!(eulers.2, 0.1, 1e-3);
395    }
396    #[test]
397    fn test_attitude_update_rolling() {
398        let gyros = Vector3::new(0.1, 0.0, 0.0);
399        let state: StrapdownState = StrapdownState::new();
400        let dt = 1.0;
401        let new_attitude = state.attitude_update(&gyros, dt);
402        let eulers = Rotation3::from_matrix(&new_attitude).euler_angles();
403        assert_approx_eq!(eulers.0, 0.1, 1e-3);
404        assert_approx_eq!(eulers.1, 0.0, 1e-3);
405        assert_approx_eq!(eulers.2, 0.0, 1e-3);
406    }
407    #[test]
408    fn test_attitude_update_pitching() {
409        let gyros = Vector3::new(0.0, 0.1, 0.0);
410        let state: StrapdownState = StrapdownState::new();
411        let dt = 1.0;
412        let new_attitude = state.attitude_update(&gyros, dt);
413        let eulers = Rotation3::from_matrix(&new_attitude).euler_angles();
414        assert_approx_eq!(eulers.0, 0.0, 1e-3);
415        assert_approx_eq!(eulers.1, 0.1, 1e-3);
416        assert_approx_eq!(eulers.2, 0.0, 1e-3);
417    }
418
419    #[test]
420    // Test the forward mechanization
421    fn test_forward_freefall() {
422        let mut state: StrapdownState = StrapdownState::new_from(
423            Vector3::new(0.0, 0.0, 0.0),
424            Vector3::new(0.0, 0.0, 0.0),
425            Vector3::new(0.0, 0.0, 0.0),
426        );
427        let imu_data = IMUData {
428            accel: Vector3::new(0.0, 0.0, 0.0), // Currently configured as relative body-frame acceleration
429            gyro: Vector3::new(0.0, 0.0, 0.0),
430        };
431        state.forward(&imu_data, 1.0);
432        assert_approx_eq!(state.velocity[0], 0.00, 1e-6);
433        assert_approx_eq!(state.velocity[1], 0.0004, 1e-3);
434        assert_approx_eq!(state.velocity[2], 9.8142, 1e-3);
435
436        assert_approx_eq!(state.position[0], 0.0);
437        assert_approx_eq!(state.position[1], 0.0);
438        assert_approx_eq!(state.position[2], 4.9071, 1e-3);
439
440        let attitude = state.attitude.matrix();
441        assert_approx_eq!(&attitude[(0, 0)], 1.0, 1e-4);
442        assert_approx_eq!(&attitude[(0, 1)], 0.0, 1e-4);
443        assert_approx_eq!(&attitude[(0, 2)], 0.0, 1e-4);
444        assert_approx_eq!(&attitude[(1, 0)], 0.0, 1e-4);
445        assert_approx_eq!(&attitude[(1, 1)], 1.0, 1e-4);
446        assert_approx_eq!(&attitude[(1, 2)], 0.0, 1e-4);
447        assert_approx_eq!(&attitude[(2, 0)], 0.0, 1e-4);
448        assert_approx_eq!(&attitude[(2, 1)], 0.0, 1e-4);
449        assert_approx_eq!(&attitude[(2, 2)], 1.0, 1e-4);
450    }
451    fn test_wrap_to_180() {
452        assert_eq!(super::wrap_to_180(190.0), -170.0);
453        assert_eq!(super::wrap_to_180(-190.0), 170.0);
454        assert_eq!(super::wrap_to_180(0.0), 0.0);
455        assert_eq!(super::wrap_to_180(180.0), 180.0);
456        assert_eq!(super::wrap_to_180(-180.0), -180.0);
457    }
458    #[test]
459    fn test_wrap_to_360() {
460        assert_eq!(super::wrap_to_360(370.0), 10.0);fn test_wrap_to_180() {
461            assert_eq!(super::wrap_to_180(190.0), -170.0);
462            assert_eq!(super::wrap_to_180(-190.0), 170.0);
463            assert_eq!(super::wrap_to_180(0.0), 0.0);
464            assert_eq!(super::wrap_to_180(180.0), 180.0);
465            assert_eq!(super::wrap_to_180(-180.0), -180.0);
466        }
467        #[test]
468        fn test_wrap_to_360() {
469            assert_eq!(super::wrap_to_360(370.0), 10.0);
470            assert_eq!(super::wrap_to_360(-10.0), 350.0);
471            assert_eq!(super::wrap_to_360(0.0), 0.0);
472        }
473        #[test]
474        fn test_wrap_to_pi() {
475            assert_eq!(super::wrap_to_pi(3.0 * std::f64::consts::PI), std::f64::consts::PI);
476            assert_eq!(super::wrap_to_pi(-3.0 * std::f64::consts::PI), -std::f64::consts::PI);
477            assert_eq!(super::wrap_to_pi(0.0), 0.0);
478            assert_eq!(super::wrap_to_pi(std::f64::consts::PI), std::f64::consts::PI);
479            assert_eq!(super::wrap_to_pi(-std::f64::consts::PI), -std::f64::consts::PI);
480        }
481        #[test]
482        fn test_wrap_to_2pi() {
483            assert_eq!(super::wrap_to_2pi(7.0 * std::f64::consts::PI), std::f64::consts::PI);
484            assert_eq!(super::wrap_to_2pi(-5.0 * std::f64::consts::PI), std::f64::consts::PI);
485            assert_eq!(super::wrap_to_2pi(0.0), 0.0);
486            assert_eq!(super::wrap_to_2pi(std::f64::consts::PI), std::f64::consts::PI);
487            assert_eq!(super::wrap_to_2pi(-std::f64::consts::PI), std::f64::consts::PI);
488        }
489        assert_eq!(super::wrap_to_360(-10.0), 350.0);
490        assert_eq!(super::wrap_to_360(0.0), 0.0);
491    }
492    #[test]
493    fn test_wrap_to_pi() {
494        assert_eq!(super::wrap_to_pi(3.0 * std::f64::consts::PI), std::f64::consts::PI);
495        assert_eq!(super::wrap_to_pi(-3.0 * std::f64::consts::PI), -std::f64::consts::PI);
496        assert_eq!(super::wrap_to_pi(0.0), 0.0);
497        assert_eq!(super::wrap_to_pi(std::f64::consts::PI), std::f64::consts::PI);
498        assert_eq!(super::wrap_to_pi(-std::f64::consts::PI), -std::f64::consts::PI);
499    }
500    #[test]
501    fn test_wrap_to_2pi() {
502        assert_eq!(super::wrap_to_2pi(7.0 * std::f64::consts::PI), std::f64::consts::PI);
503        assert_eq!(super::wrap_to_2pi(-5.0 * std::f64::consts::PI), std::f64::consts::PI);
504        assert_eq!(super::wrap_to_2pi(0.0), 0.0);
505        assert_eq!(super::wrap_to_2pi(std::f64::consts::PI), std::f64::consts::PI);
506        assert_eq!(super::wrap_to_2pi(-std::f64::consts::PI), std::f64::consts::PI);
507    }
508}
509
510
511// tester function for building
512pub fn add(a: f64, b: f64) -> f64 {
513    a + b
514}