Skip to main content

lox_frames/
dynamic.rs

1// SPDX-FileCopyrightText: 2024 Helge Eichhorn <git@helgeeichhorn.de>
2//
3// SPDX-License-Identifier: MPL-2.0
4
5use std::str::FromStr;
6
7use lox_bodies::{DynOrigin, Origin, TryRotationalElements};
8use thiserror::Error;
9
10use crate::{
11    frames::{Cirf, Iau, Icrf, Itrf, J2000, Mod, Pef, Teme, Tirf, Tod},
12    iers::{Iau2000Model, IersSystem, ReferenceSystem},
13    traits::{
14        NonBodyFixedFrameError, NonQuasiInertialFrameError, ReferenceFrame, TryBodyFixed,
15        TryQuasiInertial, frame_id,
16    },
17};
18
19/// Enum representation of all known reference frames, for dynamic dispatch.
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub enum DynFrame {
23    /// International Celestial Reference Frame.
24    #[default]
25    Icrf,
26    /// J2000 Mean Equator and Equinox.
27    J2000,
28    /// Celestial Intermediate Reference Frame.
29    Cirf,
30    /// Terrestrial Intermediate Reference Frame.
31    Tirf,
32    /// International Terrestrial Reference Frame.
33    Itrf,
34    /// IAU body-fixed frame for the given origin.
35    Iau(DynOrigin),
36    /// Mean of Date frame for the given IERS convention.
37    Mod(ReferenceSystem),
38    /// True of Date frame for the given IERS convention.
39    Tod(ReferenceSystem),
40    /// Pseudo-Earth Fixed frame for the given IERS convention.
41    Pef(ReferenceSystem),
42    /// True Equator Mean Equinox.
43    Teme,
44}
45
46impl ReferenceFrame for DynFrame {
47    fn name(&self) -> String {
48        match self {
49            DynFrame::Icrf => Icrf.name(),
50            DynFrame::J2000 => J2000.name(),
51            DynFrame::Cirf => Cirf.name(),
52            DynFrame::Tirf => Tirf.name(),
53            DynFrame::Itrf => Itrf.name(),
54            DynFrame::Iau(dyn_origin) => {
55                let body = dyn_origin.name();
56                match body {
57                    "Sun" | "Moon" => format!("IAU Body-Fixed Reference Frame for the {body}"),
58                    _ => format!("IAU Body-Fixed Reference Frame for {body}"),
59                }
60            }
61            DynFrame::Mod(sys) => Mod(*sys).name(),
62            DynFrame::Tod(sys) => Tod(*sys).name(),
63            DynFrame::Pef(sys) => Pef(*sys).name(),
64            DynFrame::Teme => Teme.name(),
65        }
66    }
67
68    fn abbreviation(&self) -> String {
69        match self {
70            DynFrame::Icrf => Icrf.abbreviation(),
71            DynFrame::J2000 => J2000.abbreviation(),
72            DynFrame::Cirf => Cirf.abbreviation(),
73            DynFrame::Tirf => Tirf.abbreviation(),
74            DynFrame::Itrf => Itrf.abbreviation(),
75            DynFrame::Iau(dyn_origin) => {
76                let body = dyn_origin.name().replace([' ', '-'], "_").to_uppercase();
77                format!("IAU_{body}")
78            }
79            DynFrame::Mod(sys) => Mod(*sys).abbreviation(),
80            DynFrame::Tod(sys) => Tod(*sys).abbreviation(),
81            DynFrame::Pef(sys) => Pef(*sys).abbreviation(),
82            DynFrame::Teme => Teme.abbreviation(),
83        }
84    }
85
86    fn frame_id(&self, _: crate::traits::private::Internal) -> Option<usize> {
87        match self {
88            DynFrame::Icrf => frame_id(&Icrf),
89            DynFrame::J2000 => frame_id(&J2000),
90            DynFrame::Cirf => frame_id(&Cirf),
91            DynFrame::Tirf => frame_id(&Tirf),
92            DynFrame::Itrf => frame_id(&Itrf),
93            DynFrame::Iau(dyn_origin) => Some(1000 + dyn_origin.id().0 as usize),
94            DynFrame::Mod(sys) => frame_id(&Mod(*sys)),
95            DynFrame::Tod(sys) => frame_id(&Tod(*sys)),
96
97            DynFrame::Pef(sys) => frame_id(&Pef(*sys)),
98            DynFrame::Teme => frame_id(&Teme),
99        }
100    }
101}
102
103impl TryQuasiInertial for DynFrame {
104    fn try_quasi_inertial(&self) -> Result<(), NonQuasiInertialFrameError> {
105        match self {
106            DynFrame::Icrf
107            | DynFrame::J2000
108            | DynFrame::Cirf
109            | DynFrame::Mod(_)
110            | DynFrame::Tod(_) => Ok(()),
111            _ => Err(NonQuasiInertialFrameError(self.abbreviation())),
112        }
113    }
114}
115
116impl TryBodyFixed for DynFrame {
117    fn try_body_fixed(&self) -> Result<(), NonBodyFixedFrameError> {
118        match self {
119            DynFrame::Iau(_) | DynFrame::Itrf | DynFrame::Tirf | DynFrame::Pef(_) => Ok(()),
120            _ => Err(NonBodyFixedFrameError(self.abbreviation())),
121        }
122    }
123}
124
125// Simple frame conversions.
126
127impl From<Icrf> for DynFrame {
128    fn from(_: Icrf) -> Self {
129        DynFrame::Icrf
130    }
131}
132
133impl From<J2000> for DynFrame {
134    fn from(_: J2000) -> Self {
135        DynFrame::J2000
136    }
137}
138
139impl From<Cirf> for DynFrame {
140    fn from(_: Cirf) -> Self {
141        DynFrame::Cirf
142    }
143}
144
145impl From<Tirf> for DynFrame {
146    fn from(_: Tirf) -> Self {
147        DynFrame::Tirf
148    }
149}
150
151impl From<Itrf> for DynFrame {
152    fn from(_: Itrf) -> Self {
153        DynFrame::Itrf
154    }
155}
156
157impl From<Teme> for DynFrame {
158    fn from(_: Teme) -> Self {
159        DynFrame::Teme
160    }
161}
162
163// Parameterized equinox-based frames.
164
165impl<T: IersSystem + Into<ReferenceSystem>> From<Mod<T>> for DynFrame {
166    fn from(frame: Mod<T>) -> Self {
167        DynFrame::Mod(frame.0.into())
168    }
169}
170
171impl<T: IersSystem + Into<ReferenceSystem>> From<Tod<T>> for DynFrame {
172    fn from(frame: Tod<T>) -> Self {
173        DynFrame::Tod(frame.0.into())
174    }
175}
176
177impl<T: IersSystem + Into<ReferenceSystem>> From<Pef<T>> for DynFrame {
178    fn from(frame: Pef<T>) -> Self {
179        DynFrame::Pef(frame.0.into())
180    }
181}
182
183// IAU body-fixed frames.
184
185impl<T: TryRotationalElements + Copy + Into<DynOrigin>> From<Iau<T>> for DynFrame {
186    fn from(frame: Iau<T>) -> Self {
187        DynFrame::Iau(frame.body().into())
188    }
189}
190
191fn parse_iau_frame(s: &str) -> Option<DynFrame> {
192    let (prefix, origin) = s.split_once("_")?;
193    if prefix.to_lowercase() != "iau" {
194        return None;
195    }
196    let origin: DynOrigin = origin.to_lowercase().parse().ok()?;
197    let _ = origin.try_rotational_elements(0.0).ok()?;
198    Some(DynFrame::Iau(origin))
199}
200
201fn parse_reference_system(s: &str) -> Option<ReferenceSystem> {
202    match s.to_uppercase().as_str() {
203        "IERS1996" => Some(ReferenceSystem::Iers1996),
204        "IERS2003" => Some(ReferenceSystem::Iers2003(Iau2000Model::A)),
205        "IERS2010" => Some(ReferenceSystem::Iers2010),
206        _ => None,
207    }
208}
209
210/// Parse frames in `FRAME(SYSTEM)` format, e.g. `MOD(IERS2003)`.
211fn parse_equinox_frame(s: &str) -> Option<DynFrame> {
212    let s_stripped = s.strip_suffix(')')?;
213    let (frame, system) = s_stripped.split_once('(')?;
214    let sys = parse_reference_system(system)?;
215    match frame.to_uppercase().as_str() {
216        "MOD" => Some(DynFrame::Mod(sys)),
217        "TOD" => Some(DynFrame::Tod(sys)),
218        "PEF" => Some(DynFrame::Pef(sys)),
219        _ => None,
220    }
221}
222
223/// No frame matching the given name is known.
224#[derive(Clone, Debug, Error, PartialEq, Eq)]
225#[error("no frame with name '{0}' is known")]
226pub struct UnknownFrameError(String);
227
228impl FromStr for DynFrame {
229    type Err = UnknownFrameError;
230
231    fn from_str(s: &str) -> Result<Self, Self::Err> {
232        match s.to_uppercase().as_str() {
233            "ICRF" => Ok(DynFrame::Icrf),
234            "J2000" | "EME2000" => Ok(DynFrame::J2000),
235            "CIRF" => Ok(DynFrame::Cirf),
236            "TIRF" => Ok(DynFrame::Tirf),
237            "ITRF" => Ok(DynFrame::Itrf),
238            "TEME" => Ok(DynFrame::Teme),
239            "MOD" => Ok(DynFrame::Mod(ReferenceSystem::Iers1996)),
240            "TOD" => Ok(DynFrame::Tod(ReferenceSystem::Iers1996)),
241            "PEF" => Ok(DynFrame::Pef(ReferenceSystem::Iers1996)),
242            _ => {
243                if let Some(frame) = parse_equinox_frame(s) {
244                    Ok(frame)
245                } else if let Some(frame) = parse_iau_frame(s) {
246                    Ok(frame)
247                } else {
248                    Err(UnknownFrameError(s.to_owned()))
249                }
250            }
251        }
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    use crate::rotations::TryRotation;
260    use crate::{Iau, providers::DefaultRotationProvider};
261
262    use glam::DVec3;
263    use lox_bodies::{DynOrigin, Earth};
264    use lox_test_utils::assert_approx_eq;
265    use lox_time::utc::Utc;
266    use rstest::rstest;
267
268    #[rstest]
269    #[case::valid("IAU_EARTH", Some(DynFrame::Iau(DynOrigin::Earth)))]
270    #[case::invalid_prefix("FOO_EARTH", None)]
271    #[case::unkown_body("IAU_RUPERT", None)]
272    #[case::undefined_rotation("IAU_SYCORAX", None)]
273    fn test_parse_iau_frame(#[case] name: &str, #[case] exp: Option<DynFrame>) {
274        let act = parse_iau_frame(name);
275        assert_eq!(act, exp)
276    }
277
278    #[rstest]
279    #[case(
280        DynFrame::Iau(DynOrigin::Earth),
281        DVec3::new(
282            -5.740_259_426_667_957e3,
283            3.121_136_072_795_472_5e3,
284            -1.863_182_656_331_802_7e3,
285        ),
286        DVec3::new(
287            -3.532_378_757_836_52,
288            -3.152_377_656_863_808,
289            5.642_296_713_889_555,
290        ),
291    )]
292    #[case(
293        DynFrame::Iau(DynOrigin::Moon),
294        DVec3::new(
295            3.777_805_761_337_502e3,
296            -5.633_812_666_439_680_5e3,
297            -3.896_880_165_980_424e2,
298        ),
299        DVec3::new(
300            2.576_901_711_027_508_3,
301            1.250_106_874_006_032_4,
302            7.100_615_382_464_156,
303        ),
304    )]
305    fn test_icrf_to_bodyfixed(#[case] frame: DynFrame, #[case] r_exp: DVec3, #[case] v_exp: DVec3) {
306        let time = Utc::from_iso("2024-07-05T09:09:18.173")
307            .unwrap()
308            .to_dyn_time();
309        let r = DVec3::new(-5530.01774359, -3487.0895338, -1850.03476185);
310        let v = DVec3::new(1.29534407, -5.02456882, 5.6391936);
311        let rot = DefaultRotationProvider
312            .try_rotation(DynFrame::Icrf, frame, time)
313            .unwrap();
314        let (r_act, v_act) = rot.rotate_state(r, v);
315        assert_approx_eq!(r_act, r_exp, rtol <= 1e-8);
316        assert_approx_eq!(v_act, v_exp, rtol <= 1e-5);
317    }
318
319    #[rstest]
320    #[case("MOD", DynFrame::Mod(ReferenceSystem::Iers1996))]
321    #[case("mod", DynFrame::Mod(ReferenceSystem::Iers1996))]
322    #[case("TOD", DynFrame::Tod(ReferenceSystem::Iers1996))]
323    #[case("tod", DynFrame::Tod(ReferenceSystem::Iers1996))]
324    #[case("PEF", DynFrame::Pef(ReferenceSystem::Iers1996))]
325    #[case("pef", DynFrame::Pef(ReferenceSystem::Iers1996))]
326    #[case("MOD(IERS1996)", DynFrame::Mod(ReferenceSystem::Iers1996))]
327    #[case(
328        "MOD(IERS2003)",
329        DynFrame::Mod(ReferenceSystem::Iers2003(Iau2000Model::A))
330    )]
331    #[case(
332        "mod(iers2003)",
333        DynFrame::Mod(ReferenceSystem::Iers2003(Iau2000Model::A))
334    )]
335    #[case(
336        "TOD(IERS2003)",
337        DynFrame::Tod(ReferenceSystem::Iers2003(Iau2000Model::A))
338    )]
339    #[case(
340        "PEF(IERS2003)",
341        DynFrame::Pef(ReferenceSystem::Iers2003(Iau2000Model::A))
342    )]
343    #[case("MOD(IERS2010)", DynFrame::Mod(ReferenceSystem::Iers2010))]
344    #[case("TOD(IERS2010)", DynFrame::Tod(ReferenceSystem::Iers2010))]
345    #[case("PEF(IERS2010)", DynFrame::Pef(ReferenceSystem::Iers2010))]
346    fn test_parse_equinox_frames(#[case] name: &str, #[case] exp: DynFrame) {
347        let act: DynFrame = name.parse().unwrap();
348        assert_eq!(act, exp);
349    }
350
351    #[test]
352    fn test_frame_id() {
353        assert_eq!(frame_id(&Icrf), frame_id(&DynFrame::Icrf));
354        assert_eq!(frame_id(&J2000), frame_id(&DynFrame::J2000));
355        assert_eq!(frame_id(&Cirf), frame_id(&DynFrame::Cirf));
356        assert_eq!(frame_id(&Tirf), frame_id(&DynFrame::Tirf));
357        assert_eq!(frame_id(&Itrf), frame_id(&DynFrame::Itrf));
358        assert_eq!(
359            frame_id(&Iau::new(Earth)),
360            frame_id(&DynFrame::Iau(DynOrigin::Earth))
361        );
362    }
363
364    #[rstest]
365    #[case("J2000", DynFrame::J2000)]
366    #[case("j2000", DynFrame::J2000)]
367    #[case("EME2000", DynFrame::J2000)]
368    fn test_parse_j2000(#[case] name: &str, #[case] exp: DynFrame) {
369        let act: DynFrame = name.parse().unwrap();
370        assert_eq!(act, exp);
371    }
372
373    #[test]
374    fn test_j2000_quasi_inertial() {
375        assert!(DynFrame::J2000.try_quasi_inertial().is_ok());
376    }
377
378    #[test]
379    fn test_from_simple_frames() {
380        assert_eq!(DynFrame::from(Icrf), DynFrame::Icrf);
381        assert_eq!(DynFrame::from(J2000), DynFrame::J2000);
382        assert_eq!(DynFrame::from(Cirf), DynFrame::Cirf);
383        assert_eq!(DynFrame::from(Tirf), DynFrame::Tirf);
384        assert_eq!(DynFrame::from(Itrf), DynFrame::Itrf);
385        assert_eq!(DynFrame::from(Teme), DynFrame::Teme);
386    }
387
388    #[test]
389    fn test_from_parameterized_frames() {
390        use crate::iers::{Iers1996, Iers2003, Iers2010};
391
392        assert_eq!(
393            DynFrame::from(Mod(Iers1996)),
394            DynFrame::Mod(ReferenceSystem::Iers1996)
395        );
396        assert_eq!(
397            DynFrame::from(Tod(Iers2003::default())),
398            DynFrame::Tod(ReferenceSystem::Iers2003(Iau2000Model::A))
399        );
400        assert_eq!(
401            DynFrame::from(Pef(Iers2010)),
402            DynFrame::Pef(ReferenceSystem::Iers2010)
403        );
404    }
405
406    #[test]
407    fn test_from_iau_frame() {
408        assert_eq!(
409            DynFrame::from(Iau::new(Earth)),
410            DynFrame::Iau(DynOrigin::Earth)
411        );
412    }
413
414    #[rstest]
415    #[case(DynFrame::Icrf)]
416    #[case(DynFrame::J2000)]
417    #[case(DynFrame::Cirf)]
418    #[case(DynFrame::Tirf)]
419    #[case(DynFrame::Itrf)]
420    #[case(DynFrame::Teme)]
421    #[case(DynFrame::Mod(ReferenceSystem::Iers1996))]
422    #[case(DynFrame::Mod(ReferenceSystem::Iers2003(Iau2000Model::A)))]
423    #[case(DynFrame::Mod(ReferenceSystem::Iers2010))]
424    #[case(DynFrame::Tod(ReferenceSystem::Iers1996))]
425    #[case(DynFrame::Tod(ReferenceSystem::Iers2003(Iau2000Model::A)))]
426    #[case(DynFrame::Tod(ReferenceSystem::Iers2010))]
427    #[case(DynFrame::Pef(ReferenceSystem::Iers1996))]
428    #[case(DynFrame::Pef(ReferenceSystem::Iers2003(Iau2000Model::A)))]
429    #[case(DynFrame::Pef(ReferenceSystem::Iers2010))]
430    #[case(DynFrame::Iau(DynOrigin::Earth))]
431    fn test_abbreviation_round_trip(#[case] frame: DynFrame) {
432        let abbr = frame.abbreviation();
433        let parsed: DynFrame = abbr
434            .parse()
435            .unwrap_or_else(|e| panic!("failed to parse abbreviation '{}': {}", abbr, e));
436        assert_eq!(parsed, frame);
437    }
438}