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, UndefinedOriginPropertyError};
8use lox_time::{Time, time_scales::DynTimeScale};
9use thiserror::Error;
10
11use crate::{
12    Iau,
13    frames::{Cirf, Icrf, Itrf, Tirf},
14    providers::DefaultTransformProvider,
15    traits::{
16        NonBodyFixedFrameError, NonQuasiInertialFrameError, ReferenceFrame, TryBodyFixed,
17        TryQuasiInertial,
18    },
19    transformations::{Rotation, TryTransform},
20};
21
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
23pub enum DynFrame {
24    #[default]
25    Icrf,
26    Cirf,
27    Tirf,
28    Itrf,
29    Iau(DynOrigin),
30}
31
32impl ReferenceFrame for DynFrame {
33    fn name(&self) -> String {
34        match self {
35            DynFrame::Icrf => Icrf.name(),
36            DynFrame::Cirf => Cirf.name(),
37            DynFrame::Tirf => Tirf.name(),
38            DynFrame::Itrf => Itrf.name(),
39            DynFrame::Iau(dyn_origin) => {
40                let body = dyn_origin.name();
41                match body {
42                    "Sun" | "Moon" => format!("IAU Body-Fixed Reference Frame for the {body}"),
43                    _ => format!("IAU Body-Fixed Reference Frame for {body}"),
44                }
45            }
46        }
47    }
48
49    fn abbreviation(&self) -> String {
50        match self {
51            DynFrame::Icrf => Icrf.abbreviation(),
52            DynFrame::Cirf => Cirf.abbreviation(),
53            DynFrame::Tirf => Tirf.abbreviation(),
54            DynFrame::Itrf => Itrf.abbreviation(),
55            DynFrame::Iau(dyn_origin) => {
56                let body = dyn_origin.name().replace([' ', '-'], "_").to_uppercase();
57                format!("IAU_{body}")
58            }
59        }
60    }
61
62    fn is_rotating(&self) -> bool {
63        match self {
64            DynFrame::Icrf | DynFrame::Cirf => false,
65            DynFrame::Tirf | DynFrame::Itrf | DynFrame::Iau(_) => true,
66        }
67    }
68}
69
70impl TryQuasiInertial for DynFrame {
71    fn try_quasi_inertial(&self) -> Result<(), NonQuasiInertialFrameError> {
72        match self {
73            DynFrame::Icrf => Ok(()),
74            _ => Err(NonQuasiInertialFrameError(self.abbreviation())),
75        }
76    }
77}
78
79impl TryBodyFixed for DynFrame {
80    fn try_body_fixed(&self) -> Result<(), NonBodyFixedFrameError> {
81        match self {
82            DynFrame::Iau(_) | DynFrame::Itrf => Ok(()),
83            _ => Err(NonBodyFixedFrameError(self.abbreviation())),
84        }
85    }
86}
87
88fn parse_iau_frame(s: &str) -> Option<DynFrame> {
89    let (prefix, origin) = s.split_once("_")?;
90    if prefix.to_lowercase() != "iau" {
91        return None;
92    }
93    let origin: DynOrigin = origin.to_lowercase().parse().ok()?;
94    let _ = origin.try_rotational_elements(0.0).ok()?;
95    Some(DynFrame::Iau(origin))
96}
97
98#[derive(Clone, Debug, Error, PartialEq, Eq)]
99#[error("no frame with name '{0}' is known")]
100pub struct UnknownFrameError(String);
101
102impl FromStr for DynFrame {
103    type Err = UnknownFrameError;
104
105    fn from_str(s: &str) -> Result<Self, Self::Err> {
106        match s {
107            "icrf" | "ICRF" => Ok(DynFrame::Icrf),
108            "cirf" | "CIRF" => Ok(DynFrame::Cirf),
109            "tirf" | "TIRF" => Ok(DynFrame::Tirf),
110            "itrf" | "ITRF" => Ok(DynFrame::Itrf),
111            _ => {
112                if let Some(frame) = parse_iau_frame(s) {
113                    Ok(frame)
114                } else {
115                    Err(UnknownFrameError(s.to_owned()))
116                }
117            }
118        }
119    }
120}
121
122#[derive(Debug, Error)]
123pub enum DynTransformError {
124    #[error("transformations between {0} and {1} require an EOP provider")]
125    MissingEopProvider(String, String),
126    #[error(transparent)]
127    MissingUt1Provider(#[from] lox_time::offsets::MissingEopProviderError),
128    #[error(transparent)]
129    UndefinedRotationalElements(#[from] UndefinedOriginPropertyError),
130}
131
132impl TryTransform<DynFrame, DynFrame, DynTimeScale> for DefaultTransformProvider {
133    type Error = DynTransformError;
134
135    fn try_transform(
136        &self,
137        origin: DynFrame,
138        target: DynFrame,
139        time: Time<DynTimeScale>,
140    ) -> Result<Rotation, Self::Error> {
141        match (origin, target) {
142            (DynFrame::Icrf, DynFrame::Icrf) => Ok(Rotation::IDENTITY),
143            (DynFrame::Icrf, DynFrame::Iau(target)) => {
144                Ok(self.try_transform(Icrf, Iau::try_new(target)?, time)?)
145            }
146            (DynFrame::Cirf, DynFrame::Cirf) => Ok(Rotation::IDENTITY),
147            (DynFrame::Tirf, DynFrame::Tirf) => Ok(Rotation::IDENTITY),
148            (DynFrame::Iau(origin), DynFrame::Icrf) => {
149                Ok(self.try_transform(Iau::try_new(origin)?, Icrf, time)?)
150            }
151            (DynFrame::Iau(origin), DynFrame::Iau(target)) => {
152                let origin = Iau::try_new(origin)?;
153                let target = Iau::try_new(target)?;
154                Ok(self
155                    .try_transform(origin, Icrf, time)?
156                    .compose(self.try_transform(Icrf, target, time)?))
157            }
158            (origin, target) => Err(DynTransformError::MissingEopProvider(
159                origin.abbreviation(),
160                target.abbreviation(),
161            )),
162        }
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    use glam::DVec3;
171    use lox_bodies::DynOrigin;
172    use lox_test_utils::assert_approx_eq;
173    use lox_time::utc::Utc;
174    use rstest::rstest;
175
176    #[rstest]
177    #[case::valid("IAU_EARTH", Some(DynFrame::Iau(DynOrigin::Earth)))]
178    #[case::invalid_prefix("FOO_EARTH", None)]
179    #[case::unkown_body("IAU_RUPERT", None)]
180    #[case::undefined_rotation("IAU_SYCORAX", None)]
181    fn test_parse_iau_frame(#[case] name: &str, #[case] exp: Option<DynFrame>) {
182        let act = parse_iau_frame(name);
183        assert_eq!(act, exp)
184    }
185
186    #[rstest]
187    #[case(
188        DynFrame::Iau(DynOrigin::Earth),
189        DVec3::new(
190            -5.740_259_426_667_957e3,
191            3.121_136_072_795_472_5e3,
192            -1.863_182_656_331_802_7e3,
193        ),
194        DVec3::new(
195            -3.532_378_757_836_52,
196            -3.152_377_656_863_808,
197            5.642_296_713_889_555,
198        ),
199    )]
200    #[case(
201        DynFrame::Iau(DynOrigin::Moon),
202        DVec3::new(
203            3.777_805_761_337_502e3,
204            -5.633_812_666_439_680_5e3,
205            -3.896_880_165_980_424e2,
206        ),
207        DVec3::new(
208            2.576_901_711_027_508_3,
209            1.250_106_874_006_032_4,
210            7.100_615_382_464_156,
211        ),
212    )]
213    fn test_icrf_to_bodyfixed(#[case] frame: DynFrame, #[case] r_exp: DVec3, #[case] v_exp: DVec3) {
214        let time = Utc::from_iso("2024-07-05T09:09:18.173")
215            .unwrap()
216            .to_dyn_time();
217        let r = DVec3::new(-5530.01774359, -3487.0895338, -1850.03476185);
218        let v = DVec3::new(1.29534407, -5.02456882, 5.6391936);
219        let rot = DefaultTransformProvider
220            .try_transform(DynFrame::Icrf, frame, time)
221            .unwrap();
222        let (r_act, v_act) = rot.rotate_state(r, v);
223        assert_approx_eq!(r_act, r_exp, rtol <= 1e-8);
224        assert_approx_eq!(v_act, v_exp, rtol <= 1e-5);
225    }
226}