Skip to main content

lox_frames/
iers.rs

1// SPDX-FileCopyrightText: 2025 Helge Eichhorn <git@helgeeichhorn.de>
2//
3// SPDX-License-Identifier: MPL-2.0
4
5use std::{
6    fmt::Display,
7    ops::{Add, AddAssign},
8};
9
10use glam::{DMat3, DVec3};
11use lox_units::Angle;
12
13use crate::iers::{cip::CipCoords, ecliptic::MeanObliquity, nutation::Nutation};
14
15/// Celestial Intermediate Origin locator.
16pub mod cio;
17/// Celestial Intermediate Pole coordinates.
18pub mod cip;
19/// Earth rotation angle and equation of the equinoxes.
20pub mod earth_rotation;
21/// Obliquity of the ecliptic.
22pub mod ecliptic;
23/// Fundamental (Delaunay) arguments.
24pub mod fundamental;
25/// Nutation models.
26pub mod nutation;
27/// Polar motion coordinates and matrices.
28pub mod polar_motion;
29/// Precession matrices and frame bias.
30pub mod precession;
31/// Terrestrial Intermediate Origin locator.
32pub mod tio;
33
34mod sealed {
35    pub trait Sealed {}
36    impl Sealed for super::Iers1996 {}
37    impl Sealed for super::Iers2003 {}
38    impl Sealed for super::Iers2010 {}
39    impl Sealed for super::ReferenceSystem {}
40}
41
42/// Sealed trait for IERS convention systems.
43pub trait IersSystem: sealed::Sealed {
44    /// Returns the numeric identifier for this convention.
45    fn id(&self) -> usize;
46    /// Returns the convention name (e.g. "IERS1996").
47    fn name(&self) -> String;
48}
49
50/// IERS 1996 conventions.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
53pub struct Iers1996;
54
55impl IersSystem for Iers1996 {
56    fn id(&self) -> usize {
57        0
58    }
59
60    fn name(&self) -> String {
61        "IERS1996".to_owned()
62    }
63}
64
65impl Display for Iers1996 {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        self.name().fmt(f)
68    }
69}
70
71/// IAU 2000 nutation model variant.
72#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
73#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
74pub enum Iau2000Model {
75    /// Full IAU 2000A model.
76    #[default]
77    A = 1,
78    /// Truncated IAU 2000B model.
79    B = 2,
80}
81
82impl Display for Iau2000Model {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        match self {
85            Iau2000Model::A => "IAU2000A".fmt(f),
86            Iau2000Model::B => "IAU2000B".fmt(f),
87        }
88    }
89}
90
91/// IERS 2003 conventions, parameterised by IAU 2000 nutation model.
92#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
93#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
94pub struct Iers2003(pub Iau2000Model);
95
96impl IersSystem for Iers2003 {
97    fn id(&self) -> usize {
98        self.0 as usize
99    }
100
101    fn name(&self) -> String {
102        "IERS2003".to_owned()
103    }
104}
105
106impl Display for Iers2003 {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        self.name().fmt(f)
109    }
110}
111
112/// IERS 2010 conventions.
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
115pub struct Iers2010;
116
117impl IersSystem for Iers2010 {
118    fn id(&self) -> usize {
119        3
120    }
121
122    fn name(&self) -> String {
123        "IERS2010".to_owned()
124    }
125}
126
127impl Display for Iers2010 {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        self.name().fmt(f)
130    }
131}
132
133/// Dynamic dispatch enum for IERS convention systems.
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
136pub enum ReferenceSystem {
137    /// IERS 1996 conventions.
138    Iers1996,
139    /// IERS 2003 conventions with the given IAU 2000 model.
140    Iers2003(Iau2000Model),
141    /// IERS 2010 conventions.
142    Iers2010,
143}
144
145impl IersSystem for ReferenceSystem {
146    fn id(&self) -> usize {
147        match self {
148            ReferenceSystem::Iers1996 => Iers1996.id(),
149            ReferenceSystem::Iers2003(iau2000) => Iers2003(*iau2000).id(),
150            ReferenceSystem::Iers2010 => Iers2010.id(),
151        }
152    }
153
154    fn name(&self) -> String {
155        match self {
156            ReferenceSystem::Iers1996 => Iers1996.to_string(),
157            ReferenceSystem::Iers2003(model) => Iers2003(*model).to_string(),
158            ReferenceSystem::Iers2010 => Iers2010.to_string(),
159        }
160    }
161}
162
163impl Display for ReferenceSystem {
164    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165        self.name().fmt(f)
166    }
167}
168
169impl From<Iers1996> for ReferenceSystem {
170    fn from(_: Iers1996) -> Self {
171        ReferenceSystem::Iers1996
172    }
173}
174
175impl From<Iers2003> for ReferenceSystem {
176    fn from(sys: Iers2003) -> Self {
177        ReferenceSystem::Iers2003(sys.0)
178    }
179}
180
181impl From<Iers2010> for ReferenceSystem {
182    fn from(_: Iers2010) -> Self {
183        ReferenceSystem::Iers2010
184    }
185}
186
187/// Earth orientation parameter corrections (δψ/δX, δε/δY).
188#[derive(Debug, Clone, Copy, Default)]
189#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
190pub struct Corrections(pub Angle, pub Angle);
191
192impl Corrections {
193    /// Returns `true` if both correction angles are zero.
194    pub fn is_zero(&self) -> bool {
195        self.0.is_zero() && self.1.is_zero()
196    }
197}
198
199impl Add<Corrections> for Nutation {
200    type Output = Self;
201
202    fn add(self, rhs: Corrections) -> Self::Output {
203        Self {
204            dpsi: self.dpsi + rhs.0,
205            deps: self.deps + rhs.1,
206        }
207    }
208}
209
210impl AddAssign<Corrections> for Nutation {
211    fn add_assign(&mut self, rhs: Corrections) {
212        self.dpsi += rhs.0;
213        self.deps += rhs.1;
214    }
215}
216
217impl Add<Corrections> for CipCoords {
218    type Output = Self;
219
220    fn add(self, rhs: Corrections) -> Self::Output {
221        Self {
222            x: self.x + rhs.0,
223            y: self.y + rhs.1,
224        }
225    }
226}
227
228impl AddAssign<Corrections> for CipCoords {
229    fn add_assign(&mut self, rhs: Corrections) {
230        self.x += rhs.0;
231        self.y += rhs.1;
232    }
233}
234
235impl ReferenceSystem {
236    /// Converts EOP corrections between ecliptic and equatorial representations.
237    pub fn ecliptic_corrections(
238        &self,
239        corr: Corrections,
240        nut: Nutation,
241        epsa: MeanObliquity,
242        rpb: DMat3,
243    ) -> Corrections {
244        match self {
245            ReferenceSystem::Iers1996 => corr,
246            ReferenceSystem::Iers2003(_) | ReferenceSystem::Iers2010 => {
247                let Corrections(dx, dy) = corr;
248                let rbpn = nut.nutation_matrix(epsa) * rpb;
249                let v1 = DVec3::new(dx.as_f64(), dy.as_f64(), 0.0);
250                let v2 = rbpn * v1;
251                Corrections(Angle::new(v2.x / epsa.0.sin()), Angle::new(v2.y))
252            }
253        }
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use rstest::rstest;
260
261    use super::*;
262
263    #[rstest]
264    #[case(Iers1996, 0)]
265    #[case(Iers2003(Iau2000Model::A), 1)]
266    #[case(Iers2003(Iau2000Model::B), 2)]
267    #[case(Iers2010, 3)]
268    #[case(ReferenceSystem::Iers1996, 0)]
269    #[case(ReferenceSystem::Iers2003(Iau2000Model::A), 1)]
270    #[case(ReferenceSystem::Iers2003(Iau2000Model::B), 2)]
271    #[case(ReferenceSystem::Iers2010, 3)]
272    fn test_iers_convention_id<T: IersSystem>(#[case] iers: T, #[case] exp: usize) {
273        let act = iers.id();
274        assert_eq!(act, exp);
275    }
276}