anise/frames/
frame.rs

1/*
2 * ANISE Toolkit
3 * Copyright (C) 2021-onward Christopher Rabotin <christopher.rabotin@gmail.com> et al. (cf. AUTHORS.md)
4 * This Source Code Form is subject to the terms of the Mozilla Public
5 * License, v. 2.0. If a copy of the MPL was not distributed with this
6 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7 *
8 * Documentation: https://nyxspace.com/
9 */
10
11use core::fmt;
12use core::fmt::Debug;
13use serde_derive::{Deserialize, Serialize};
14use snafu::ResultExt;
15
16#[cfg(feature = "metaload")]
17use serde_dhall::StaticType;
18
19use crate::astro::PhysicsResult;
20use crate::constants::celestial_objects::{
21    celestial_name_from_id, id_from_celestial_name, SOLAR_SYSTEM_BARYCENTER,
22};
23use crate::constants::orientations::{id_from_orientation_name, orientation_name_from_id, J2000};
24use crate::errors::{AlmanacError, EphemerisSnafu, OrientationSnafu, PhysicsError};
25use crate::prelude::FrameUid;
26use crate::structure::planetocentric::ellipsoid::Ellipsoid;
27use crate::NaifId;
28
29#[cfg(feature = "python")]
30use pyo3::exceptions::PyTypeError;
31#[cfg(feature = "python")]
32use pyo3::prelude::*;
33#[cfg(feature = "python")]
34use pyo3::pyclass::CompareOp;
35
36/// A Frame uniquely defined by its ephemeris center and orientation. Refer to FrameDetail for frames combined with parameters.
37///
38/// :type ephemeris_id: int
39/// :type orientation_id: int
40/// :type mu_km3_s2: float, optional
41/// :type shape: Ellipsoid, optional
42/// :rtype: Frame
43#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
44#[cfg_attr(feature = "metaload", derive(StaticType))]
45#[cfg_attr(feature = "python", pyclass)]
46#[cfg_attr(feature = "python", pyo3(module = "anise.astro"))]
47pub struct Frame {
48    pub ephemeris_id: NaifId,
49    pub orientation_id: NaifId,
50    /// Gravity parameter of this frame, only defined on celestial frames
51    pub mu_km3_s2: Option<f64>,
52    /// Shape of the geoid of this frame, only defined on geodetic frames
53    pub shape: Option<Ellipsoid>,
54}
55
56impl Frame {
57    /// Constructs a new frame given its ephemeris and orientations IDs, without defining anything else (so this is not a valid celestial frame, although the data could be populated later).
58    pub const fn new(ephemeris_id: NaifId, orientation_id: NaifId) -> Self {
59        Self {
60            ephemeris_id,
61            orientation_id,
62            mu_km3_s2: None,
63            shape: None,
64        }
65    }
66
67    pub const fn from_ephem_j2000(ephemeris_id: NaifId) -> Self {
68        Self::new(ephemeris_id, J2000)
69    }
70
71    pub const fn from_orient_ssb(orientation_id: NaifId) -> Self {
72        Self::new(SOLAR_SYSTEM_BARYCENTER, orientation_id)
73    }
74
75    /// Attempts to create a new frame from its center and reference frame name.
76    /// This function is compatible with the CCSDS OEM names.
77    pub fn from_name(center: &str, ref_frame: &str) -> Result<Self, AlmanacError> {
78        let ephemeris_id = id_from_celestial_name(center).context(EphemerisSnafu {
79            action: "converting center name to its ID",
80        })?;
81
82        let orientation_id = id_from_orientation_name(ref_frame).context(OrientationSnafu {
83            action: "converting reference frame to its ID",
84        })?;
85
86        Ok(Self::new(ephemeris_id, orientation_id))
87    }
88
89    /// Define Ellipsoid shape and return a new [Frame]
90    pub fn with_ellipsoid(mut self, shape: Ellipsoid) -> Self {
91        self.shape = Some(shape);
92        self
93    }
94
95    /// Returns a copy of this frame with the graviational parameter and the shape information from this frame.
96    /// Use this to prevent astrodynamical computations.
97    ///
98    /// :rtype: None
99    pub fn stripped(mut self) -> Self {
100        self.strip();
101        self
102    }
103}
104
105#[cfg(feature = "python")]
106#[cfg_attr(feature = "python", pymethods)]
107impl Frame {
108    /// Initializes a new [Frame] provided its ephemeris and orientation identifiers, and optionally its gravitational parameter (in km^3/s^2) and optionally its shape (cf. [Ellipsoid]).
109    #[new]
110    #[pyo3(signature=(ephemeris_id, orientation_id, mu_km3_s2=None, shape=None))]
111    pub fn py_new(
112        ephemeris_id: NaifId,
113        orientation_id: NaifId,
114        mu_km3_s2: Option<f64>,
115        shape: Option<Ellipsoid>,
116    ) -> Self {
117        Self {
118            ephemeris_id,
119            orientation_id,
120            mu_km3_s2,
121            shape,
122        }
123    }
124
125    fn __str__(&self) -> String {
126        format!("{self}")
127    }
128
129    fn __repr__(&self) -> String {
130        format!("{self} (@{self:p})")
131    }
132
133    fn __richcmp__(&self, other: &Self, op: CompareOp) -> Result<bool, PyErr> {
134        match op {
135            CompareOp::Eq => Ok(self == other),
136            CompareOp::Ne => Ok(self != other),
137            _ => Err(PyErr::new::<PyTypeError, _>(format!(
138                "{op:?} not available"
139            ))),
140        }
141    }
142
143    /// Allows for pickling the object
144    ///
145    /// :rtype: typing.Tuple
146    fn __getnewargs__(&self) -> Result<(NaifId, NaifId, Option<f64>, Option<Ellipsoid>), PyErr> {
147        Ok((
148            self.ephemeris_id,
149            self.orientation_id,
150            self.mu_km3_s2,
151            self.shape,
152        ))
153    }
154
155    /// :rtype: int
156    #[getter]
157    fn get_ephemeris_id(&self) -> PyResult<NaifId> {
158        Ok(self.ephemeris_id)
159    }
160    /// :type ephemeris_id: int
161    #[setter]
162    fn set_ephemeris_id(&mut self, ephemeris_id: NaifId) -> PyResult<()> {
163        self.ephemeris_id = ephemeris_id;
164        Ok(())
165    }
166    /// :rtype: int
167    #[getter]
168    fn get_orientation_id(&self) -> PyResult<NaifId> {
169        Ok(self.orientation_id)
170    }
171    /// :type orientation_id: int
172    #[setter]
173    fn set_orientation_id(&mut self, orientation_id: NaifId) -> PyResult<()> {
174        self.orientation_id = orientation_id;
175        Ok(())
176    }
177    /// :rtype: float
178    #[getter]
179    fn get_mu_km3_s2(&self) -> PyResult<Option<f64>> {
180        Ok(self.mu_km3_s2)
181    }
182    /// :type mu_km3_s2: float
183    #[setter]
184    fn set_mu_km3_s2(&mut self, mu_km3_s2: Option<f64>) -> PyResult<()> {
185        self.mu_km3_s2 = mu_km3_s2;
186        Ok(())
187    }
188    /// :rtype: Ellipsoid
189    #[getter]
190    fn get_shape(&self) -> PyResult<Option<Ellipsoid>> {
191        Ok(self.shape)
192    }
193    /// :type shape: Ellipsoid
194    #[setter]
195    fn set_shape(&mut self, shape: Option<Ellipsoid>) -> PyResult<()> {
196        self.shape = shape;
197        Ok(())
198    }
199}
200
201#[cfg_attr(feature = "python", pymethods)]
202impl Frame {
203    /// Returns a copy of this Frame whose ephemeris ID is set to the provided ID
204    ///
205    /// :type new_ephem_id: int
206    /// :rtype: Frame
207    pub const fn with_ephem(&self, new_ephem_id: NaifId) -> Self {
208        let mut me = *self;
209        me.ephemeris_id = new_ephem_id;
210        me
211    }
212
213    /// Returns a copy of this Frame whose orientation ID is set to the provided ID
214    ///
215    /// :type new_orient_id: int
216    /// :rtype: Frame
217    pub const fn with_orient(&self, new_orient_id: NaifId) -> Self {
218        let mut me = *self;
219        me.orientation_id = new_orient_id;
220        me
221    }
222
223    /// Returns whether this is a celestial frame
224    ///
225    /// :rtype: bool
226    pub const fn is_celestial(&self) -> bool {
227        self.mu_km3_s2.is_some()
228    }
229
230    /// Returns whether this is a geodetic frame
231    ///
232    /// :rtype: bool
233    pub const fn is_geodetic(&self) -> bool {
234        self.mu_km3_s2.is_some() && self.shape.is_some()
235    }
236
237    /// Returns true if the ephemeris origin is equal to the provided ID
238    ///
239    /// :type other_id: int
240    /// :rtype: bool
241    pub const fn ephem_origin_id_match(&self, other_id: NaifId) -> bool {
242        self.ephemeris_id == other_id
243    }
244    /// Returns true if the orientation origin is equal to the provided ID
245    ///
246    /// :type other_id: int
247    /// :rtype: bool
248    pub const fn orient_origin_id_match(&self, other_id: NaifId) -> bool {
249        self.orientation_id == other_id
250    }
251    /// Returns true if the ephemeris origin is equal to the provided frame
252    ///
253    /// :type other: Frame
254    /// :rtype: bool
255    pub const fn ephem_origin_match(&self, other: Self) -> bool {
256        self.ephem_origin_id_match(other.ephemeris_id)
257    }
258    /// Returns true if the orientation origin is equal to the provided frame
259    ///
260    /// :type other: Frame
261    /// :rtype: bool
262    pub const fn orient_origin_match(&self, other: Self) -> bool {
263        self.orient_origin_id_match(other.orientation_id)
264    }
265
266    /// Removes the graviational parameter and the shape information from this frame.
267    /// Use this to prevent astrodynamical computations.
268    ///
269    /// :rtype: None
270    pub fn strip(&mut self) {
271        self.mu_km3_s2 = None;
272        self.shape = None;
273    }
274
275    /// Returns the gravitational parameters of this frame, if defined
276    ///
277    /// :rtype: float
278    pub fn mu_km3_s2(&self) -> PhysicsResult<f64> {
279        self.mu_km3_s2.ok_or(PhysicsError::MissingFrameData {
280            action: "retrieving gravitational parameter",
281            data: "mu_km3_s2",
282            frame: self.into(),
283        })
284    }
285
286    /// Returns a copy of this frame with the graviational parameter set to the new value.
287    ///
288    /// :type mu_km3_s2: float
289    /// :rtype: Frame
290    pub fn with_mu_km3_s2(&self, mu_km3_s2: f64) -> Self {
291        let mut me = *self;
292        me.mu_km3_s2 = Some(mu_km3_s2);
293        me
294    }
295
296    /// Returns the mean equatorial radius in km, if defined
297    ///
298    /// :rtype: float
299    pub fn mean_equatorial_radius_km(&self) -> PhysicsResult<f64> {
300        Ok(self
301            .shape
302            .ok_or(PhysicsError::MissingFrameData {
303                action: "retrieving mean equatorial radius",
304                data: "shape",
305                frame: self.into(),
306            })?
307            .mean_equatorial_radius_km())
308    }
309
310    /// Returns the semi major radius of the tri-axial ellipoid shape of this frame, if defined
311    ///
312    /// :rtype: float
313    pub fn semi_major_radius_km(&self) -> PhysicsResult<f64> {
314        Ok(self
315            .shape
316            .ok_or(PhysicsError::MissingFrameData {
317                action: "retrieving semi major axis radius",
318                data: "shape",
319                frame: self.into(),
320            })?
321            .semi_major_equatorial_radius_km)
322    }
323
324    /// Returns the flattening ratio (unitless)
325    ///
326    /// :rtype: float
327    pub fn flattening(&self) -> PhysicsResult<f64> {
328        Ok(self
329            .shape
330            .ok_or(PhysicsError::MissingFrameData {
331                action: "retrieving flattening ratio",
332                data: "shape",
333                frame: self.into(),
334            })?
335            .flattening())
336    }
337
338    /// Returns the polar radius in km, if defined
339    ///
340    /// :rtype: float
341    pub fn polar_radius_km(&self) -> PhysicsResult<f64> {
342        Ok(self
343            .shape
344            .ok_or(PhysicsError::MissingFrameData {
345                action: "retrieving polar radius",
346                data: "shape",
347                frame: self.into(),
348            })?
349            .polar_radius_km)
350    }
351}
352
353impl fmt::Display for Frame {
354    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
355        let body_name = match celestial_name_from_id(self.ephemeris_id) {
356            Some(name) => name.to_string(),
357            None => format!("body {}", self.ephemeris_id),
358        };
359
360        let orientation_name = match orientation_name_from_id(self.orientation_id) {
361            Some(name) => name.to_string(),
362            None => format!("orientation {}", self.orientation_id),
363        };
364
365        write!(f, "{body_name} {orientation_name}")?;
366        if self.is_geodetic() {
367            write!(
368                f,
369                " (μ = {} km^3/s^2, {})",
370                self.mu_km3_s2.unwrap(),
371                self.shape.unwrap()
372            )?;
373        } else if self.is_celestial() {
374            write!(f, " (μ = {} km^3/s^2)", self.mu_km3_s2.unwrap())?;
375        }
376        Ok(())
377    }
378}
379
380impl fmt::LowerExp for Frame {
381    /// Only prints the ephemeris name
382    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
383        match celestial_name_from_id(self.ephemeris_id) {
384            Some(name) => write!(f, "{name}"),
385            None => write!(f, "{}", self.ephemeris_id),
386        }
387    }
388}
389
390impl fmt::Octal for Frame {
391    /// Only prints the orientation name
392    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
393        match orientation_name_from_id(self.orientation_id) {
394            Some(name) => write!(f, "{name}"),
395            None => write!(f, "orientation {}", self.orientation_id),
396        }
397    }
398}
399
400impl fmt::LowerHex for Frame {
401    /// Only prints the UID
402    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
403        let uid: FrameUid = self.into();
404        write!(f, "{uid}")
405    }
406}
407
408#[cfg(test)]
409mod frame_ut {
410    use super::Frame;
411    use crate::constants::frames::{EARTH_J2000, EME2000};
412
413    #[test]
414    fn format_frame() {
415        assert_eq!(format!("{EME2000}"), "Earth J2000");
416        assert_eq!(format!("{EME2000:x}"), "Earth J2000");
417        assert_eq!(format!("{EME2000:o}"), "J2000");
418        assert_eq!(format!("{EME2000:e}"), "Earth");
419    }
420
421    #[cfg(feature = "metaload")]
422    #[test]
423    fn dhall_serde() {
424        let serialized = serde_dhall::serialize(&EME2000)
425            .static_type_annotation()
426            .to_string()
427            .unwrap();
428        assert_eq!(serialized, "{ ephemeris_id = +399, mu_km3_s2 = None Double, orientation_id = +1, shape = None { polar_radius_km : Double, semi_major_equatorial_radius_km : Double, semi_minor_equatorial_radius_km : Double } }");
429        assert_eq!(
430            serde_dhall::from_str(&serialized).parse::<Frame>().unwrap(),
431            EME2000
432        );
433    }
434
435    #[test]
436    fn ccsds_name_to_frame() {
437        assert_eq!(Frame::from_name("Earth", "ICRF").unwrap(), EARTH_J2000);
438    }
439}