1use 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#[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 pub mu_km3_s2: Option<f64>,
52 pub shape: Option<Ellipsoid>,
54}
55
56impl Frame {
57 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 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 pub fn with_ellipsoid(mut self, shape: Ellipsoid) -> Self {
91 self.shape = Some(shape);
92 self
93 }
94
95 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 #[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 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 #[getter]
157 fn get_ephemeris_id(&self) -> PyResult<NaifId> {
158 Ok(self.ephemeris_id)
159 }
160 #[setter]
162 fn set_ephemeris_id(&mut self, ephemeris_id: NaifId) -> PyResult<()> {
163 self.ephemeris_id = ephemeris_id;
164 Ok(())
165 }
166 #[getter]
168 fn get_orientation_id(&self) -> PyResult<NaifId> {
169 Ok(self.orientation_id)
170 }
171 #[setter]
173 fn set_orientation_id(&mut self, orientation_id: NaifId) -> PyResult<()> {
174 self.orientation_id = orientation_id;
175 Ok(())
176 }
177 #[getter]
179 fn get_mu_km3_s2(&self) -> PyResult<Option<f64>> {
180 Ok(self.mu_km3_s2)
181 }
182 #[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 #[getter]
190 fn get_shape(&self) -> PyResult<Option<Ellipsoid>> {
191 Ok(self.shape)
192 }
193 #[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 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 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 pub const fn is_celestial(&self) -> bool {
227 self.mu_km3_s2.is_some()
228 }
229
230 pub const fn is_geodetic(&self) -> bool {
234 self.mu_km3_s2.is_some() && self.shape.is_some()
235 }
236
237 pub const fn ephem_origin_id_match(&self, other_id: NaifId) -> bool {
242 self.ephemeris_id == other_id
243 }
244 pub const fn orient_origin_id_match(&self, other_id: NaifId) -> bool {
249 self.orientation_id == other_id
250 }
251 pub const fn ephem_origin_match(&self, other: Self) -> bool {
256 self.ephem_origin_id_match(other.ephemeris_id)
257 }
258 pub const fn orient_origin_match(&self, other: Self) -> bool {
263 self.orient_origin_id_match(other.orientation_id)
264 }
265
266 pub fn strip(&mut self) {
271 self.mu_km3_s2 = None;
272 self.shape = None;
273 }
274
275 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 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 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 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 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 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 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 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 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}