Skip to main content

lox_frames/
frames.rs

1// SPDX-FileCopyrightText: 2024 Helge Eichhorn <git@helgeeichhorn.de>
2// SPDX-FileCopyrightText: 2025 Helge Eichhorn <git@helgeeichhorn.de>
3//
4// SPDX-License-Identifier: MPL-2.0
5
6use std::str::FromStr;
7
8use lox_bodies::{
9    CoordinateOrigin, Earth, Origin, RotationalElements, Spheroid, TryRotationalElements,
10    TrySpheroid, UndefinedOriginPropertyError,
11};
12use lox_core::coords::Ellipsoid;
13use thiserror::Error;
14
15use crate::{
16    iers::{Iau2000Model, IersSystem, ReferenceSystem},
17    traits::{
18        BodyFixed, FrameKey, NonBodyFixedFrameError, NonQuasiInertialFrameError, QuasiInertial,
19        ReferenceEllipsoid, ReferenceFrame, TryBodyFixed, TryQuasiInertial, TryReferenceEllipsoid,
20        UndefinedReferenceEllipsoidError, frame_key,
21    },
22};
23
24/// International Celestial Reference Frame.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27#[cfg_attr(feature = "serde", serde(into = "&'static str", try_from = "String"))]
28pub struct Icrf;
29
30impl ReferenceFrame for Icrf {
31    fn name(&self) -> String {
32        "International Celestial Reference Frame".to_string()
33    }
34
35    fn abbreviation(&self) -> String {
36        "ICRF".to_string()
37    }
38
39    fn frame_key(&self, _: crate::traits::private::Internal) -> Option<FrameKey> {
40        Some(FrameKey::Icrf)
41    }
42}
43
44impl QuasiInertial for Icrf {}
45
46/// J2000 Mean Equator and Equinox frame.
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49#[cfg_attr(feature = "serde", serde(into = "&'static str", try_from = "String"))]
50pub struct J2000;
51
52impl ReferenceFrame for J2000 {
53    fn name(&self) -> String {
54        "J2000 Mean Equator and Equinox".to_string()
55    }
56
57    fn abbreviation(&self) -> String {
58        "J2000".to_string()
59    }
60
61    fn frame_key(&self, _: crate::traits::private::Internal) -> Option<FrameKey> {
62        Some(FrameKey::J2000)
63    }
64}
65
66impl QuasiInertial for J2000 {}
67
68/// Celestial Intermediate Reference Frame.
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
71#[cfg_attr(feature = "serde", serde(into = "&'static str", try_from = "String"))]
72pub struct Cirf;
73
74impl ReferenceFrame for Cirf {
75    fn name(&self) -> String {
76        "Celestial Intermediate Reference Frame".to_string()
77    }
78
79    fn abbreviation(&self) -> String {
80        "CIRF".to_string()
81    }
82
83    fn frame_key(&self, _: crate::traits::private::Internal) -> Option<FrameKey> {
84        Some(FrameKey::Cirf)
85    }
86}
87
88impl QuasiInertial for Cirf {}
89
90/// Terrestrial Intermediate Reference Frame.
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
92#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
93#[cfg_attr(feature = "serde", serde(into = "&'static str", try_from = "String"))]
94pub struct Tirf;
95
96impl ReferenceFrame for Tirf {
97    fn name(&self) -> String {
98        "Terrestrial Intermediate Reference Frame".to_string()
99    }
100
101    fn abbreviation(&self) -> String {
102        "TIRF".to_string()
103    }
104
105    fn frame_key(&self, _: crate::traits::private::Internal) -> Option<FrameKey> {
106        Some(FrameKey::Tirf)
107    }
108}
109
110impl BodyFixed for Tirf {
111    type Origin = Earth;
112
113    fn origin(&self) -> Self::Origin {
114        Earth
115    }
116}
117
118// TIRF differs from ITRF only by polar motion, so it shares ITRF's datum.
119impl ReferenceEllipsoid for Tirf {
120    fn reference_ellipsoid(&self) -> Ellipsoid {
121        Ellipsoid::GRS80
122    }
123}
124
125/// International Terrestrial Reference Frame.
126#[derive(Clone, Copy, Debug, PartialEq, Eq)]
127#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
128#[cfg_attr(feature = "serde", serde(into = "&'static str", try_from = "String"))]
129pub struct Itrf;
130
131impl ReferenceFrame for Itrf {
132    fn name(&self) -> String {
133        "International Terrestrial Reference Frame".to_string()
134    }
135
136    fn abbreviation(&self) -> String {
137        "ITRF".to_string()
138    }
139
140    fn frame_key(&self, _: crate::traits::private::Internal) -> Option<FrameKey> {
141        Some(FrameKey::Itrf)
142    }
143}
144
145impl BodyFixed for Itrf {
146    type Origin = Earth;
147
148    fn origin(&self) -> Self::Origin {
149        Earth
150    }
151}
152
153impl ReferenceEllipsoid for Itrf {
154    fn reference_ellipsoid(&self) -> Ellipsoid {
155        Ellipsoid::GRS80
156    }
157}
158
159/// Mean of Date frame, parameterised by IERS convention.
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
162pub struct Mod<T: IersSystem>(pub T);
163
164impl<T> ReferenceFrame for Mod<T>
165where
166    T: IersSystem + Into<ReferenceSystem> + Copy,
167{
168    fn name(&self) -> String {
169        format!("{} Mean of Date Frame", self.0.name())
170    }
171
172    fn abbreviation(&self) -> String {
173        format!("MOD({})", self.0.abbreviation())
174    }
175
176    fn frame_key(&self, _: crate::traits::private::Internal) -> Option<FrameKey> {
177        Some(FrameKey::Mod(self.0.into()))
178    }
179}
180
181impl<T> QuasiInertial for Mod<T> where T: IersSystem + Into<ReferenceSystem> + Copy {}
182
183/// True of Date frame, parameterised by IERS convention.
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
186pub struct Tod<T: IersSystem>(pub T);
187
188impl<T> ReferenceFrame for Tod<T>
189where
190    T: IersSystem + Into<ReferenceSystem> + Copy,
191{
192    fn name(&self) -> String {
193        format!("{} True of Date Frame", self.0.name())
194    }
195
196    fn abbreviation(&self) -> String {
197        format!("TOD({})", self.0.abbreviation())
198    }
199
200    fn frame_key(&self, _: crate::traits::private::Internal) -> Option<FrameKey> {
201        Some(FrameKey::Tod(self.0.into()))
202    }
203}
204
205impl<T> QuasiInertial for Tod<T> where T: IersSystem + Into<ReferenceSystem> + Copy {}
206
207/// Pseudo-Earth Fixed frame, parameterised by IERS convention.
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
210pub struct Pef<T: IersSystem>(pub T);
211
212impl<T> ReferenceFrame for Pef<T>
213where
214    T: IersSystem + Into<ReferenceSystem> + Copy,
215{
216    fn name(&self) -> String {
217        format!("{} Pseudo-Earth Fixed Frame", self.0.name())
218    }
219
220    fn abbreviation(&self) -> String {
221        format!("PEF({})", self.0.abbreviation())
222    }
223
224    fn frame_key(&self, _: crate::traits::private::Internal) -> Option<FrameKey> {
225        Some(FrameKey::Pef(self.0.into()))
226    }
227}
228
229impl<T> BodyFixed for Pef<T>
230where
231    T: IersSystem + Into<ReferenceSystem> + Copy,
232{
233    type Origin = Earth;
234
235    fn origin(&self) -> Self::Origin {
236        Earth
237    }
238}
239
240// PEF differs from ITRF only by polar motion, so it shares ITRF's datum.
241impl<T> ReferenceEllipsoid for Pef<T>
242where
243    T: IersSystem + Into<ReferenceSystem> + Copy,
244{
245    fn reference_ellipsoid(&self) -> Ellipsoid {
246        Ellipsoid::GRS80
247    }
248}
249
250/// True Equator Mean Equinox frame.
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
253#[cfg_attr(feature = "serde", serde(into = "&'static str", try_from = "String"))]
254pub struct Teme;
255
256impl ReferenceFrame for Teme {
257    fn name(&self) -> String {
258        "True Equator Mean Equinox".to_owned()
259    }
260
261    fn abbreviation(&self) -> String {
262        "TEME".to_owned()
263    }
264
265    fn frame_key(&self, _: crate::traits::private::Internal) -> Option<FrameKey> {
266        Some(FrameKey::Teme)
267    }
268}
269
270impl QuasiInertial for Teme {}
271
272// -- serde: serialize frame ZSTs as their abbreviation --
273
274macro_rules! impl_frame_serde {
275    ($ty:ident, $abbrev:literal) => {
276        #[cfg(feature = "serde")]
277        impl From<$ty> for &'static str {
278            fn from(_: $ty) -> Self {
279                $abbrev
280            }
281        }
282
283        #[cfg(feature = "serde")]
284        impl TryFrom<String> for $ty {
285            type Error = String;
286            fn try_from(s: String) -> Result<Self, Self::Error> {
287                if s == $abbrev {
288                    Ok($ty)
289                } else {
290                    Err(format!("expected \"{}\", got \"{}\"", $abbrev, s))
291                }
292            }
293        }
294    };
295}
296
297impl_frame_serde!(Icrf, "ICRF");
298impl_frame_serde!(J2000, "J2000");
299impl_frame_serde!(Cirf, "CIRF");
300impl_frame_serde!(Tirf, "TIRF");
301impl_frame_serde!(Itrf, "ITRF");
302impl_frame_serde!(Teme, "TEME");
303
304/// IAU body-fixed reference frame derived from rotational elements.
305#[derive(Clone, Copy, Debug, PartialEq, Eq)]
306#[cfg_attr(feature = "serde", derive(serde::Serialize))]
307pub struct Iau<T: TryRotationalElements>(T);
308
309// Deserialization goes through `try_new` so a body with undefined rotational
310// elements cannot produce a frame that later panics on use.
311#[cfg(feature = "serde")]
312impl<'de, T> serde::Deserialize<'de> for Iau<T>
313where
314    T: TryRotationalElements + serde::Deserialize<'de>,
315{
316    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
317    where
318        D: serde::Deserializer<'de>,
319    {
320        let body = T::deserialize(deserializer)?;
321        Iau::try_new(body).map_err(serde::de::Error::custom)
322    }
323}
324
325impl<T> Iau<T>
326where
327    T: RotationalElements,
328{
329    /// Creates an IAU frame for a body with known rotational elements.
330    pub fn new(body: T) -> Self {
331        Self(body)
332    }
333}
334
335impl<T> Iau<T>
336where
337    T: TryRotationalElements,
338{
339    /// Creates an IAU frame, returning an error if rotational elements are undefined.
340    pub fn try_new(body: T) -> Result<Self, UndefinedOriginPropertyError> {
341        let _ = body.try_right_ascension(0.0)?;
342        Ok(Self(body))
343    }
344
345    /// Returns the underlying body.
346    pub fn body(&self) -> T
347    where
348        T: Copy,
349    {
350        self.0
351    }
352
353    /// Returns the rotational elements (right ascension, declination, prime meridian) at
354    /// the given Julian centuries since J2000.
355    pub fn rotational_elements(&self, j2000: f64) -> (f64, f64, f64) {
356        // Construction (`new`, `try_new`, and deserialization) guarantees the
357        // body has defined rotational elements.
358        self.0
359            .try_rotational_elements(j2000)
360            .expect("Iau frame wraps a body with defined rotational elements")
361    }
362
363    /// Returns the time derivatives of the rotational elements.
364    pub fn rotational_element_rates(&self, j2000: f64) -> (f64, f64, f64) {
365        self.0
366            .try_rotational_element_rates(j2000)
367            .expect("Iau frame wraps a body with defined rotational elements")
368    }
369}
370
371impl<T: TryRotationalElements + Copy> BodyFixed for Iau<T> {
372    type Origin = T;
373
374    fn origin(&self) -> Self::Origin {
375        self.0
376    }
377}
378
379impl<T: RotationalElements + Spheroid + Copy> ReferenceEllipsoid for Iau<T> {
380    fn reference_ellipsoid(&self) -> Ellipsoid {
381        self.0.ellipsoid()
382    }
383}
384
385/// Full name of the IAU body-fixed frame for a body named `body`.
386pub(crate) fn iau_name(body: &str) -> String {
387    match body {
388        "Sun" | "Moon" => format!("IAU Body-Fixed Reference Frame for the {body}"),
389        _ => format!("IAU Body-Fixed Reference Frame for {body}"),
390    }
391}
392
393/// Abbreviation of the IAU body-fixed frame for a body named `body`.
394pub(crate) fn iau_abbreviation(body: &str) -> String {
395    format!("IAU_{}", body.replace([' ', '-'], "_").to_uppercase())
396}
397
398impl<T> ReferenceFrame for Iau<T>
399where
400    T: TryRotationalElements + CoordinateOrigin,
401{
402    fn name(&self) -> String {
403        iau_name(self.0.name())
404    }
405
406    fn abbreviation(&self) -> String {
407        iau_abbreviation(self.0.name())
408    }
409
410    fn frame_key(&self, _: crate::traits::private::Internal) -> Option<FrameKey> {
411        Some(FrameKey::Iau(self.0.id()))
412    }
413}
414
415#[cfg(all(test, feature = "serde"))]
416mod serde_tests {
417    use lox_bodies::Origin;
418
419    use super::Iau;
420
421    #[test]
422    fn deserialize_valid_body() {
423        let json = serde_json::to_string(&Origin::Earth).unwrap();
424        let frame: Iau<Origin> = serde_json::from_str(&json).unwrap();
425        assert_eq!(frame.body(), Origin::Earth);
426    }
427
428    #[test]
429    fn deserialize_rejects_undefined_elements() {
430        // Sycorax has no rotational elements; deserializing it as an IAU frame
431        // must fail rather than yield a frame that panics on first use.
432        let json = serde_json::to_string(&Origin::Sycorax).unwrap();
433        let result: Result<Iau<Origin>, _> = serde_json::from_str(&json);
434        assert!(result.is_err());
435    }
436}
437
438/// A reference frame determined at runtime.
439///
440/// Covers the same set of frames as the zero-sized frame types, as a single
441/// closed enum. Because the frame is not known statically, frame properties
442/// are reached through the fallible checks ([`TryQuasiInertial`],
443/// [`TryBodyFixed`]) rather than the marker traits.
444#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
445#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
446pub enum Frame {
447    /// International Celestial Reference Frame.
448    #[default]
449    Icrf,
450    /// J2000 Mean Equator and Equinox.
451    J2000,
452    /// Celestial Intermediate Reference Frame.
453    Cirf,
454    /// Terrestrial Intermediate Reference Frame.
455    Tirf,
456    /// International Terrestrial Reference Frame.
457    Itrf,
458    /// IAU body-fixed frame for the given origin.
459    Iau(Origin),
460    /// Mean of Date frame for the given IERS convention.
461    Mod(ReferenceSystem),
462    /// True of Date frame for the given IERS convention.
463    Tod(ReferenceSystem),
464    /// Pseudo-Earth Fixed frame for the given IERS convention.
465    Pef(ReferenceSystem),
466    /// True Equator Mean Equinox.
467    Teme,
468}
469
470impl ReferenceFrame for Frame {
471    fn name(&self) -> String {
472        match self {
473            Frame::Icrf => Icrf.name(),
474            Frame::J2000 => J2000.name(),
475            Frame::Cirf => Cirf.name(),
476            Frame::Tirf => Tirf.name(),
477            Frame::Itrf => Itrf.name(),
478            Frame::Iau(dynamic_origin) => iau_name(dynamic_origin.name()),
479            Frame::Mod(sys) => Mod(*sys).name(),
480            Frame::Tod(sys) => Tod(*sys).name(),
481            Frame::Pef(sys) => Pef(*sys).name(),
482            Frame::Teme => Teme.name(),
483        }
484    }
485
486    fn abbreviation(&self) -> String {
487        match self {
488            Frame::Icrf => Icrf.abbreviation(),
489            Frame::J2000 => J2000.abbreviation(),
490            Frame::Cirf => Cirf.abbreviation(),
491            Frame::Tirf => Tirf.abbreviation(),
492            Frame::Itrf => Itrf.abbreviation(),
493            Frame::Iau(dynamic_origin) => iau_abbreviation(dynamic_origin.name()),
494            Frame::Mod(sys) => Mod(*sys).abbreviation(),
495            Frame::Tod(sys) => Tod(*sys).abbreviation(),
496            Frame::Pef(sys) => Pef(*sys).abbreviation(),
497            Frame::Teme => Teme.abbreviation(),
498        }
499    }
500
501    fn frame_key(&self, _: crate::traits::private::Internal) -> Option<FrameKey> {
502        match self {
503            Frame::Icrf => frame_key(&Icrf),
504            Frame::J2000 => frame_key(&J2000),
505            Frame::Cirf => frame_key(&Cirf),
506            Frame::Tirf => frame_key(&Tirf),
507            Frame::Itrf => frame_key(&Itrf),
508            Frame::Iau(dynamic_origin) => Some(FrameKey::Iau(dynamic_origin.id())),
509            Frame::Mod(sys) => frame_key(&Mod(*sys)),
510            Frame::Tod(sys) => frame_key(&Tod(*sys)),
511
512            Frame::Pef(sys) => frame_key(&Pef(*sys)),
513            Frame::Teme => frame_key(&Teme),
514        }
515    }
516}
517
518impl TryQuasiInertial for Frame {
519    fn try_quasi_inertial(&self) -> Result<(), NonQuasiInertialFrameError> {
520        match self {
521            Frame::Icrf
522            | Frame::J2000
523            | Frame::Cirf
524            | Frame::Mod(_)
525            | Frame::Tod(_)
526            | Frame::Teme => Ok(()),
527            _ => Err(NonQuasiInertialFrameError(self.abbreviation())),
528        }
529    }
530}
531
532impl TryBodyFixed for Frame {
533    type Origin = Origin;
534
535    fn try_body_fixed(&self) -> Result<(), NonBodyFixedFrameError> {
536        match self {
537            Frame::Iau(_) | Frame::Itrf | Frame::Tirf | Frame::Pef(_) => Ok(()),
538            _ => Err(NonBodyFixedFrameError(self.abbreviation())),
539        }
540    }
541
542    fn try_origin(&self) -> Result<Self::Origin, NonBodyFixedFrameError> {
543        match self {
544            Frame::Iau(origin) => Ok(*origin),
545            Frame::Itrf => Ok(Itrf.origin().into()),
546            Frame::Tirf => Ok(Tirf.origin().into()),
547            Frame::Pef(sys) => Ok(Pef(*sys).origin().into()),
548            _ => Err(NonBodyFixedFrameError(self.abbreviation())),
549        }
550    }
551}
552
553impl TryReferenceEllipsoid for Frame {
554    fn try_reference_ellipsoid(&self) -> Result<Ellipsoid, UndefinedReferenceEllipsoidError> {
555        match self {
556            Frame::Iau(origin) => Ok(origin.try_ellipsoid()?),
557            Frame::Itrf => Ok(Itrf.reference_ellipsoid()),
558            Frame::Tirf => Ok(Tirf.reference_ellipsoid()),
559            Frame::Pef(sys) => Ok(Pef(*sys).reference_ellipsoid()),
560            _ => Err(UndefinedReferenceEllipsoidError::NotBodyFixed(
561                NonBodyFixedFrameError(self.abbreviation()),
562            )),
563        }
564    }
565}
566
567// Simple frame conversions.
568
569impl From<Icrf> for Frame {
570    fn from(_: Icrf) -> Self {
571        Frame::Icrf
572    }
573}
574
575impl From<J2000> for Frame {
576    fn from(_: J2000) -> Self {
577        Frame::J2000
578    }
579}
580
581impl From<Cirf> for Frame {
582    fn from(_: Cirf) -> Self {
583        Frame::Cirf
584    }
585}
586
587impl From<Tirf> for Frame {
588    fn from(_: Tirf) -> Self {
589        Frame::Tirf
590    }
591}
592
593impl From<Itrf> for Frame {
594    fn from(_: Itrf) -> Self {
595        Frame::Itrf
596    }
597}
598
599impl From<Teme> for Frame {
600    fn from(_: Teme) -> Self {
601        Frame::Teme
602    }
603}
604
605// Parameterized equinox-based frames.
606
607impl<T: IersSystem + Into<ReferenceSystem>> From<Mod<T>> for Frame {
608    fn from(frame: Mod<T>) -> Self {
609        Frame::Mod(frame.0.into())
610    }
611}
612
613impl<T: IersSystem + Into<ReferenceSystem>> From<Tod<T>> for Frame {
614    fn from(frame: Tod<T>) -> Self {
615        Frame::Tod(frame.0.into())
616    }
617}
618
619impl<T: IersSystem + Into<ReferenceSystem>> From<Pef<T>> for Frame {
620    fn from(frame: Pef<T>) -> Self {
621        Frame::Pef(frame.0.into())
622    }
623}
624
625// IAU body-fixed frames.
626
627impl<T: TryRotationalElements + Copy + Into<Origin>> From<Iau<T>> for Frame {
628    fn from(frame: Iau<T>) -> Self {
629        Frame::Iau(frame.body().into())
630    }
631}
632
633fn parse_iau_frame(s: &str) -> Option<Frame> {
634    let (prefix, origin) = s.split_once("_")?;
635    if prefix.to_lowercase() != "iau" {
636        return None;
637    }
638    let origin: Origin = origin.to_lowercase().parse().ok()?;
639    let _ = origin.try_rotational_elements(0.0).ok()?;
640    Some(Frame::Iau(origin))
641}
642
643fn parse_reference_system(s: &str) -> Option<ReferenceSystem> {
644    match s.to_uppercase().as_str() {
645        "IERS1996" => Some(ReferenceSystem::Iers1996),
646        "IERS2003" | "IERS2003A" => Some(ReferenceSystem::Iers2003(Iau2000Model::A)),
647        "IERS2003B" => Some(ReferenceSystem::Iers2003(Iau2000Model::B)),
648        "IERS2010" => Some(ReferenceSystem::Iers2010),
649        _ => None,
650    }
651}
652
653/// Parse frames in `FRAME(SYSTEM)` format, e.g. `MOD(IERS2003)`.
654fn parse_equinox_frame(s: &str) -> Option<Frame> {
655    let s_stripped = s.strip_suffix(')')?;
656    let (frame, system) = s_stripped.split_once('(')?;
657    let sys = parse_reference_system(system)?;
658    match frame.to_uppercase().as_str() {
659        "MOD" => Some(Frame::Mod(sys)),
660        "TOD" => Some(Frame::Tod(sys)),
661        "PEF" => Some(Frame::Pef(sys)),
662        _ => None,
663    }
664}
665
666/// No frame matching the given name is known.
667#[derive(Clone, Debug, Error, PartialEq, Eq)]
668#[error("no frame with name '{0}' is known")]
669pub struct UnknownFrameError(String);
670
671impl FromStr for Frame {
672    type Err = UnknownFrameError;
673
674    fn from_str(s: &str) -> Result<Self, Self::Err> {
675        match s.to_uppercase().as_str() {
676            "ICRF" => Ok(Frame::Icrf),
677            "J2000" | "EME2000" => Ok(Frame::J2000),
678            "CIRF" => Ok(Frame::Cirf),
679            "TIRF" => Ok(Frame::Tirf),
680            "ITRF" => Ok(Frame::Itrf),
681            "TEME" => Ok(Frame::Teme),
682            "MOD" => Ok(Frame::Mod(ReferenceSystem::Iers1996)),
683            "TOD" => Ok(Frame::Tod(ReferenceSystem::Iers1996)),
684            "PEF" => Ok(Frame::Pef(ReferenceSystem::Iers1996)),
685            _ => {
686                if let Some(frame) = parse_equinox_frame(s) {
687                    Ok(frame)
688                } else if let Some(frame) = parse_iau_frame(s) {
689                    Ok(frame)
690                } else {
691                    Err(UnknownFrameError(s.to_owned()))
692                }
693            }
694        }
695    }
696}
697
698#[cfg(test)]
699mod tests {
700    use super::*;
701
702    use crate::iers::{Iers1996, Iers2003, Iers2010};
703    use crate::rotations::TryRotation;
704    use crate::traits::frame_key;
705    use crate::{Iau, providers::DefaultRotationProvider};
706
707    use lox_approx::assert_approx_eq;
708    use lox_bodies::{Earth, Origin};
709    use lox_core::glam::DVec3;
710    use lox_time::utc::Utc;
711    use rstest::rstest;
712
713    #[test]
714    fn zst_frame_names_and_abbreviations() {
715        assert_eq!(Icrf.abbreviation(), "ICRF");
716        assert_eq!(J2000.abbreviation(), "J2000");
717        assert_eq!(Cirf.abbreviation(), "CIRF");
718        assert_eq!(Tirf.abbreviation(), "TIRF");
719        assert_eq!(Itrf.abbreviation(), "ITRF");
720        assert_eq!(Teme.abbreviation(), "TEME");
721        assert!(Icrf.name().contains("Celestial"));
722        assert!(J2000.name().contains("J2000"));
723        assert!(Cirf.name().contains("Celestial Intermediate"));
724        assert!(Tirf.name().contains("Terrestrial Intermediate"));
725        assert!(Itrf.name().contains("Terrestrial Reference"));
726        assert!(Teme.name().contains("True Equator"));
727    }
728
729    #[test]
730    fn iau_frame_naming() {
731        // Sun/Moon take the "the" article; other bodies do not.
732        let sun = Iau::try_new(Origin::Sun).unwrap();
733        let earth = Iau::try_new(Origin::Earth).unwrap();
734        assert!(sun.name().contains("for the Sun"));
735        assert!(earth.name().contains("for Earth"));
736        assert_eq!(earth.abbreviation(), "IAU_EARTH");
737    }
738
739    #[test]
740    fn custom_frame_has_no_key() {
741        struct Custom;
742        impl ReferenceFrame for Custom {
743            fn name(&self) -> String {
744                "Custom".to_owned()
745            }
746            fn abbreviation(&self) -> String {
747                "CUS".to_owned()
748            }
749        }
750        assert_eq!(frame_key(&Custom), None);
751    }
752    #[rstest]
753    #[case::valid("IAU_EARTH", Some(Frame::Iau(Origin::Earth)))]
754    #[case::invalid_prefix("FOO_EARTH", None)]
755    #[case::unkown_body("IAU_RUPERT", None)]
756    #[case::undefined_rotation("IAU_SYCORAX", None)]
757    fn test_parse_iau_frame(#[case] name: &str, #[case] exp: Option<Frame>) {
758        let act = parse_iau_frame(name);
759        assert_eq!(act, exp)
760    }
761
762    #[rstest]
763    #[case(
764        Frame::Iau(Origin::Earth),
765        DVec3::new(
766            -5.740_259_426_667_957e3,
767            3.121_136_072_795_472_5e3,
768            -1.863_182_656_331_802_7e3,
769        ),
770        DVec3::new(
771            -3.532_378_757_836_52,
772            -3.152_377_656_863_808,
773            5.642_296_713_889_555,
774        ),
775    )]
776    #[case(
777        Frame::Iau(Origin::Moon),
778        DVec3::new(
779            3.777_805_761_337_502e3,
780            -5.633_812_666_439_680_5e3,
781            -3.896_880_165_980_424e2,
782        ),
783        DVec3::new(
784            2.576_901_711_027_508_3,
785            1.250_106_874_006_032_4,
786            7.100_615_382_464_156,
787        ),
788    )]
789    fn test_icrf_to_bodyfixed(#[case] frame: Frame, #[case] r_exp: DVec3, #[case] v_exp: DVec3) {
790        let time = Utc::from_iso("2024-07-05T09:09:18.173")
791            .unwrap()
792            .to_dynamic_time();
793        let r = DVec3::new(-5530.01774359, -3487.0895338, -1850.03476185);
794        let v = DVec3::new(1.29534407, -5.02456882, 5.6391936);
795        let rot = DefaultRotationProvider
796            .try_rotation(Frame::Icrf, frame, time)
797            .unwrap();
798        let (r_act, v_act) = rot.rotate_state(r, v);
799        assert_approx_eq!(r_act, r_exp, rtol <= 1e-8);
800        assert_approx_eq!(v_act, v_exp, rtol <= 1e-5);
801    }
802
803    #[rstest]
804    #[case("MOD", Frame::Mod(ReferenceSystem::Iers1996))]
805    #[case("mod", Frame::Mod(ReferenceSystem::Iers1996))]
806    #[case("TOD", Frame::Tod(ReferenceSystem::Iers1996))]
807    #[case("tod", Frame::Tod(ReferenceSystem::Iers1996))]
808    #[case("PEF", Frame::Pef(ReferenceSystem::Iers1996))]
809    #[case("pef", Frame::Pef(ReferenceSystem::Iers1996))]
810    #[case("MOD(IERS1996)", Frame::Mod(ReferenceSystem::Iers1996))]
811    #[case(
812        "MOD(IERS2003)",
813        Frame::Mod(ReferenceSystem::Iers2003(Iau2000Model::A))
814    )]
815    #[case(
816        "mod(iers2003)",
817        Frame::Mod(ReferenceSystem::Iers2003(Iau2000Model::A))
818    )]
819    #[case(
820        "TOD(IERS2003)",
821        Frame::Tod(ReferenceSystem::Iers2003(Iau2000Model::A))
822    )]
823    #[case(
824        "PEF(IERS2003)",
825        Frame::Pef(ReferenceSystem::Iers2003(Iau2000Model::A))
826    )]
827    #[case("MOD(IERS2010)", Frame::Mod(ReferenceSystem::Iers2010))]
828    #[case("TOD(IERS2010)", Frame::Tod(ReferenceSystem::Iers2010))]
829    #[case("PEF(IERS2010)", Frame::Pef(ReferenceSystem::Iers2010))]
830    fn test_parse_equinox_frames(#[case] name: &str, #[case] exp: Frame) {
831        let act: Frame = name.parse().unwrap();
832        assert_eq!(act, exp);
833    }
834
835    #[test]
836    fn test_frame_key() {
837        assert_eq!(frame_key(&Icrf), frame_key(&Frame::Icrf));
838        assert_eq!(frame_key(&J2000), frame_key(&Frame::J2000));
839        assert_eq!(frame_key(&Cirf), frame_key(&Frame::Cirf));
840        assert_eq!(frame_key(&Tirf), frame_key(&Frame::Tirf));
841        assert_eq!(frame_key(&Itrf), frame_key(&Frame::Itrf));
842        assert_eq!(
843            frame_key(&Iau::new(Earth)),
844            frame_key(&Frame::Iau(Origin::Earth))
845        );
846        // Parameterized frames agree too, and the nutation model is part of the key.
847        let mod_b = ReferenceSystem::Iers2003(Iau2000Model::B);
848        assert_eq!(frame_key(&Mod(mod_b)), frame_key(&Frame::Mod(mod_b)));
849        assert_eq!(frame_key(&Teme), frame_key(&Frame::Teme));
850        assert_ne!(
851            frame_key(&Frame::Mod(ReferenceSystem::Iers2003(Iau2000Model::A))),
852            frame_key(&Frame::Mod(mod_b))
853        );
854    }
855
856    #[rstest]
857    #[case("J2000", Frame::J2000)]
858    #[case("j2000", Frame::J2000)]
859    #[case("EME2000", Frame::J2000)]
860    fn test_parse_j2000(#[case] name: &str, #[case] exp: Frame) {
861        let act: Frame = name.parse().unwrap();
862        assert_eq!(act, exp);
863    }
864
865    #[test]
866    fn test_j2000_quasi_inertial() {
867        assert!(Frame::J2000.try_quasi_inertial().is_ok());
868    }
869
870    /// Quasi-inertial frames do not rotate with a central body; body-fixed
871    /// frames do. Every variant is listed so adding one forces a decision.
872    #[rstest]
873    #[case(Frame::Icrf, true)]
874    #[case(Frame::J2000, true)]
875    #[case(Frame::Cirf, true)]
876    #[case(Frame::Teme, true)]
877    #[case(Frame::Mod(ReferenceSystem::Iers1996), true)]
878    #[case(Frame::Mod(ReferenceSystem::Iers2010), true)]
879    #[case(Frame::Tod(ReferenceSystem::Iers1996), true)]
880    #[case(Frame::Tod(ReferenceSystem::Iers2010), true)]
881    #[case(Frame::Tirf, false)]
882    #[case(Frame::Itrf, false)]
883    #[case(Frame::Pef(ReferenceSystem::Iers1996), false)]
884    #[case(Frame::Pef(ReferenceSystem::Iers2010), false)]
885    #[case(Frame::Iau(Origin::Earth), false)]
886    fn test_quasi_inertial_classification(#[case] frame: Frame, #[case] exp: bool) {
887        assert_eq!(frame.try_quasi_inertial().is_ok(), exp);
888        // The two classifications are mutually exclusive.
889        assert!(!(frame.try_quasi_inertial().is_ok() && frame.try_body_fixed().is_ok()));
890    }
891
892    #[rstest]
893    #[case(Frame::Tirf, true)]
894    #[case(Frame::Itrf, true)]
895    #[case(Frame::Pef(ReferenceSystem::Iers1996), true)]
896    #[case(Frame::Pef(ReferenceSystem::Iers2010), true)]
897    #[case(Frame::Iau(Origin::Earth), true)]
898    #[case(Frame::Iau(Origin::Moon), true)]
899    #[case(Frame::Icrf, false)]
900    #[case(Frame::J2000, false)]
901    #[case(Frame::Cirf, false)]
902    #[case(Frame::Teme, false)]
903    #[case(Frame::Mod(ReferenceSystem::Iers1996), false)]
904    #[case(Frame::Tod(ReferenceSystem::Iers1996), false)]
905    fn test_body_fixed_classification(#[case] frame: Frame, #[case] exp: bool) {
906        assert_eq!(frame.try_body_fixed().is_ok(), exp);
907        assert_eq!(frame.try_origin().is_ok(), exp);
908    }
909
910    /// The terrestrial frames are all realizations of the same rotating Earth.
911    #[rstest]
912    #[case(Frame::Itrf)]
913    #[case(Frame::Tirf)]
914    #[case(Frame::Pef(ReferenceSystem::Iers2010))]
915    fn test_terrestrial_frames_share_origin_and_datum(#[case] frame: Frame) {
916        assert_eq!(frame.try_origin().unwrap(), Origin::Earth);
917        assert_eq!(frame.try_reference_ellipsoid().unwrap(), Ellipsoid::GRS80);
918    }
919
920    // The zero-sized types carry their capabilities as compile-time markers and
921    // `Frame` as runtime checks. The two must not drift apart: passing a ZST to
922    // these helpers requires the marker, and the assertion covers the enum.
923
924    fn assert_quasi_inertial_agrees(frame: impl QuasiInertial + Copy + Into<Frame>) {
925        let dynamic: Frame = frame.into();
926        assert!(
927            dynamic.try_quasi_inertial().is_ok(),
928            "{} is quasi-inertial as a ZST but not as a Frame",
929            dynamic.abbreviation()
930        );
931    }
932
933    fn assert_body_fixed_agrees<F>(frame: F)
934    where
935        F: BodyFixed + Copy + Into<Frame>,
936        F::Origin: Into<Origin>,
937    {
938        let dynamic: Frame = frame.into();
939        assert!(
940            dynamic.try_body_fixed().is_ok(),
941            "{} is body-fixed as a ZST but not as a Frame",
942            dynamic.abbreviation()
943        );
944        assert_eq!(dynamic.try_origin().unwrap(), frame.origin().into());
945    }
946
947    fn assert_reference_ellipsoid_agrees(frame: impl ReferenceEllipsoid + Copy + Into<Frame>) {
948        let dynamic: Frame = frame.into();
949        assert_eq!(
950            dynamic.try_reference_ellipsoid().unwrap(),
951            frame.reference_ellipsoid()
952        );
953    }
954
955    #[test]
956    fn test_quasi_inertial_zsts_agree_with_frame() {
957        assert_quasi_inertial_agrees(Icrf);
958        assert_quasi_inertial_agrees(J2000);
959        assert_quasi_inertial_agrees(Cirf);
960        assert_quasi_inertial_agrees(Teme);
961        assert_quasi_inertial_agrees(Mod(Iers1996));
962        assert_quasi_inertial_agrees(Mod(Iers2003::default()));
963        assert_quasi_inertial_agrees(Mod(Iers2010));
964        assert_quasi_inertial_agrees(Tod(Iers1996));
965        assert_quasi_inertial_agrees(Tod(Iers2003::default()));
966        assert_quasi_inertial_agrees(Tod(Iers2010));
967    }
968
969    #[test]
970    fn test_body_fixed_zsts_agree_with_frame() {
971        assert_body_fixed_agrees(Itrf);
972        assert_body_fixed_agrees(Tirf);
973        assert_body_fixed_agrees(Pef(Iers1996));
974        assert_body_fixed_agrees(Pef(Iers2003::default()));
975        assert_body_fixed_agrees(Pef(Iers2010));
976        assert_body_fixed_agrees(Iau::new(Earth));
977    }
978
979    #[test]
980    fn test_reference_ellipsoid_zsts_agree_with_frame() {
981        assert_reference_ellipsoid_agrees(Itrf);
982        assert_reference_ellipsoid_agrees(Tirf);
983        assert_reference_ellipsoid_agrees(Pef(Iers1996));
984        assert_reference_ellipsoid_agrees(Pef(Iers2010));
985        assert_reference_ellipsoid_agrees(Iau::new(Earth));
986    }
987
988    #[test]
989    fn test_from_simple_frames() {
990        assert_eq!(Frame::from(Icrf), Frame::Icrf);
991        assert_eq!(Frame::from(J2000), Frame::J2000);
992        assert_eq!(Frame::from(Cirf), Frame::Cirf);
993        assert_eq!(Frame::from(Tirf), Frame::Tirf);
994        assert_eq!(Frame::from(Itrf), Frame::Itrf);
995        assert_eq!(Frame::from(Teme), Frame::Teme);
996    }
997
998    #[test]
999    fn test_from_parameterized_frames() {
1000        assert_eq!(
1001            Frame::from(Mod(Iers1996)),
1002            Frame::Mod(ReferenceSystem::Iers1996)
1003        );
1004        assert_eq!(
1005            Frame::from(Tod(Iers2003::default())),
1006            Frame::Tod(ReferenceSystem::Iers2003(Iau2000Model::A))
1007        );
1008        assert_eq!(
1009            Frame::from(Pef(Iers2010)),
1010            Frame::Pef(ReferenceSystem::Iers2010)
1011        );
1012    }
1013
1014    #[test]
1015    fn test_from_iau_frame() {
1016        assert_eq!(Frame::from(Iau::new(Earth)), Frame::Iau(Origin::Earth));
1017    }
1018
1019    #[rstest]
1020    #[case(Frame::Icrf)]
1021    #[case(Frame::J2000)]
1022    #[case(Frame::Cirf)]
1023    #[case(Frame::Tirf)]
1024    #[case(Frame::Itrf)]
1025    #[case(Frame::Teme)]
1026    #[case(Frame::Mod(ReferenceSystem::Iers1996))]
1027    #[case(Frame::Mod(ReferenceSystem::Iers2003(Iau2000Model::A)))]
1028    #[case(Frame::Mod(ReferenceSystem::Iers2003(Iau2000Model::B)))]
1029    #[case(Frame::Mod(ReferenceSystem::Iers2010))]
1030    #[case(Frame::Tod(ReferenceSystem::Iers1996))]
1031    #[case(Frame::Tod(ReferenceSystem::Iers2003(Iau2000Model::A)))]
1032    #[case(Frame::Tod(ReferenceSystem::Iers2003(Iau2000Model::B)))]
1033    #[case(Frame::Tod(ReferenceSystem::Iers2010))]
1034    #[case(Frame::Pef(ReferenceSystem::Iers1996))]
1035    #[case(Frame::Pef(ReferenceSystem::Iers2003(Iau2000Model::A)))]
1036    #[case(Frame::Pef(ReferenceSystem::Iers2003(Iau2000Model::B)))]
1037    #[case(Frame::Pef(ReferenceSystem::Iers2010))]
1038    #[case(Frame::Iau(Origin::Earth))]
1039    fn test_abbreviation_round_trip(#[case] frame: Frame) {
1040        let abbr = frame.abbreviation();
1041        let parsed: Frame = abbr
1042            .parse()
1043            .unwrap_or_else(|e| panic!("failed to parse abbreviation '{}': {}", abbr, e));
1044        assert_eq!(parsed, frame);
1045    }
1046}