Skip to main content

lox_bodies/
lib.rs

1// SPDX-FileCopyrightText: 2023 Angus Morrison <github@angus-morrison.com>
2// SPDX-FileCopyrightText: 2023 Helge Eichhorn <git@helgeeichhorn.de>
3//
4// SPDX-License-Identifier: MPL-2.0
5
6#![warn(missing_docs)]
7
8//! Definitions for celestial bodies, barycenters, and minor bodies in the solar system.
9//!
10//! Body properties such as gravitational parameters, radii, and rotational elements are derived
11//! from the IAU/NAIF SPICE planetary constants kernel (PCK).
12
13pub use crate::dynamic::DynOrigin;
14pub use generated::*;
15use lox_core::elements::GravitationalParameter;
16use lox_core::f64::consts::{SECONDS_PER_DAY, SECONDS_PER_JULIAN_CENTURY};
17use lox_core::units::Distance;
18use std::fmt::{Display, Formatter};
19use thiserror::Error;
20
21/// Dynamic dispatch variants of origin types.
22pub mod dynamic;
23#[allow(clippy::approx_constant)]
24mod generated;
25
26/// A NAIF ID code identifying a celestial body or barycenter.
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29#[repr(transparent)]
30pub struct NaifId(pub i32);
31
32impl Display for NaifId {
33    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
34        write!(f, "{}", self.0)
35    }
36}
37
38/// `Origin` is implemented for all bodies and barycenters.
39pub trait Origin {
40    /// Returns the NAIF ID of the origin.
41    fn id(&self) -> NaifId;
42    /// Returns the name of the origin.
43    fn name(&self) -> &'static str;
44}
45
46/// Error returned when a property is not defined for a given origin.
47#[derive(Clone, Debug, Error, Eq, PartialEq)]
48#[error("undefined property '{prop}' for origin '{origin}'")]
49pub struct UndefinedOriginPropertyError {
50    origin: String,
51    prop: String,
52}
53
54/// Equatorial-a, equatorial-b, and polar radii of a triaxial ellipsoid.
55pub type Radii = (Distance, Distance, Distance);
56
57/// Fallible accessor for the triaxial ellipsoid radii of a body.
58pub trait TryTriaxialEllipsoid: Origin {
59    /// Returns the triaxial ellipsoid radii, or an error if undefined.
60    fn try_radii(&self) -> Result<Radii, UndefinedOriginPropertyError>;
61}
62
63/// Infallible accessor for the triaxial ellipsoid radii of a body.
64pub trait TriaxialEllipsoid: Origin {
65    /// Returns the triaxial ellipsoid radii.
66    fn radii(&self) -> Radii;
67}
68
69impl<T: TriaxialEllipsoid> TryTriaxialEllipsoid for T {
70    fn try_radii(&self) -> Result<Radii, UndefinedOriginPropertyError> {
71        Ok(self.radii())
72    }
73}
74
75fn flattening(equatorial_radius: Distance, polar_radius: Distance) -> f64 {
76    let r_eq = equatorial_radius.as_f64();
77    let r_p = polar_radius.as_f64();
78    (r_eq - r_p) / r_eq
79}
80
81/// Infallible accessor for the spheroid properties of a body.
82pub trait Spheroid: TriaxialEllipsoid {
83    /// Returns the equatorial radius.
84    fn equatorial_radius(&self) -> Distance {
85        self.radii().0
86    }
87
88    /// Returns the polar radius.
89    fn polar_radius(&self) -> Distance {
90        self.radii().2
91    }
92
93    /// Returns the flattening factor.
94    fn flattening(&self) -> f64 {
95        flattening(self.equatorial_radius(), self.polar_radius())
96    }
97}
98
99/// Fallible accessor for the spheroid properties of a body.
100pub trait TrySpheroid: TryTriaxialEllipsoid {
101    /// Returns the equatorial radius, or an error if undefined.
102    fn try_equatorial_radius(&self) -> Result<Distance, UndefinedOriginPropertyError>;
103
104    /// Returns the polar radius, or an error if undefined.
105    fn try_polar_radius(&self) -> Result<Distance, UndefinedOriginPropertyError>;
106
107    /// Returns the flattening factor, or an error if undefined.
108    fn try_flattening(&self) -> Result<f64, UndefinedOriginPropertyError> {
109        self.try_radii().map(|radii| flattening(radii.0, radii.2))
110    }
111}
112
113impl<T: Spheroid> TrySpheroid for T {
114    fn try_equatorial_radius(&self) -> Result<Distance, UndefinedOriginPropertyError> {
115        Ok(self.equatorial_radius())
116    }
117
118    fn try_polar_radius(&self) -> Result<Distance, UndefinedOriginPropertyError> {
119        Ok(self.polar_radius())
120    }
121
122    fn try_flattening(&self) -> Result<f64, UndefinedOriginPropertyError> {
123        Ok(self.flattening())
124    }
125}
126
127/// Fallible accessor for the mean radius of a body.
128pub trait TryMeanRadius: Origin {
129    /// Returns the mean radius, or an error if undefined.
130    fn try_mean_radius(&self) -> Result<Distance, UndefinedOriginPropertyError>;
131}
132
133/// Infallible accessor for the mean radius of a body.
134pub trait MeanRadius: Origin {
135    /// Returns the mean radius.
136    fn mean_radius(&self) -> Distance;
137}
138
139impl<T: MeanRadius> TryMeanRadius for T {
140    fn try_mean_radius(&self) -> Result<Distance, UndefinedOriginPropertyError> {
141        Ok(self.mean_radius())
142    }
143}
144
145/// Infallible accessor for the gravitational parameter of a body.
146pub trait PointMass: Origin {
147    /// Returns the gravitational parameter.
148    fn gravitational_parameter(&self) -> GravitationalParameter;
149}
150
151/// Fallible accessor for the gravitational parameter of a body.
152pub trait TryPointMass: Origin {
153    /// Returns the gravitational parameter, or an error if undefined.
154    fn try_gravitational_parameter(
155        &self,
156    ) -> Result<GravitationalParameter, UndefinedOriginPropertyError>;
157}
158
159impl<T: PointMass> TryPointMass for T {
160    fn try_gravitational_parameter(
161        &self,
162    ) -> Result<GravitationalParameter, UndefinedOriginPropertyError> {
163        Ok(self.gravitational_parameter())
164    }
165}
166
167#[derive(Clone, Copy, Debug)]
168pub(crate) enum RotationalElementType {
169    RightAscension,
170    Declination,
171    Rotation,
172}
173
174impl RotationalElementType {
175    fn sincos(&self, val: f64) -> f64 {
176        match self {
177            RotationalElementType::Declination => val.cos(),
178            _ => val.sin(),
179        }
180    }
181
182    fn sincos_dot(&self, val: f64) -> f64 {
183        match self {
184            RotationalElementType::Declination => val.sin(),
185            _ => val.cos(),
186        }
187    }
188
189    fn sign(&self) -> f64 {
190        match self {
191            RotationalElementType::Declination => -1.0,
192            _ => 1.0,
193        }
194    }
195
196    fn dt(&self) -> f64 {
197        match self {
198            RotationalElementType::Rotation => SECONDS_PER_DAY,
199            _ => SECONDS_PER_JULIAN_CENTURY,
200        }
201    }
202}
203
204pub(crate) struct RotationalElement<const N: usize> {
205    typ: RotationalElementType,
206    c0: f64,
207    c1: f64,
208    c2: f64,
209    c: [f64; N],
210    theta0: [f64; N],
211    theta1: [f64; N],
212}
213
214impl<const N: usize> RotationalElement<N> {
215    fn trig_term(&self, t: f64) -> f64 {
216        self.c
217            .iter()
218            .zip(self.theta0.iter())
219            .zip(self.theta1.iter())
220            .map(|((&c, &theta0), &theta1)| {
221                c * self
222                    .typ
223                    .sincos(theta0 + theta1 * t / SECONDS_PER_JULIAN_CENTURY)
224            })
225            .sum()
226    }
227
228    fn trig_term_dot(&self, t: f64) -> f64 {
229        self.c
230            .iter()
231            .zip(self.theta0.iter())
232            .zip(self.theta1.iter())
233            .map(|((&c, &theta0), &theta1)| {
234                c * theta1 / SECONDS_PER_JULIAN_CENTURY
235                    * self
236                        .typ
237                        .sincos_dot(theta0 + theta1 * t / SECONDS_PER_JULIAN_CENTURY)
238            })
239            .sum()
240    }
241
242    fn angle(&self, t: f64) -> f64 {
243        self.c0
244            + self.c1 * t / self.typ.dt()
245            + self.c2 * t.powi(2) / self.typ.dt().powi(2)
246            + self.trig_term(t)
247    }
248
249    fn angle_dot(&self, t: f64) -> f64 {
250        self.c1 / self.typ.dt()
251            + 2.0 * self.c2 * t / self.typ.dt().powi(2)
252            + self.typ.sign() * self.trig_term_dot(t)
253    }
254}
255
256/// Right ascension, declination, and prime meridian angles in radians.
257pub type Elements = (f64, f64, f64);
258
259/// Infallible accessor for the rotational elements of a body.
260pub trait RotationalElements: Origin {
261    /// Returns the right ascension, declination, and prime meridian at epoch `t` (seconds since J2000 TDB).
262    fn rotational_elements(&self, t: f64) -> Elements;
263
264    /// Returns the rates of the rotational elements at epoch `t`.
265    fn rotational_element_rates(&self, t: f64) -> Elements;
266
267    /// Returns the right ascension at epoch `t`.
268    fn right_ascension(&self, t: f64) -> f64 {
269        self.rotational_elements(t).0
270    }
271
272    /// Returns the right ascension rate at epoch `t`.
273    fn right_ascension_rate(&self, t: f64) -> f64 {
274        self.rotational_element_rates(t).0
275    }
276
277    /// Returns the declination at epoch `t`.
278    fn declination(&self, t: f64) -> f64 {
279        self.rotational_elements(t).1
280    }
281
282    /// Returns the declination rate at epoch `t`.
283    fn declination_rate(&self, t: f64) -> f64 {
284        self.rotational_element_rates(t).1
285    }
286
287    /// Returns the prime meridian angle at epoch `t`.
288    fn rotation_angle(&self, t: f64) -> f64 {
289        self.rotational_elements(t).2
290    }
291
292    /// Returns the prime meridian rotation rate at epoch `t`.
293    fn rotation_rate(&self, t: f64) -> f64 {
294        self.rotational_element_rates(t).2
295    }
296}
297
298/// Fallible accessor for the rotational elements of a body.
299pub trait TryRotationalElements: Origin {
300    /// Returns the rotational elements at epoch `t`, or an error if undefined.
301    fn try_rotational_elements(&self, t: f64) -> Result<Elements, UndefinedOriginPropertyError>;
302
303    /// Returns the rotational element rates at epoch `t`, or an error if undefined.
304    fn try_rotational_element_rates(
305        &self,
306        t: f64,
307    ) -> Result<Elements, UndefinedOriginPropertyError>;
308
309    /// Returns the right ascension at epoch `t`, or an error if undefined.
310    fn try_right_ascension(&self, t: f64) -> Result<f64, UndefinedOriginPropertyError> {
311        self.try_rotational_elements(t).map(|r| r.0)
312    }
313
314    /// Returns the right ascension rate at epoch `t`, or an error if undefined.
315    fn try_right_ascension_rate(&self, t: f64) -> Result<f64, UndefinedOriginPropertyError> {
316        self.try_rotational_element_rates(t).map(|r| r.0)
317    }
318
319    /// Returns the declination at epoch `t`, or an error if undefined.
320    fn try_declination(&self, t: f64) -> Result<f64, UndefinedOriginPropertyError> {
321        self.try_rotational_elements(t).map(|r| r.1)
322    }
323
324    /// Returns the declination rate at epoch `t`, or an error if undefined.
325    fn try_declination_rate(&self, t: f64) -> Result<f64, UndefinedOriginPropertyError> {
326        self.try_rotational_element_rates(t).map(|r| r.1)
327    }
328
329    /// Returns the prime meridian angle at epoch `t`, or an error if undefined.
330    fn try_rotation_angle(&self, t: f64) -> Result<f64, UndefinedOriginPropertyError> {
331        self.try_rotational_elements(t).map(|r| r.2)
332    }
333
334    /// Returns the prime meridian rotation rate at epoch `t`, or an error if undefined.
335    fn try_rotation_rate(&self, t: f64) -> Result<f64, UndefinedOriginPropertyError> {
336        self.try_rotational_element_rates(t).map(|r| r.2)
337    }
338}
339
340impl<T: RotationalElements> TryRotationalElements for T {
341    fn try_rotational_elements(&self, t: f64) -> Result<Elements, UndefinedOriginPropertyError> {
342        Ok(self.rotational_elements(t))
343    }
344
345    fn try_rotational_element_rates(
346        &self,
347        t: f64,
348    ) -> Result<Elements, UndefinedOriginPropertyError> {
349        Ok(self.rotational_element_rates(t))
350    }
351}
352
353/// Fallible accessor for the J2 zonal harmonic coefficient.
354pub trait TryJ2 {
355    /// Returns the J2 coefficient, or an error if undefined.
356    fn try_j2(&self) -> Result<f64, UndefinedOriginPropertyError>;
357}
358
359/// Infallible accessor for the J2 zonal harmonic coefficient.
360pub trait J2 {
361    /// Returns the J2 coefficient.
362    fn j2(&self) -> f64;
363}
364
365impl<T> TryJ2 for T
366where
367    T: J2,
368{
369    fn try_j2(&self) -> Result<f64, UndefinedOriginPropertyError> {
370        Ok(self.j2())
371    }
372}
373
374impl J2 for Earth {
375    fn j2(&self) -> f64 {
376        1.08262668e-3
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use lox_test_utils::assert_approx_eq;
383
384    use super::*;
385
386    // Jupiter is manually redefined here using known data. This avoids a dependency on the
387    // correctness of the PCK parser to test RotationalElements, and prevents compiler errors
388    // when generated files are malformed or deleted in preparation for regeneration.
389    #[derive(Debug, Copy, Clone, Eq, PartialEq)]
390    pub struct Jupiter;
391
392    impl Origin for Jupiter {
393        fn id(&self) -> NaifId {
394            NaifId(599)
395        }
396
397        fn name(&self) -> &'static str {
398            "Jupiter"
399        }
400    }
401    #[derive(Debug, Copy, Clone, Eq, PartialEq)]
402    pub struct Rupert;
403
404    impl Origin for Rupert {
405        fn id(&self) -> NaifId {
406            NaifId(1099)
407        }
408
409        fn name(&self) -> &'static str {
410            "Persephone/Rupert"
411        }
412    }
413
414    #[test]
415    fn test_body() {
416        let body = Jupiter;
417        let id = body.id().0;
418        let name = body.name();
419        assert_eq!(id, 599);
420        assert_eq!(name, "Jupiter");
421
422        let body = Rupert;
423        let id = body.id().0;
424        let name = body.name();
425        assert_eq!(id, 1099);
426        assert_eq!(name, "Persephone/Rupert");
427    }
428
429    const RIGHT_ASCENSION_JUPITER: RotationalElement<15> = RotationalElement {
430        typ: RotationalElementType::RightAscension,
431        c0: 4.6784701644349695f64,
432        c1: -0.00011342894808711148f64,
433        c2: 0f64,
434        c: [
435            0f64,
436            0f64,
437            0f64,
438            0f64,
439            0f64,
440            0f64,
441            0f64,
442            0f64,
443            0f64,
444            0f64,
445            0.0000020420352248333656f64,
446            0.000016371188383706813f64,
447            0.000024993114888558796f64,
448            0.0000005235987755982989f64,
449            0.00003752457891787809f64,
450        ],
451        theta0: [
452            1.2796754075622423f64,
453            0.42970006184100396f64,
454            4.9549897464119015f64,
455            6.2098814785958245f64,
456            2.092649773141201f64,
457            4.010766621082969f64,
458            6.147922290150026f64,
459            1.9783307071355725f64,
460            2.5593508151244846f64,
461            0.8594001236820079f64,
462            1.734171606432425f64,
463            3.0699533280603655f64,
464            5.241627996900319f64,
465            1.9898901100379935f64,
466            0.864134346731335f64,
467        ],
468        theta1: [
469            1596.503281347521f64,
470            787.7927551311844f64,
471            84.66068602648895f64,
472            20.792107379008446f64,
473            4.574507969477138f64,
474            1.1222467090323538f64,
475            41.58421475801689f64,
476            105.9414855960558f64,
477            3193.006562695042f64,
478            1575.5855102623689f64,
479            84.65553032387855f64,
480            20.80363527871787f64,
481            4.582318317879813f64,
482            105.94580703128374f64,
483            1.1222467090323538f64,
484        ],
485    };
486
487    const DECLINATION_JUPITER: RotationalElement<15> = RotationalElement {
488        typ: RotationalElementType::Declination,
489        c0: 1.1256553894213766f64,
490        c1: 0.00004211479485062318f64,
491        c2: 0f64,
492        c: [
493            0f64,
494            0f64,
495            0f64,
496            0f64,
497            0f64,
498            0f64,
499            0f64,
500            0f64,
501            0f64,
502            0f64,
503            0.0000008726646259971648f64,
504            0.000007051130178057092f64,
505            0.000010768681484805013f64,
506            -0.00000022689280275926283f64,
507            0.00001616174887346749f64,
508        ],
509        theta0: [
510            1.2796754075622423f64,
511            0.42970006184100396f64,
512            4.9549897464119015f64,
513            6.2098814785958245f64,
514            2.092649773141201f64,
515            4.010766621082969f64,
516            6.147922290150026f64,
517            1.9783307071355725f64,
518            2.5593508151244846f64,
519            0.8594001236820079f64,
520            1.734171606432425f64,
521            3.0699533280603655f64,
522            5.241627996900319f64,
523            1.9898901100379935f64,
524            0.864134346731335f64,
525        ],
526        theta1: [
527            1596.503281347521f64,
528            787.7927551311844f64,
529            84.66068602648895f64,
530            20.792107379008446f64,
531            4.574507969477138f64,
532            1.1222467090323538f64,
533            41.58421475801689f64,
534            105.9414855960558f64,
535            3193.006562695042f64,
536            1575.5855102623689f64,
537            84.65553032387855f64,
538            20.80363527871787f64,
539            4.582318317879813f64,
540            105.94580703128374f64,
541            1.1222467090323538f64,
542        ],
543    };
544
545    const ROTATION_JUPITER: RotationalElement<0> = RotationalElement {
546        typ: RotationalElementType::Rotation,
547        c0: 4.973315703557842f64,
548        c1: 15.193719457141356f64,
549        c2: 0f64,
550        c: [],
551        theta0: [],
552        theta1: [],
553    };
554
555    impl RotationalElements for Jupiter {
556        fn rotational_elements(&self, t: f64) -> Elements {
557            (
558                RIGHT_ASCENSION_JUPITER.angle(t),
559                DECLINATION_JUPITER.angle(t),
560                ROTATION_JUPITER.angle(t),
561            )
562        }
563
564        fn rotational_element_rates(&self, t: f64) -> Elements {
565            (
566                RIGHT_ASCENSION_JUPITER.angle_dot(t),
567                DECLINATION_JUPITER.angle_dot(t),
568                ROTATION_JUPITER.angle_dot(t),
569            )
570        }
571    }
572
573    #[test]
574    fn test_rotational_elements_right_ascension() {
575        assert_approx_eq!(
576            Jupiter.right_ascension(0.0),
577            4.678480799964803,
578            rtol <= 1e-8
579        );
580    }
581
582    #[test]
583    fn test_rotational_elements_right_ascension_dot() {
584        assert_approx_eq!(
585            Jupiter.right_ascension_rate(0.0),
586            -1.3266588500099516e-13,
587            rtol <= 1e-8
588        );
589    }
590
591    #[test]
592    fn test_rotational_elements_declination() {
593        assert_approx_eq!(Jupiter.declination(0.0), 1.1256642372977634, rtol <= 1e-8);
594    }
595
596    #[test]
597    fn test_rotational_elements_declination_dot() {
598        assert_approx_eq!(
599            Jupiter.declination_rate(0.0),
600            3.004482367136341e-15,
601            rtol <= 1e-8
602        );
603    }
604
605    #[test]
606    fn test_rotational_elements_prime_meridian() {
607        assert_approx_eq!(Jupiter.rotation_angle(0.0), 4.973315703557842, rtol <= 1e-8);
608    }
609
610    #[test]
611    fn test_rotational_elements_prime_meridian_dot() {
612        assert_approx_eq!(
613            Jupiter.rotation_rate(0.0),
614            0.00017585323445765458,
615            rtol <= 1e-8
616        );
617    }
618
619    #[test]
620    fn test_rotational_elements_error() {
621        assert!(DynOrigin::Mundilfari.try_rotational_elements(0.0).is_err());
622        assert!(
623            DynOrigin::Mundilfari
624                .try_rotational_element_rates(0.0)
625                .is_err()
626        );
627    }
628}