Skip to main content

lox_orbits/
propagators.rs

1// SPDX-FileCopyrightText: 2024 Helge Eichhorn <git@helgeeichhorn.de>
2//
3// SPDX-License-Identifier: MPL-2.0
4
5use lox_bodies::Origin;
6use lox_frames::ReferenceFrame;
7use lox_time::Time;
8use lox_time::intervals::TimeInterval;
9use lox_time::time_scales::{DynTimeScale, TimeScale};
10
11use crate::orbits::{CartesianOrbit, DynTrajectory, TrajectorError, Trajectory};
12
13use self::numerical::{DynJ2Propagator, J2Error};
14use self::semi_analytical::{DynVallado, ValladoError};
15use self::sgp4::{Sgp4, Sgp4Error};
16
17/// Numerical orbit propagators (e.g. J2 perturbation via ODE integration).
18pub mod numerical;
19/// Semi-analytical orbit propagators (e.g. Vallado universal variable method).
20pub mod semi_analytical;
21/// SGP4 orbit propagator for TLE-based satellite prediction.
22pub mod sgp4;
23mod stumpff;
24
25/// Common interface for orbit propagators.
26pub trait Propagator<T, O>
27where
28    T: TimeScale + Copy,
29    O: Origin + Copy,
30{
31    /// The propagator's native reference frame.
32    type Frame: ReferenceFrame + Copy;
33    /// The error type returned by propagation methods.
34    type Error: std::error::Error + 'static;
35
36    /// Evaluate the state at a single time.
37    fn state_at(&self, time: Time<T>) -> Result<CartesianOrbit<T, O, Self::Frame>, Self::Error>;
38
39    /// Propagate over the given interval in the native frame.
40    /// The propagator chooses the time steps.
41    fn propagate(
42        &self,
43        interval: TimeInterval<T>,
44    ) -> Result<Trajectory<T, O, Self::Frame>, Self::Error>;
45
46    /// Propagate to an iterable of caller-chosen times.
47    fn propagate_to(
48        &self,
49        times: impl IntoIterator<Item = Time<T>>,
50    ) -> Result<Trajectory<T, O, Self::Frame>, Self::Error>
51    where
52        Self::Error: From<TrajectorError>,
53    {
54        let states: Result<Vec<_>, _> = times.into_iter().map(|t| self.state_at(t)).collect();
55        Ok(Trajectory::try_new(states?)?)
56    }
57}
58
59/// An orbit source that can be propagated over a time interval to produce
60/// a [`DynTrajectory`].
61///
62/// Wraps the concrete propagator types (SGP4, Vallado, J2) or a pre-computed
63/// trajectory.
64#[derive(Debug, Clone)]
65pub enum OrbitSource {
66    /// SGP4 propagator initialized from a TLE.
67    Sgp4(Sgp4),
68    /// Vallado universal-variable Keplerian propagator.
69    Vallado(DynVallado),
70    /// J2-perturbed numerical propagator.
71    J2(DynJ2Propagator),
72    /// Pre-computed trajectory used as-is.
73    Trajectory(DynTrajectory),
74}
75
76/// Errors that can occur when propagating an [`OrbitSource`].
77#[derive(Debug, thiserror::Error)]
78pub enum PropagateError {
79    /// SGP4 propagation error.
80    #[error(transparent)]
81    Sgp4(#[from] Sgp4Error),
82    /// Vallado propagation error.
83    #[error(transparent)]
84    Vallado(#[from] ValladoError),
85    /// J2 numerical propagation error.
86    #[error(transparent)]
87    J2(#[from] J2Error),
88}
89
90impl OrbitSource {
91    /// Propagate the orbit source over the given interval, returning a
92    /// [`DynTrajectory`] in the source's native reference frame.
93    pub fn propagate(
94        &self,
95        interval: TimeInterval<DynTimeScale>,
96    ) -> Result<DynTrajectory, PropagateError> {
97        match self {
98            Self::Sgp4(sgp4) => {
99                let tai_interval = TimeInterval::new(
100                    interval.start().to_scale(lox_time::time_scales::Tai),
101                    interval.end().to_scale(lox_time::time_scales::Tai),
102                );
103                let traj = Propagator::propagate(sgp4, tai_interval)?;
104                Ok(traj.into_dyn())
105            }
106            Self::Vallado(v) => Ok(Propagator::propagate(v, interval)?),
107            Self::J2(j2) => Ok(Propagator::propagate(j2, interval)?),
108            Self::Trajectory(t) => Ok(t.clone()),
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use lox_bodies::DynOrigin;
117    use lox_frames::DynFrame;
118    use lox_time::time_scales::DynTimeScale;
119
120    fn make_trajectory() -> DynTrajectory {
121        DynTrajectory::from_csv_dyn(
122            &lox_test_utils::read_data_file("trajectory_lunar.csv"),
123            DynOrigin::Earth,
124            DynFrame::Icrf,
125        )
126        .unwrap()
127    }
128
129    #[test]
130    fn test_orbit_source_trajectory_propagate() {
131        let traj = make_trajectory();
132        let interval = TimeInterval::new(
133            traj.start_time().to_scale(DynTimeScale::Tai),
134            traj.end_time().to_scale(DynTimeScale::Tai),
135        );
136        let source = OrbitSource::Trajectory(traj.clone());
137        let result = source.propagate(interval).unwrap();
138        assert_eq!(result.states().len(), traj.states().len());
139    }
140}