Skip to main content

lox_core/
units.rs

1// SPDX-FileCopyrightText: 2025 Helge Eichhorn <git@helgeeichhorn.de>
2//
3// SPDX-License-Identifier: MPL-2.0
4
5//! Newtype wrappers for unitful [`f64`] double precision values
6
7use std::{
8    f64::consts::{FRAC_PI_2, FRAC_PI_4, PI, TAU},
9    fmt::{Display, Formatter, Result},
10    ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign},
11};
12
13use glam::DMat3;
14use lox_test_utils::ApproxEq;
15
16use crate::f64::consts::SECONDS_PER_DAY;
17
18/// Degrees in full circle
19pub const DEGREES_IN_CIRCLE: f64 = 360.0;
20
21/// Arcseconds in full circle
22pub const ARCSECONDS_IN_CIRCLE: f64 = DEGREES_IN_CIRCLE * 60.0 * 60.0;
23
24/// Radians per arcsecond
25pub const RADIANS_IN_ARCSECOND: f64 = TAU / ARCSECONDS_IN_CIRCLE;
26
27type Radians = f64;
28
29/// Angle in radians.
30#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32#[repr(transparent)]
33pub struct Angle(Radians);
34
35impl Angle {
36    /// An angle equal to zero.
37    pub const ZERO: Self = Self(0.0);
38    /// An angle equal to π.
39    pub const PI: Self = Self(PI);
40    /// An angle equal to τ = 2π.
41    pub const TAU: Self = Self(TAU);
42    /// An angle equal to π/2.
43    pub const FRAC_PI_2: Self = Self(FRAC_PI_2);
44    /// An angle equal to π/4.
45    pub const FRAC_PI_4: Self = Self(FRAC_PI_4);
46
47    /// Creates a new angle from an `f64` value in radians.
48    pub const fn new(rad: f64) -> Self {
49        Self(rad)
50    }
51
52    /// Creates a new angle from an `f64` value in radians.
53    pub const fn radians(rad: f64) -> Self {
54        Self(rad)
55    }
56
57    /// Creates a new angle from an `f64` value in radians and normalize the angle
58    /// to the interval [0.0, 2π).
59    pub const fn radians_normalized(rad: f64) -> Self {
60        Self(rad).mod_two_pi()
61    }
62
63    /// Creates a new angle from an `f64` value in radians and normalize the angle
64    /// to the interval (-2π, 2π).
65    pub const fn radians_normalized_signed(rad: f64) -> Self {
66        Self(rad).mod_two_pi_signed()
67    }
68
69    /// Creates a new angle from an `f64` value in degrees.
70    pub const fn degrees(deg: f64) -> Self {
71        Self(deg.to_radians())
72    }
73
74    /// Creates a new angle from hours, minutes, and seconds (HMS notation).
75    pub const fn from_hms(hours: i64, minutes: u8, seconds: f64) -> Self {
76        Self::degrees(15.0 * (hours as f64 + minutes as f64 / 60.0 + seconds / 3600.0))
77    }
78
79    /// Creates a new angle from an `f64` value in degrees and normalize the angle
80    /// to the interval [0.0, 2π).
81    pub const fn degrees_normalized(deg: f64) -> Self {
82        Self((deg % DEGREES_IN_CIRCLE).to_radians()).mod_two_pi()
83    }
84
85    /// Creates a new angle from an `f64` value in degrees and normalize the angle
86    /// to the interval (-2π, 2π).
87    pub const fn degrees_normalized_signed(deg: f64) -> Self {
88        Self((deg % DEGREES_IN_CIRCLE).to_radians())
89    }
90
91    /// Creates a new angle from an `f64` value in arcseconds.
92    pub const fn arcseconds(asec: f64) -> Self {
93        Self(asec * RADIANS_IN_ARCSECOND)
94    }
95
96    /// Creates a new angle from an `f64` value in arcseconds and normalize the angle
97    /// to the interval [0.0, 2π).
98    pub const fn arcseconds_normalized(asec: f64) -> Self {
99        Self((asec % ARCSECONDS_IN_CIRCLE) * RADIANS_IN_ARCSECOND).mod_two_pi()
100    }
101
102    /// Creates a new angle from an `f64` value in arcseconds and normalize the angle
103    /// to the interval (-2π, 2π).
104    pub const fn arcseconds_normalized_signed(asec: f64) -> Self {
105        Self((asec % ARCSECONDS_IN_CIRCLE) * RADIANS_IN_ARCSECOND)
106    }
107
108    /// Returns `true` if the angle is exactly zero.
109    pub fn is_zero(&self) -> bool {
110        self.0 == 0.0
111    }
112
113    /// Returns the absolute value of the angle.
114    pub const fn abs(&self) -> Self {
115        Self(self.0.abs())
116    }
117
118    /// Creates an angle from the arcsine of a value.
119    pub fn from_asin(value: f64) -> Self {
120        Self(value.asin())
121    }
122
123    /// Creates an angle from the inverse hyperbolic sine of a value.
124    pub fn from_asinh(value: f64) -> Self {
125        Self(value.asinh())
126    }
127
128    /// Creates an angle from the arccosine of a value.
129    pub fn from_acos(value: f64) -> Self {
130        Self(value.acos())
131    }
132
133    /// Creates an angle from the inverse hyperbolic cosine of a value.
134    pub fn from_acosh(value: f64) -> Self {
135        Self(value.acosh())
136    }
137
138    /// Creates an angle from the arctangent of a value.
139    pub fn from_atan(value: f64) -> Self {
140        Self(value.atan())
141    }
142
143    /// Creates an angle from the inverse hyperbolic tangent of a value.
144    pub fn from_atanh(value: f64) -> Self {
145        Self(value.atanh())
146    }
147
148    /// Creates an angle from the two-argument arctangent of `y` and `x`.
149    pub fn from_atan2(y: f64, x: f64) -> Self {
150        Self(y.atan2(x))
151    }
152
153    /// Returns the cosine of the angle.
154    pub fn cos(&self) -> f64 {
155        self.0.cos()
156    }
157
158    /// Returns the hyperbolic cosine of the angle.
159    pub fn cosh(&self) -> f64 {
160        self.0.cosh()
161    }
162
163    /// Returns the sine of the angle.
164    pub fn sin(&self) -> f64 {
165        self.0.sin()
166    }
167
168    /// Returns the hyperbolic sine of the angle.
169    pub fn sinh(&self) -> f64 {
170        self.0.sinh()
171    }
172
173    /// Returns sine and cosine of the angle.
174    pub fn sin_cos(&self) -> (f64, f64) {
175        self.0.sin_cos()
176    }
177
178    /// Returns the tangent of the angle.
179    pub fn tan(&self) -> f64 {
180        self.0.tan()
181    }
182
183    /// Returns the hyperbolic tangent of the angle.
184    pub fn tanh(&self) -> f64 {
185        self.0.tanh()
186    }
187
188    /// Returns a new angle that is normalized to the interval [0.0, 2π).
189    pub const fn mod_two_pi(&self) -> Self {
190        let mut a = self.0 % TAU;
191        if a < 0.0 {
192            a += TAU
193        }
194        Self(a)
195    }
196
197    /// Returns a new angle that is normalized to the interval (-2π, 2π).
198    pub const fn mod_two_pi_signed(&self) -> Self {
199        Self(self.0 % TAU)
200    }
201
202    /// Returns a new angle that is normalized to a (-π, π) interval
203    /// centered around `center`.
204    pub const fn normalize_two_pi(&self, center: Self) -> Self {
205        Self(self.0 - TAU * ((self.0 + PI - center.0) / TAU).floor())
206    }
207
208    /// Returns the value of the angle in radians.
209    pub const fn as_f64(&self) -> f64 {
210        self.0
211    }
212
213    /// Returns the value of the angle in radians.
214    pub const fn to_radians(&self) -> f64 {
215        self.0
216    }
217
218    /// Returns the value of the angle in degrees.
219    pub const fn to_degrees(&self) -> f64 {
220        self.0.to_degrees()
221    }
222
223    /// Returns the value of the angle in arcseconds.
224    pub const fn to_arcseconds(&self) -> f64 {
225        self.0 / RADIANS_IN_ARCSECOND
226    }
227
228    /// Returns the 3×3 rotation matrix for a rotation about the X axis.
229    pub fn rotation_x(&self) -> DMat3 {
230        DMat3::from_rotation_x(-self.to_radians())
231    }
232
233    /// Returns the 3×3 rotation matrix for a rotation about the Y axis.
234    pub fn rotation_y(&self) -> DMat3 {
235        DMat3::from_rotation_y(-self.to_radians())
236    }
237
238    /// Returns the 3×3 rotation matrix for a rotation about the Z axis.
239    pub fn rotation_z(&self) -> DMat3 {
240        DMat3::from_rotation_z(-self.to_radians())
241    }
242}
243
244impl Display for Angle {
245    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
246        self.0.to_degrees().fmt(f)?;
247        write!(f, " deg")
248    }
249}
250
251/// A trait for creating [`Angle`] instances from primitives.
252///
253/// By default it is implemented for [`f64`] and [`i64`].
254///
255/// # Examples
256///
257/// ```
258/// use lox_core::units::AngleUnits;
259///
260/// let angle = 360.deg();
261/// assert_eq!(angle.to_radians(), std::f64::consts::TAU);
262/// ```
263pub trait AngleUnits {
264    /// Creates an angle from a value in radians.
265    fn rad(&self) -> Angle;
266    /// Creates an angle from a value in degrees.
267    fn deg(&self) -> Angle;
268    /// Creates an angle from a value in arcseconds.
269    fn arcsec(&self) -> Angle;
270    /// Creates an angle from a value in milliarcseconds.
271    fn mas(&self) -> Angle;
272    /// Creates an angle from a value in microarcseconds.
273    fn uas(&self) -> Angle;
274}
275
276impl AngleUnits for f64 {
277    fn rad(&self) -> Angle {
278        Angle::radians(*self)
279    }
280
281    fn deg(&self) -> Angle {
282        Angle::degrees(*self)
283    }
284
285    fn arcsec(&self) -> Angle {
286        Angle::arcseconds(*self)
287    }
288
289    fn mas(&self) -> Angle {
290        Angle::arcseconds(self * 1e-3)
291    }
292
293    fn uas(&self) -> Angle {
294        Angle::arcseconds(self * 1e-6)
295    }
296}
297
298impl AngleUnits for i64 {
299    fn rad(&self) -> Angle {
300        Angle::radians(*self as f64)
301    }
302
303    fn deg(&self) -> Angle {
304        Angle::degrees(*self as f64)
305    }
306
307    fn arcsec(&self) -> Angle {
308        Angle::arcseconds(*self as f64)
309    }
310
311    fn mas(&self) -> Angle {
312        Angle::arcseconds(*self as f64 * 1e-3)
313    }
314
315    fn uas(&self) -> Angle {
316        Angle::arcseconds(*self as f64 * 1e-6)
317    }
318}
319
320/// The astronomical unit in meters.
321pub const ASTRONOMICAL_UNIT: f64 = 1.495978707e11;
322
323type Meters = f64;
324
325/// Distance in meters.
326#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
327#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
328#[repr(transparent)]
329pub struct Distance(Meters);
330
331impl Distance {
332    /// Create a new distance from an `f64` value in meters.
333    pub const fn new(m: f64) -> Self {
334        Self(m)
335    }
336
337    /// Create a new distance from an `f64` value in meters.
338    pub const fn meters(m: f64) -> Self {
339        Self(m)
340    }
341
342    /// Create a new distance from an `f64` value in kilometers.
343    pub const fn kilometers(m: f64) -> Self {
344        Self(m * 1e3)
345    }
346
347    /// Create a new distance from an `f64` value in astronomical units.
348    pub const fn astronomical_units(au: f64) -> Self {
349        Self(au * ASTRONOMICAL_UNIT)
350    }
351
352    /// Returns the value of the distance in meters as an `f64`.
353    pub const fn as_f64(&self) -> f64 {
354        self.0
355    }
356
357    /// Returns the value of the distance in meters.
358    pub const fn to_meters(&self) -> f64 {
359        self.0
360    }
361
362    /// Returns the value of the distance in kilometers.
363    pub const fn to_kilometers(&self) -> f64 {
364        self.0 * 1e-3
365    }
366
367    /// Returns the value of the distance in astronomical units.
368    pub const fn to_astronomical_units(&self) -> f64 {
369        self.0 / ASTRONOMICAL_UNIT
370    }
371}
372
373impl Display for Distance {
374    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
375        (1e-3 * self.0).fmt(f)?;
376        write!(f, " km")
377    }
378}
379
380/// A trait for creating [`Distance`] instances from primitives.
381///
382/// By default it is implemented for [`f64`] and [`i64`].
383///
384/// # Examples
385///
386/// ```
387/// use lox_core::units::DistanceUnits;
388///
389/// let d = 1.km();
390/// assert_eq!(d.to_meters(), 1e3);
391/// ```
392pub trait DistanceUnits {
393    /// Creates a distance from a value in meters.
394    fn m(&self) -> Distance;
395    /// Creates a distance from a value in kilometers.
396    fn km(&self) -> Distance;
397    /// Creates a distance from a value in astronomical units.
398    fn au(&self) -> Distance;
399}
400
401impl DistanceUnits for f64 {
402    fn m(&self) -> Distance {
403        Distance::meters(*self)
404    }
405
406    fn km(&self) -> Distance {
407        Distance::kilometers(*self)
408    }
409
410    fn au(&self) -> Distance {
411        Distance::astronomical_units(*self)
412    }
413}
414
415impl DistanceUnits for i64 {
416    fn m(&self) -> Distance {
417        Distance::meters(*self as f64)
418    }
419
420    fn km(&self) -> Distance {
421        Distance::kilometers(*self as f64)
422    }
423
424    fn au(&self) -> Distance {
425        Distance::astronomical_units(*self as f64)
426    }
427}
428
429type MetersPerSecond = f64;
430
431/// Velocity in meters per second.
432#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
433#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
434#[repr(transparent)]
435pub struct Velocity(MetersPerSecond);
436
437impl Velocity {
438    /// Creates a new velocity from an `f64` value in m/s.
439    pub const fn new(mps: f64) -> Self {
440        Self(mps)
441    }
442
443    /// Creates a new velocity from an `f64` value in m/s.
444    pub const fn meters_per_second(mps: f64) -> Self {
445        Self(mps)
446    }
447
448    /// Creates a new velocity from an `f64` value in km/s.
449    pub const fn kilometers_per_second(mps: f64) -> Self {
450        Self(mps * 1e3)
451    }
452
453    /// Creates a new velocity from an `f64` value in au/d.
454    pub const fn astronomical_units_per_day(aud: f64) -> Self {
455        Self(aud * ASTRONOMICAL_UNIT / SECONDS_PER_DAY)
456    }
457
458    /// Creates a new velocity from an `f64` value in 1/c.
459    pub const fn fraction_of_speed_of_light(c: f64) -> Self {
460        Self(c * SPEED_OF_LIGHT)
461    }
462
463    /// Returns the value of the velocity in m/s as an `f64`.
464    pub const fn as_f64(&self) -> f64 {
465        self.0
466    }
467
468    /// Returns the value of the velocity in m/s.
469    pub const fn to_meters_per_second(&self) -> f64 {
470        self.0
471    }
472
473    /// Returns the value of the velocity in km/s.
474    pub const fn to_kilometers_per_second(&self) -> f64 {
475        self.0 * 1e-3
476    }
477
478    /// Returns the value of the velocity in au/d.
479    pub const fn to_astronomical_units_per_day(&self) -> f64 {
480        self.0 * SECONDS_PER_DAY / ASTRONOMICAL_UNIT
481    }
482
483    /// Returns the value of the velocity in 1/c.
484    pub const fn to_fraction_of_speed_of_light(&self) -> f64 {
485        self.0 / SPEED_OF_LIGHT
486    }
487}
488
489impl Display for Velocity {
490    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
491        (1e-3 * self.0).fmt(f)?;
492        write!(f, " km/s")
493    }
494}
495
496/// A trait for creating [`Velocity`] instances from primitives.
497///
498/// By default it is implemented for [`f64`] and [`i64`].
499///
500/// # Examples
501///
502/// ```
503/// use lox_core::units::VelocityUnits;
504///
505/// let v = 1.kps();
506/// assert_eq!(v.to_meters_per_second(), 1e3);
507/// ```
508pub trait VelocityUnits {
509    /// Creates a velocity from a value in m/s.
510    fn mps(&self) -> Velocity;
511    /// Creates a velocity from a value in km/s.
512    fn kps(&self) -> Velocity;
513    /// Creates a velocity from a value in au/d.
514    fn aud(&self) -> Velocity;
515    /// Crates a velocity from a value in 1/c (fraction of the speed of light).
516    fn c(&self) -> Velocity;
517}
518
519impl VelocityUnits for f64 {
520    fn mps(&self) -> Velocity {
521        Velocity::meters_per_second(*self)
522    }
523
524    fn kps(&self) -> Velocity {
525        Velocity::kilometers_per_second(*self)
526    }
527
528    fn aud(&self) -> Velocity {
529        Velocity::astronomical_units_per_day(*self)
530    }
531
532    fn c(&self) -> Velocity {
533        Velocity::fraction_of_speed_of_light(*self)
534    }
535}
536
537impl VelocityUnits for i64 {
538    fn mps(&self) -> Velocity {
539        Velocity::meters_per_second(*self as f64)
540    }
541
542    fn kps(&self) -> Velocity {
543        Velocity::kilometers_per_second(*self as f64)
544    }
545
546    fn aud(&self) -> Velocity {
547        Velocity::astronomical_units_per_day(*self as f64)
548    }
549
550    fn c(&self) -> Velocity {
551        Velocity::fraction_of_speed_of_light(*self as f64)
552    }
553}
554
555/// The speed of light in vacuum in m/s.
556pub const SPEED_OF_LIGHT: f64 = 299792458.0;
557
558/// IEEE letter codes for frequency bands commonly used for satellite communications.
559#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
560#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
561pub enum FrequencyBand {
562    /// HF (High Frequency) – 3 to 30 MHz
563    HF,
564    /// VHF (Very High Frequency) – 30 to 300 MHz
565    VHF,
566    /// UHF (Ultra-High Frequency) – 0.3 to 1 GHz
567    UHF,
568    /// L – 1 to 2 GHz
569    L,
570    /// S – 2 to 4 GHz
571    S,
572    /// C – 4 to 8 GHz
573    C,
574    /// X – 8 to 12 GHz
575    X,
576    /// Kᵤ – 12 to 18 GHz
577    Ku,
578    /// K – 18 to 27 GHz
579    K,
580    /// Kₐ – 27 to 40 GHz
581    Ka,
582    /// V – 40 to 75 GHz
583    V,
584    /// W – 75 to 110 GHz
585    W,
586    /// G – 110 to 300 GHz
587    G,
588}
589
590type Hertz = f64;
591
592/// Frequency in Hertz
593#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
594#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
595#[repr(transparent)]
596pub struct Frequency(Hertz);
597
598impl Frequency {
599    /// Creates a new frequency from an `f64` value in Hz.
600    pub const fn new(hz: Hertz) -> Self {
601        Self(hz)
602    }
603
604    /// Creates a new frequency from an `f64` value in Hz.
605    pub const fn hertz(hz: Hertz) -> Self {
606        Self(hz)
607    }
608
609    /// Creates a new frequency from an `f64` value in KHz.
610    pub const fn kilohertz(hz: Hertz) -> Self {
611        Self(hz * 1e3)
612    }
613
614    /// Creates a new frequency from an `f64` value in MHz.
615    pub const fn megahertz(hz: Hertz) -> Self {
616        Self(hz * 1e6)
617    }
618
619    /// Creates a new frequency from an `f64` value in GHz.
620    pub const fn gigahertz(hz: Hertz) -> Self {
621        Self(hz * 1e9)
622    }
623
624    /// Creates a new frequency from an `f64` value in THz.
625    pub const fn terahertz(hz: Hertz) -> Self {
626        Self(hz * 1e12)
627    }
628
629    /// Returns the value of the frequency in Hz.
630    pub const fn to_hertz(&self) -> f64 {
631        self.0
632    }
633
634    /// Returns the value of the frequency in KHz.
635    pub const fn to_kilohertz(&self) -> f64 {
636        self.0 * 1e-3
637    }
638
639    /// Returns the value of the frequency in MHz.
640    pub const fn to_megahertz(&self) -> f64 {
641        self.0 * 1e-6
642    }
643
644    /// Returns the value of the frequency in GHz.
645    pub const fn to_gigahertz(&self) -> f64 {
646        self.0 * 1e-9
647    }
648
649    /// Returns the value of the frequency in THz.
650    pub const fn to_terahertz(&self) -> f64 {
651        self.0 * 1e-12
652    }
653
654    /// Returns the wavelength.
655    pub fn wavelength(&self) -> Distance {
656        Distance(SPEED_OF_LIGHT / self.0)
657    }
658
659    /// Returns the IEEE letter code if the frequency matches one of the bands.
660    pub fn band(&self) -> Option<FrequencyBand> {
661        match self.0 {
662            f if f < 3e6 => None,
663            f if f < 30e6 => Some(FrequencyBand::HF),
664            f if f < 300e6 => Some(FrequencyBand::VHF),
665            f if f < 1e9 => Some(FrequencyBand::UHF),
666            f if f < 2e9 => Some(FrequencyBand::L),
667            f if f < 4e9 => Some(FrequencyBand::S),
668            f if f < 8e9 => Some(FrequencyBand::C),
669            f if f < 12e9 => Some(FrequencyBand::X),
670            f if f < 18e9 => Some(FrequencyBand::Ku),
671            f if f < 27e9 => Some(FrequencyBand::K),
672            f if f < 40e9 => Some(FrequencyBand::Ka),
673            f if f < 75e9 => Some(FrequencyBand::V),
674            f if f < 110e9 => Some(FrequencyBand::W),
675            f if f < 300e9 => Some(FrequencyBand::G),
676            _ => None,
677        }
678    }
679}
680
681impl Display for Frequency {
682    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
683        (1e-9 * self.0).fmt(f)?;
684        write!(f, " GHz")
685    }
686}
687
688/// A trait for creating [`Frequency`] instances from primitives.
689///
690/// By default it is implemented for [`f64`] and [`i64`].
691///
692/// # Examples
693///
694/// ```
695/// use lox_core::units::FrequencyUnits;
696///
697/// let f = 1.ghz();
698/// assert_eq!(f.to_hertz(), 1e9);
699/// ```
700pub trait FrequencyUnits {
701    /// Creates a frequency from a value in Hz.
702    fn hz(&self) -> Frequency;
703    /// Creates a frequency from a value in KHz.
704    fn khz(&self) -> Frequency;
705    /// Creates a frequency from a value in MHz.
706    fn mhz(&self) -> Frequency;
707    /// Creates a frequency from a value in GHz.
708    fn ghz(&self) -> Frequency;
709    /// Creates a frequency from a value in THz.
710    fn thz(&self) -> Frequency;
711}
712
713impl FrequencyUnits for f64 {
714    fn hz(&self) -> Frequency {
715        Frequency::hertz(*self)
716    }
717
718    fn khz(&self) -> Frequency {
719        Frequency::kilohertz(*self)
720    }
721
722    fn mhz(&self) -> Frequency {
723        Frequency::megahertz(*self)
724    }
725
726    fn ghz(&self) -> Frequency {
727        Frequency::gigahertz(*self)
728    }
729
730    fn thz(&self) -> Frequency {
731        Frequency::terahertz(*self)
732    }
733}
734
735impl FrequencyUnits for i64 {
736    fn hz(&self) -> Frequency {
737        Frequency::hertz(*self as f64)
738    }
739
740    fn khz(&self) -> Frequency {
741        Frequency::kilohertz(*self as f64)
742    }
743
744    fn mhz(&self) -> Frequency {
745        Frequency::megahertz(*self as f64)
746    }
747
748    fn ghz(&self) -> Frequency {
749        Frequency::gigahertz(*self as f64)
750    }
751
752    fn thz(&self) -> Frequency {
753        Frequency::terahertz(*self as f64)
754    }
755}
756
757/// Temperature in Kelvin (deprecated type alias, use [`Temperature`] instead).
758pub type Kelvin = f64;
759
760type KelvinValue = f64;
761
762/// Temperature in Kelvin.
763#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
764#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
765#[repr(transparent)]
766pub struct Temperature(KelvinValue);
767
768impl Temperature {
769    /// Creates a new temperature from an `f64` value in Kelvin.
770    pub const fn new(k: f64) -> Self {
771        Self(k)
772    }
773
774    /// Creates a new temperature from an `f64` value in Kelvin.
775    pub const fn kelvin(k: f64) -> Self {
776        Self(k)
777    }
778
779    /// Returns the value in Kelvin as an `f64`.
780    pub const fn as_f64(&self) -> f64 {
781        self.0
782    }
783
784    /// Returns the value in Kelvin.
785    pub const fn to_kelvin(&self) -> f64 {
786        self.0
787    }
788}
789
790impl Display for Temperature {
791    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
792        self.0.fmt(f)?;
793        write!(f, " K")
794    }
795}
796
797type Watts = f64;
798
799/// Power in Watts.
800#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
801#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
802#[repr(transparent)]
803pub struct Power(Watts);
804
805impl Power {
806    /// Creates a new power from an `f64` value in Watts.
807    pub const fn new(w: f64) -> Self {
808        Self(w)
809    }
810
811    /// Creates a new power from an `f64` value in Watts.
812    pub const fn watts(w: f64) -> Self {
813        Self(w)
814    }
815
816    /// Creates a new power from an `f64` value in kilowatts.
817    pub const fn kilowatts(kw: f64) -> Self {
818        Self(kw * 1e3)
819    }
820
821    /// Returns the value in Watts as an `f64`.
822    pub const fn as_f64(&self) -> f64 {
823        self.0
824    }
825
826    /// Returns the value in Watts.
827    pub const fn to_watts(&self) -> f64 {
828        self.0
829    }
830
831    /// Returns the value in kilowatts.
832    pub const fn to_kilowatts(&self) -> f64 {
833        self.0 * 1e-3
834    }
835
836    /// Returns the value in dBW.
837    pub fn to_dbw(&self) -> f64 {
838        10.0 * self.0.log10()
839    }
840}
841
842impl Display for Power {
843    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
844        self.0.fmt(f)?;
845        write!(f, " W")
846    }
847}
848
849type BitsPerSecond = f64;
850
851/// Data rate in bits per second.
852#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
853#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
854#[repr(transparent)]
855pub struct DataRate(BitsPerSecond);
856
857impl DataRate {
858    /// Creates a new data rate from an `f64` value in bits/s.
859    pub const fn new(bps: f64) -> Self {
860        Self(bps)
861    }
862
863    /// Creates a new data rate from an `f64` value in bits/s.
864    pub const fn bits_per_second(bps: f64) -> Self {
865        Self(bps)
866    }
867
868    /// Creates a new data rate from an `f64` value in kilobits/s.
869    pub const fn kilobits_per_second(kbps: f64) -> Self {
870        Self(kbps * 1e3)
871    }
872
873    /// Creates a new data rate from an `f64` value in megabits/s.
874    pub const fn megabits_per_second(mbps: f64) -> Self {
875        Self(mbps * 1e6)
876    }
877
878    /// Returns the value in bits/s as an `f64`.
879    pub const fn as_f64(&self) -> f64 {
880        self.0
881    }
882
883    /// Returns the value in bits/s.
884    pub const fn to_bits_per_second(&self) -> f64 {
885        self.0
886    }
887
888    /// Returns the value in kilobits/s.
889    pub const fn to_kilobits_per_second(&self) -> f64 {
890        self.0 * 1e-3
891    }
892
893    /// Returns the value in megabits/s.
894    pub const fn to_megabits_per_second(&self) -> f64 {
895        self.0 * 1e-6
896    }
897}
898
899impl Display for DataRate {
900    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
901        (1e-3 * self.0).fmt(f)?;
902        write!(f, " kbps")
903    }
904}
905
906type RadiansPerSecond = f64;
907
908/// Angular rate in radians per second.
909#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
910#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
911#[repr(transparent)]
912pub struct AngularRate(RadiansPerSecond);
913
914impl AngularRate {
915    /// Creates a new angular rate from an `f64` value in rad/s.
916    pub const fn new(rps: f64) -> Self {
917        Self(rps)
918    }
919
920    /// Creates a new angular rate from an `f64` value in rad/s.
921    pub const fn radians_per_second(rps: f64) -> Self {
922        Self(rps)
923    }
924
925    /// Creates a new angular rate from an `f64` value in deg/s.
926    pub const fn degrees_per_second(dps: f64) -> Self {
927        Self(dps.to_radians())
928    }
929
930    /// Returns the value in rad/s as an `f64`.
931    pub const fn as_f64(&self) -> f64 {
932        self.0
933    }
934
935    /// Returns the value in rad/s.
936    pub const fn to_radians_per_second(&self) -> f64 {
937        self.0
938    }
939
940    /// Returns the value in deg/s.
941    pub const fn to_degrees_per_second(&self) -> f64 {
942        self.0.to_degrees()
943    }
944}
945
946impl Display for AngularRate {
947    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
948        self.0.to_degrees().fmt(f)?;
949        write!(f, " deg/s")
950    }
951}
952
953type DecibelValue = f64;
954
955/// A value in decibels.
956#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd, ApproxEq)]
957#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
958#[repr(transparent)]
959pub struct Decibel(DecibelValue);
960
961impl Decibel {
962    /// Creates a new `Decibel` from a value already in dB.
963    pub const fn new(db: f64) -> Self {
964        Self(db)
965    }
966
967    /// Converts a linear power-ratio value to decibels.
968    pub fn from_linear(val: f64) -> Self {
969        Self(10.0 * val.log10())
970    }
971
972    /// Converts this decibel value to a linear power-ratio.
973    pub fn to_linear(self) -> f64 {
974        10.0_f64.powf(self.0 / 10.0)
975    }
976
977    /// Returns the raw `f64` value in dB.
978    pub const fn as_f64(self) -> f64 {
979        self.0
980    }
981}
982
983impl Display for Decibel {
984    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
985        self.0.fmt(f)?;
986        write!(f, " dB")
987    }
988}
989
990/// A trait for creating [`Decibel`] instances from primitives.
991///
992/// By default it is implemented for [`f64`] and [`i64`].
993///
994/// # Examples
995///
996/// ```
997/// use lox_core::units::DecibelUnits;
998///
999/// let d = 3.0.db();
1000/// assert_eq!(d.as_f64(), 3.0);
1001/// ```
1002pub trait DecibelUnits {
1003    /// Creates a decibel value.
1004    fn db(&self) -> Decibel;
1005}
1006
1007impl DecibelUnits for f64 {
1008    fn db(&self) -> Decibel {
1009        Decibel::new(*self)
1010    }
1011}
1012
1013impl DecibelUnits for i64 {
1014    fn db(&self) -> Decibel {
1015        Decibel::new(*self as f64)
1016    }
1017}
1018
1019macro_rules! trait_impls {
1020    ($($unit:ident),*) => {
1021        $(
1022            impl Neg for $unit {
1023                type Output = Self;
1024
1025                fn neg(self) -> Self::Output {
1026                    Self(-self.0)
1027                }
1028            }
1029
1030            impl Add for $unit {
1031                type Output = Self;
1032
1033                fn add(self, rhs: Self) -> Self::Output {
1034                    Self(self.0 + rhs.0)
1035                }
1036            }
1037
1038            impl AddAssign for $unit {
1039                fn add_assign(&mut self, rhs: Self) {
1040                    self.0 = self.0 + rhs.0;
1041                }
1042            }
1043
1044            impl Sub for $unit {
1045                type Output = Self;
1046
1047                fn sub(self, rhs: Self) -> Self::Output {
1048                    Self(self.0 - rhs.0)
1049                }
1050            }
1051
1052            impl SubAssign for $unit {
1053                fn sub_assign(&mut self, rhs: Self) {
1054                    self.0 = self.0 - rhs.0
1055                }
1056            }
1057
1058            impl Mul<$unit> for f64 {
1059                type Output = $unit;
1060
1061                fn mul(self, rhs: $unit) -> Self::Output {
1062                    $unit(self * rhs.0)
1063                }
1064            }
1065
1066            impl From<$unit> for f64 {
1067                fn from(val: $unit) -> Self {
1068                    val.0
1069                }
1070            }
1071        )*
1072    };
1073}
1074
1075trait_impls!(
1076    Angle,
1077    AngularRate,
1078    DataRate,
1079    Decibel,
1080    Distance,
1081    Frequency,
1082    Power,
1083    Temperature,
1084    Velocity
1085);
1086
1087#[cfg(test)]
1088mod tests {
1089    use alloc::format;
1090    use std::f64::consts::{FRAC_PI_2, PI};
1091
1092    use lox_test_utils::assert_approx_eq;
1093    use rstest::rstest;
1094
1095    extern crate alloc;
1096
1097    use super::*;
1098
1099    #[test]
1100    fn test_angle_deg() {
1101        let angle = 90.0.deg();
1102        assert_approx_eq!(angle.0, FRAC_PI_2, rtol <= 1e-10);
1103    }
1104
1105    #[test]
1106    fn test_angle_rad() {
1107        let angle = PI.rad();
1108        assert_approx_eq!(angle.0, PI, rtol <= 1e-10);
1109    }
1110
1111    #[test]
1112    fn test_angle_conversions() {
1113        let angle_deg = 180.0.deg();
1114        let angle_rad = PI.rad();
1115        assert_approx_eq!(angle_deg.0, angle_rad.0, rtol <= 1e-10);
1116    }
1117
1118    #[test]
1119    fn test_angle_display() {
1120        let angle = 90.123456.deg();
1121        assert_eq!(format!("{:.2}", angle), "90.12 deg")
1122    }
1123
1124    #[test]
1125    fn test_angle_neg() {
1126        assert_eq!(Angle(-1.0), -1.0.rad())
1127    }
1128
1129    const TOLERANCE: f64 = f64::EPSILON;
1130
1131    #[rstest]
1132    // Center 0.0 – expected range [-π, π).
1133    #[case(Angle::ZERO, Angle::ZERO, 0.0)]
1134    #[case(Angle::PI, Angle::ZERO, -PI)]
1135    #[case(-Angle::PI, Angle::ZERO, -PI)]
1136    #[case(Angle::TAU, Angle::ZERO, 0.0)]
1137    #[case(Angle::FRAC_PI_2, Angle::ZERO, FRAC_PI_2)]
1138    #[case(-Angle::FRAC_PI_2, Angle::ZERO, -FRAC_PI_2)]
1139    // Center π – expected range [0, 2π).
1140    #[case(Angle::ZERO, Angle::PI, 0.0)]
1141    #[case(Angle::PI, Angle::PI, PI)]
1142    #[case(-Angle::PI, Angle::PI, PI)]
1143    #[case(Angle::TAU, Angle::PI, 0.0)]
1144    #[case(Angle::FRAC_PI_2, Angle::PI, FRAC_PI_2)]
1145    #[case(-Angle::FRAC_PI_2, Angle::PI, 3.0 * PI / 2.0)]
1146    // Center -π – expected range [-2π, 0).
1147    #[case(Angle::ZERO, -Angle::PI, -TAU)]
1148    #[case(Angle::PI, -Angle::PI, -PI)]
1149    #[case(-Angle::PI, -Angle::PI, -PI)]
1150    #[case(Angle::TAU, -Angle::PI, -TAU)]
1151    #[case(Angle::FRAC_PI_2, -Angle::PI, -3.0 * PI / 2.0)]
1152    #[case(-Angle::FRAC_PI_2, -Angle::PI, -FRAC_PI_2)]
1153    fn test_angle_normalize_two_pi(#[case] angle: Angle, #[case] center: Angle, #[case] exp: f64) {
1154        // atol is preferred to rtol for floating-point comparisons with 0.0. See
1155        // https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/#inferna
1156        if exp == 0.0 {
1157            assert_approx_eq!(angle.normalize_two_pi(center).0, exp, atol <= TOLERANCE);
1158        } else {
1159            assert_approx_eq!(angle.normalize_two_pi(center).0, exp, rtol <= TOLERANCE);
1160        }
1161    }
1162
1163    #[test]
1164    fn test_distance_m() {
1165        let distance = 1000.0.m();
1166        assert_eq!(distance.0, 1000.0);
1167    }
1168
1169    #[test]
1170    fn test_distance_km() {
1171        let distance = 1.0.km();
1172        assert_eq!(distance.0, 1000.0);
1173    }
1174
1175    #[test]
1176    fn test_distance_au() {
1177        let distance = 1.0.au();
1178        assert_eq!(distance.0, ASTRONOMICAL_UNIT);
1179    }
1180
1181    #[test]
1182    fn test_distance_conversions() {
1183        let d1 = 1.5e11.m();
1184        let d2 = (1.5e11 / ASTRONOMICAL_UNIT).au();
1185        assert_approx_eq!(d1.0, d2.0, rtol <= 1e-9);
1186    }
1187
1188    #[test]
1189    fn test_distance_display() {
1190        let distance = 9.123456.km();
1191        assert_eq!(format!("{:.2}", distance), "9.12 km")
1192    }
1193
1194    #[test]
1195    fn test_distance_neg() {
1196        assert_eq!(Distance(-1.0), -1.0.m())
1197    }
1198
1199    #[test]
1200    fn test_velocity_mps() {
1201        let velocity = 1000.0.mps();
1202        assert_eq!(velocity.0, 1000.0);
1203    }
1204
1205    #[test]
1206    fn test_velocity_kps() {
1207        let velocity = 1.0.kps();
1208        assert_eq!(velocity.0, 1000.0);
1209    }
1210
1211    #[test]
1212    fn test_velocity_conversions() {
1213        let v1 = 7500.0.mps();
1214        let v2 = 7.5.kps();
1215        assert_eq!(v1.0, v2.0);
1216    }
1217
1218    #[test]
1219    fn test_velocity_display() {
1220        let velocity = 9.123456.kps();
1221        assert_eq!(format!("{:.2}", velocity), "9.12 km/s")
1222    }
1223
1224    #[test]
1225    fn test_velocity_neg() {
1226        assert_eq!(Velocity(-1.0), -1.0.mps())
1227    }
1228
1229    #[test]
1230    fn test_frequency_hz() {
1231        let frequency = 1000.0.hz();
1232        assert_eq!(frequency.0, 1000.0);
1233    }
1234
1235    #[test]
1236    fn test_frequency_khz() {
1237        let frequency = 1.0.khz();
1238        assert_eq!(frequency.0, 1000.0);
1239    }
1240
1241    #[test]
1242    fn test_frequency_mhz() {
1243        let frequency = 1.0.mhz();
1244        assert_eq!(frequency.0, 1_000_000.0);
1245    }
1246
1247    #[test]
1248    fn test_frequency_ghz() {
1249        let frequency = 1.0.ghz();
1250        assert_eq!(frequency.0, 1_000_000_000.0);
1251    }
1252
1253    #[test]
1254    fn test_frequency_thz() {
1255        let frequency = 1.0.thz();
1256        assert_eq!(frequency.0, 1_000_000_000_000.0);
1257    }
1258
1259    #[test]
1260    fn test_frequency_conversions() {
1261        let f1 = 2.4.ghz();
1262        let f2 = 2400.0.mhz();
1263        assert_eq!(f1.0, f2.0);
1264    }
1265
1266    #[test]
1267    fn test_frequency_wavelength() {
1268        let f = 1.0.ghz();
1269        let wavelength = f.wavelength();
1270        assert_approx_eq!(wavelength.0, 0.299792458, rtol <= 1e-9);
1271    }
1272
1273    #[test]
1274    fn test_frequency_wavelength_speed_of_light() {
1275        let f = 299792458.0.hz(); // 1 Hz at speed of light
1276        let wavelength = f.wavelength();
1277        assert_approx_eq!(wavelength.0, 1.0, rtol <= 1e-10);
1278    }
1279
1280    #[test]
1281    fn test_frequency_display() {
1282        let frequency = 2.4123456.ghz();
1283        assert_eq!(format!("{:.2}", frequency), "2.41 GHz");
1284    }
1285
1286    #[rstest]
1287    #[case(0.0.hz(), None)]
1288    #[case(3.0.mhz(), Some(FrequencyBand::HF))]
1289    #[case(30.0.mhz(), Some(FrequencyBand::VHF))]
1290    #[case(300.0.mhz(), Some(FrequencyBand::UHF))]
1291    #[case(1.0.ghz(), Some(FrequencyBand::L))]
1292    #[case(2.0.ghz(), Some(FrequencyBand::S))]
1293    #[case(4.0.ghz(), Some(FrequencyBand::C))]
1294    #[case(8.0.ghz(), Some(FrequencyBand::X))]
1295    #[case(12.0.ghz(), Some(FrequencyBand::Ku))]
1296    #[case(18.0.ghz(), Some(FrequencyBand::K))]
1297    #[case(27.0.ghz(), Some(FrequencyBand::Ka))]
1298    #[case(40.0.ghz(), Some(FrequencyBand::V))]
1299    #[case(75.0.ghz(), Some(FrequencyBand::W))]
1300    #[case(110.0.ghz(), Some(FrequencyBand::G))]
1301    #[case(1.0.thz(), None)]
1302    fn test_frequency_band(#[case] f: Frequency, #[case] exp: Option<FrequencyBand>) {
1303        assert_eq!(f.band(), exp)
1304    }
1305
1306    #[test]
1307    fn test_decibel_db() {
1308        let d = 3.0.db();
1309        assert_eq!(d.as_f64(), 3.0);
1310    }
1311
1312    #[test]
1313    fn test_decibel_from_linear() {
1314        let d = Decibel::from_linear(100.0);
1315        assert_approx_eq!(d.0, 20.0, rtol <= 1e-10);
1316    }
1317
1318    #[test]
1319    fn test_decibel_to_linear() {
1320        let d = Decibel::new(20.0);
1321        assert_approx_eq!(d.to_linear(), 100.0, rtol <= 1e-10);
1322    }
1323
1324    #[test]
1325    fn test_decibel_roundtrip() {
1326        let val = 42.5;
1327        let d = Decibel::new(val);
1328        let roundtripped = Decibel::from_linear(d.to_linear());
1329        assert_approx_eq!(roundtripped.0, val, rtol <= 1e-10);
1330    }
1331
1332    #[test]
1333    fn test_decibel_add() {
1334        let sum = 3.0.db() + 3.0.db();
1335        assert_approx_eq!(sum.0, 6.0, rtol <= 1e-10);
1336    }
1337
1338    #[test]
1339    fn test_decibel_sub() {
1340        let diff = 6.0.db() - 3.0.db();
1341        assert_approx_eq!(diff.0, 3.0, rtol <= 1e-10);
1342    }
1343
1344    #[test]
1345    fn test_decibel_neg() {
1346        assert_eq!(-3.0.db(), Decibel::new(-3.0));
1347    }
1348
1349    #[test]
1350    fn test_decibel_display() {
1351        let d = 3.0.db();
1352        assert_eq!(format!("{:.1}", d), "3.0 dB");
1353    }
1354
1355    // --- Temperature ---
1356
1357    #[test]
1358    fn test_temperature_new() {
1359        let t = Temperature::new(290.0);
1360        assert_eq!(t.as_f64(), 290.0);
1361    }
1362
1363    #[test]
1364    fn test_temperature_kelvin() {
1365        let t = Temperature::kelvin(300.0);
1366        assert_eq!(t.to_kelvin(), 300.0);
1367    }
1368
1369    #[test]
1370    fn test_temperature_display() {
1371        let t = Temperature::new(290.0);
1372        assert_eq!(format!("{}", t), "290 K");
1373    }
1374
1375    #[test]
1376    fn test_temperature_arithmetic() {
1377        let a = Temperature::new(100.0);
1378        let b = Temperature::new(200.0);
1379        assert_eq!((a + b).as_f64(), 300.0);
1380        assert_eq!((b - a).as_f64(), 100.0);
1381        assert_eq!((-a).as_f64(), -100.0);
1382        assert_eq!((2.0 * a).as_f64(), 200.0);
1383    }
1384
1385    // --- Power ---
1386
1387    #[test]
1388    fn test_power_watts() {
1389        let p = Power::watts(100.0);
1390        assert_eq!(p.to_watts(), 100.0);
1391    }
1392
1393    #[test]
1394    fn test_power_kilowatts() {
1395        let p = Power::kilowatts(1.0);
1396        assert_eq!(p.to_watts(), 1000.0);
1397        assert_eq!(p.to_kilowatts(), 1.0);
1398    }
1399
1400    #[test]
1401    fn test_power_dbw() {
1402        let p = Power::watts(100.0);
1403        assert_approx_eq!(p.to_dbw(), 20.0, rtol <= 1e-10);
1404    }
1405
1406    #[test]
1407    fn test_power_display() {
1408        let p = Power::watts(100.0);
1409        assert_eq!(format!("{}", p), "100 W");
1410    }
1411
1412    #[test]
1413    fn test_power_arithmetic() {
1414        let a = Power::watts(50.0);
1415        let b = Power::watts(150.0);
1416        assert_eq!((a + b).as_f64(), 200.0);
1417        assert_eq!((b - a).as_f64(), 100.0);
1418        assert_eq!((-a).as_f64(), -50.0);
1419    }
1420
1421    // --- DataRate ---
1422
1423    #[test]
1424    fn test_data_rate_bps() {
1425        let dr = DataRate::bits_per_second(1000.0);
1426        assert_eq!(dr.to_bits_per_second(), 1000.0);
1427        assert_eq!(dr.to_kilobits_per_second(), 1.0);
1428        assert_eq!(dr.to_megabits_per_second(), 0.001);
1429    }
1430
1431    #[test]
1432    fn test_data_rate_kbps() {
1433        let dr = DataRate::kilobits_per_second(1.0);
1434        assert_eq!(dr.to_bits_per_second(), 1000.0);
1435    }
1436
1437    #[test]
1438    fn test_data_rate_mbps() {
1439        let dr = DataRate::megabits_per_second(1.0);
1440        assert_eq!(dr.to_bits_per_second(), 1_000_000.0);
1441    }
1442
1443    #[test]
1444    fn test_data_rate_display() {
1445        let dr = DataRate::kilobits_per_second(1.0);
1446        assert_eq!(format!("{}", dr), "1 kbps");
1447    }
1448
1449    #[test]
1450    fn test_data_rate_arithmetic() {
1451        let a = DataRate::new(100.0);
1452        let b = DataRate::new(200.0);
1453        assert_eq!((a + b).as_f64(), 300.0);
1454        assert_eq!((b - a).as_f64(), 100.0);
1455        assert_eq!((-a).as_f64(), -100.0);
1456    }
1457
1458    // --- AngularRate ---
1459
1460    #[test]
1461    fn test_angular_rate_rps() {
1462        let ar = AngularRate::radians_per_second(1.0);
1463        assert_eq!(ar.to_radians_per_second(), 1.0);
1464        assert_approx_eq!(ar.to_degrees_per_second(), 57.29577951308232, rtol <= 1e-10);
1465    }
1466
1467    #[test]
1468    fn test_angular_rate_dps() {
1469        let ar = AngularRate::degrees_per_second(180.0);
1470        assert_approx_eq!(
1471            ar.to_radians_per_second(),
1472            core::f64::consts::PI,
1473            rtol <= 1e-10
1474        );
1475    }
1476
1477    #[test]
1478    fn test_angular_rate_display() {
1479        let ar = AngularRate::radians_per_second(1.0);
1480        let s = format!("{}", ar);
1481        assert!(s.contains("deg/s"));
1482    }
1483
1484    #[test]
1485    fn test_angular_rate_arithmetic() {
1486        let a = AngularRate::new(1.0);
1487        let b = AngularRate::new(2.0);
1488        assert_eq!((a + b).as_f64(), 3.0);
1489        assert_eq!((b - a).as_f64(), 1.0);
1490        assert_eq!((-a).as_f64(), -1.0);
1491        assert_eq!((3.0 * a).as_f64(), 3.0);
1492    }
1493}