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