rust_gnc/units/angular.rs
1//! # Angular Units
2//!
3//! This module provides type-safe representations for angular measurements.
4//! It handles the circular logic required for navigation, specifically
5//! the transition across the ±π (180°) boundary.
6
7/// A type-safe wrapper for angular measurements in radians.
8///
9/// Range: Usually normalized to (-π, π] for flight dynamics.
10#[derive(Debug, Clone, Copy, PartialEq, Default)]
11pub struct Radians(pub f32);
12
13impl Radians {
14 /// Normalizes the angle to the range (-π, π].
15 ///
16 /// This is essential for preventing "the long way around" maneuvers
17 /// and ensuring the PID controller receives the smallest possible error.
18 ///
19 /// ### Performance
20 /// This implementation uses a deterministic approach to ensure consistent
21 /// execution time in real-time flight loops.
22 pub fn normalize(&self) -> Self {
23 let pi = core::f32::consts::PI;
24 let two_pi = 2.0 * pi;
25
26 // Industry Standard: Use a non-looping normalization for O(1) performance.
27 // This prevents timing jitter on microcontrollers if the input is very large.
28 let mut angle = self.0;
29 if angle <= -pi || angle > pi {
30 angle = angle - two_pi * libm::floorf((angle + pi) / two_pi);
31 }
32 Radians(angle)
33 }
34
35 /// Calculates the shortest angular distance to a target.
36 ///
37 /// Returns a value in the range (-π, π]. A positive result indicates
38 /// a clockwise rotation, while a negative result indicates counter-clockwise.
39 ///
40 /// ### Example
41 /// Moving from 179° to -179° will return a distance of 2° (0.035 rad)
42 /// instead of -358°.
43 pub fn shortest_distance_to(&self, target: Radians) -> f32 {
44 let delta = target.0 - self.0;
45 Radians(delta).normalize().0
46 }
47}
48
49/// A type-safe wrapper for angular measurements in degrees.
50///
51/// Primarily used for human-readable telemetry, logging, and configuration.
52#[derive(Debug, Clone, Copy, PartialEq, Default)]
53pub struct Degrees(pub f32);
54
55impl From<Radians> for Degrees {
56 /// Converts Radians to Degrees using the standard constant π.
57 fn from(radians: Radians) -> Self {
58 Degrees(radians.0 * 180.0 / core::f32::consts::PI)
59 }
60}
61
62impl From<Degrees> for Radians {
63 /// Converts Degrees to Radians. Used for ingesting user configuration
64 /// into the flight-ready physics engine.
65 fn from(degrees: Degrees) -> Self {
66 Radians(degrees.0 * core::f32::consts::PI / 180.0)
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn test_normalization_boundaries() {
76 // Test wrap-around at PI
77 assert!((Radians(3.2).normalize().0 - (-3.0831)).abs() < 0.001);
78 // Test wrap-around at -PI
79 assert!((Radians(-3.2).normalize().0 - 3.0831).abs() < 0.001);
80 // Test identity
81 assert_eq!(Radians(1.0).normalize().0, 1.0);
82 }
83}