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, Icrf, Itrf, J2000, Mod, Pef, Teme, Tirf, Tod},
12    iers::{Iau2000Model, 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
114fn parse_iau_frame(s: &str) -> Option<DynFrame> {
115    let (prefix, origin) = s.split_once("_")?;
116    if prefix.to_lowercase() != "iau" {
117        return None;
118    }
119    let origin: DynOrigin = origin.to_lowercase().parse().ok()?;
120    let _ = origin.try_rotational_elements(0.0).ok()?;
121    Some(DynFrame::Iau(origin))
122}
123
124fn parse_reference_system(s: &str) -> Option<ReferenceSystem> {
125    match s.to_uppercase().as_str() {
126        "IERS1996" => Some(ReferenceSystem::Iers1996),
127        "IERS2003" => Some(ReferenceSystem::Iers2003(Iau2000Model::A)),
128        "IERS2010" => Some(ReferenceSystem::Iers2010),
129        _ => None,
130    }
131}
132
133/// Parse frames in `FRAME(SYSTEM)` format, e.g. `MOD(IERS2003)`.
134fn parse_equinox_frame(s: &str) -> Option<DynFrame> {
135    let s_stripped = s.strip_suffix(')')?;
136    let (frame, system) = s_stripped.split_once('(')?;
137    let sys = parse_reference_system(system)?;
138    match frame.to_uppercase().as_str() {
139        "MOD" => Some(DynFrame::Mod(sys)),
140        "TOD" => Some(DynFrame::Tod(sys)),
141        "PEF" => Some(DynFrame::Pef(sys)),
142        _ => None,
143    }
144}
145
146#[derive(Clone, Debug, Error, PartialEq, Eq)]
147#[error("no frame with name '{0}' is known")]
148pub struct UnknownFrameError(String);
149
150impl FromStr for DynFrame {
151    type Err = UnknownFrameError;
152
153    fn from_str(s: &str) -> Result<Self, Self::Err> {
154        match s.to_uppercase().as_str() {
155            "ICRF" => Ok(DynFrame::Icrf),
156            "J2000" | "EME2000" => Ok(DynFrame::J2000),
157            "CIRF" => Ok(DynFrame::Cirf),
158            "TIRF" => Ok(DynFrame::Tirf),
159            "ITRF" => Ok(DynFrame::Itrf),
160            "TEME" => Ok(DynFrame::Teme),
161            "MOD" => Ok(DynFrame::Mod(ReferenceSystem::Iers1996)),
162            "TOD" => Ok(DynFrame::Tod(ReferenceSystem::Iers1996)),
163            "PEF" => Ok(DynFrame::Pef(ReferenceSystem::Iers1996)),
164            _ => {
165                if let Some(frame) = parse_equinox_frame(s) {
166                    Ok(frame)
167                } else if let Some(frame) = parse_iau_frame(s) {
168                    Ok(frame)
169                } else {
170                    Err(UnknownFrameError(s.to_owned()))
171                }
172            }
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    use crate::rotations::TryRotation;
182    use crate::{Iau, providers::DefaultRotationProvider};
183
184    use glam::DVec3;
185    use lox_bodies::{DynOrigin, Earth};
186    use lox_test_utils::assert_approx_eq;
187    use lox_time::utc::Utc;
188    use rstest::rstest;
189
190    #[rstest]
191    #[case::valid("IAU_EARTH", Some(DynFrame::Iau(DynOrigin::Earth)))]
192    #[case::invalid_prefix("FOO_EARTH", None)]
193    #[case::unkown_body("IAU_RUPERT", None)]
194    #[case::undefined_rotation("IAU_SYCORAX", None)]
195    fn test_parse_iau_frame(#[case] name: &str, #[case] exp: Option<DynFrame>) {
196        let act = parse_iau_frame(name);
197        assert_eq!(act, exp)
198    }
199
200    #[rstest]
201    #[case(
202        DynFrame::Iau(DynOrigin::Earth),
203        DVec3::new(
204            -5.740_259_426_667_957e3,
205            3.121_136_072_795_472_5e3,
206            -1.863_182_656_331_802_7e3,
207        ),
208        DVec3::new(
209            -3.532_378_757_836_52,
210            -3.152_377_656_863_808,
211            5.642_296_713_889_555,
212        ),
213    )]
214    #[case(
215        DynFrame::Iau(DynOrigin::Moon),
216        DVec3::new(
217            3.777_805_761_337_502e3,
218            -5.633_812_666_439_680_5e3,
219            -3.896_880_165_980_424e2,
220        ),
221        DVec3::new(
222            2.576_901_711_027_508_3,
223            1.250_106_874_006_032_4,
224            7.100_615_382_464_156,
225        ),
226    )]
227    fn test_icrf_to_bodyfixed(#[case] frame: DynFrame, #[case] r_exp: DVec3, #[case] v_exp: DVec3) {
228        let time = Utc::from_iso("2024-07-05T09:09:18.173")
229            .unwrap()
230            .to_dyn_time();
231        let r = DVec3::new(-5530.01774359, -3487.0895338, -1850.03476185);
232        let v = DVec3::new(1.29534407, -5.02456882, 5.6391936);
233        let rot = DefaultRotationProvider
234            .try_rotation(DynFrame::Icrf, frame, time)
235            .unwrap();
236        let (r_act, v_act) = rot.rotate_state(r, v);
237        assert_approx_eq!(r_act, r_exp, rtol <= 1e-8);
238        assert_approx_eq!(v_act, v_exp, rtol <= 1e-5);
239    }
240
241    #[rstest]
242    #[case("MOD", DynFrame::Mod(ReferenceSystem::Iers1996))]
243    #[case("mod", DynFrame::Mod(ReferenceSystem::Iers1996))]
244    #[case("TOD", DynFrame::Tod(ReferenceSystem::Iers1996))]
245    #[case("tod", DynFrame::Tod(ReferenceSystem::Iers1996))]
246    #[case("PEF", DynFrame::Pef(ReferenceSystem::Iers1996))]
247    #[case("pef", DynFrame::Pef(ReferenceSystem::Iers1996))]
248    #[case("MOD(IERS1996)", DynFrame::Mod(ReferenceSystem::Iers1996))]
249    #[case(
250        "MOD(IERS2003)",
251        DynFrame::Mod(ReferenceSystem::Iers2003(Iau2000Model::A))
252    )]
253    #[case(
254        "mod(iers2003)",
255        DynFrame::Mod(ReferenceSystem::Iers2003(Iau2000Model::A))
256    )]
257    #[case(
258        "TOD(IERS2003)",
259        DynFrame::Tod(ReferenceSystem::Iers2003(Iau2000Model::A))
260    )]
261    #[case(
262        "PEF(IERS2003)",
263        DynFrame::Pef(ReferenceSystem::Iers2003(Iau2000Model::A))
264    )]
265    #[case("MOD(IERS2010)", DynFrame::Mod(ReferenceSystem::Iers2010))]
266    #[case("TOD(IERS2010)", DynFrame::Tod(ReferenceSystem::Iers2010))]
267    #[case("PEF(IERS2010)", DynFrame::Pef(ReferenceSystem::Iers2010))]
268    fn test_parse_equinox_frames(#[case] name: &str, #[case] exp: DynFrame) {
269        let act: DynFrame = name.parse().unwrap();
270        assert_eq!(act, exp);
271    }
272
273    #[test]
274    fn test_frame_id() {
275        assert_eq!(frame_id(&Icrf), frame_id(&DynFrame::Icrf));
276        assert_eq!(frame_id(&J2000), frame_id(&DynFrame::J2000));
277        assert_eq!(frame_id(&Cirf), frame_id(&DynFrame::Cirf));
278        assert_eq!(frame_id(&Tirf), frame_id(&DynFrame::Tirf));
279        assert_eq!(frame_id(&Itrf), frame_id(&DynFrame::Itrf));
280        assert_eq!(
281            frame_id(&Iau::new(Earth)),
282            frame_id(&DynFrame::Iau(DynOrigin::Earth))
283        );
284    }
285
286    #[rstest]
287    #[case("J2000", DynFrame::J2000)]
288    #[case("j2000", DynFrame::J2000)]
289    #[case("EME2000", DynFrame::J2000)]
290    fn test_parse_j2000(#[case] name: &str, #[case] exp: DynFrame) {
291        let act: DynFrame = name.parse().unwrap();
292        assert_eq!(act, exp);
293    }
294
295    #[test]
296    fn test_j2000_quasi_inertial() {
297        assert!(DynFrame::J2000.try_quasi_inertial().is_ok());
298    }
299
300    #[rstest]
301    #[case(DynFrame::Icrf)]
302    #[case(DynFrame::J2000)]
303    #[case(DynFrame::Cirf)]
304    #[case(DynFrame::Tirf)]
305    #[case(DynFrame::Itrf)]
306    #[case(DynFrame::Teme)]
307    #[case(DynFrame::Mod(ReferenceSystem::Iers1996))]
308    #[case(DynFrame::Mod(ReferenceSystem::Iers2003(Iau2000Model::A)))]
309    #[case(DynFrame::Mod(ReferenceSystem::Iers2010))]
310    #[case(DynFrame::Tod(ReferenceSystem::Iers1996))]
311    #[case(DynFrame::Tod(ReferenceSystem::Iers2003(Iau2000Model::A)))]
312    #[case(DynFrame::Tod(ReferenceSystem::Iers2010))]
313    #[case(DynFrame::Pef(ReferenceSystem::Iers1996))]
314    #[case(DynFrame::Pef(ReferenceSystem::Iers2003(Iau2000Model::A)))]
315    #[case(DynFrame::Pef(ReferenceSystem::Iers2010))]
316    #[case(DynFrame::Iau(DynOrigin::Earth))]
317    fn test_abbreviation_round_trip(#[case] frame: DynFrame) {
318        let abbr = frame.abbreviation();
319        let parsed: DynFrame = abbr
320            .parse()
321            .unwrap_or_else(|e| panic!("failed to parse abbreviation '{}': {}", abbr, e));
322        assert_eq!(parsed, frame);
323    }
324}