lox_orbits/propagators/
sgp4.rs1use glam::DVec3;
6use lox_core::coords::Cartesian;
7pub use sgp4::Elements;
8use sgp4::{Constants, ElementsError, MinutesSinceEpoch};
9use thiserror::Error;
10
11use lox_bodies::Earth;
12use lox_core::f64::consts::SECONDS_PER_MINUTE;
13use lox_frames::Teme;
14use lox_time::Time;
15use lox_time::deltas::TimeDelta;
16use lox_time::intervals::TimeInterval;
17use lox_time::time_scales::Tai;
18use lox_time::utc::UtcError;
19
20use crate::orbits::{CartesianOrbit, TrajectorError, Trajectory};
21use crate::propagators::Propagator;
22
23#[derive(Debug, Error)]
25pub enum Sgp4Error {
26 #[error(transparent)]
28 ElementsError(#[from] ElementsError),
29 #[error(transparent)]
31 TrajectoryError(#[from] TrajectorError),
32 #[error(transparent)]
34 Sgp4(#[from] sgp4::Error),
35 #[error(transparent)]
37 Utc(#[from] UtcError),
38}
39
40#[derive(Debug, Clone)]
42pub struct Sgp4 {
43 constants: Constants,
44 time: Time<Tai>,
45 step: Option<TimeDelta>,
46}
47
48impl Sgp4 {
49 pub fn new(initial_state: Elements) -> Result<Self, Sgp4Error> {
51 let time: Time<Tai> = initial_state.datetime.and_utc().into();
52 let constants = Constants::from_elements_afspc_compatibility_mode(&initial_state)?;
56 Ok(Self {
57 constants,
58 time,
59 step: None,
60 })
61 }
62
63 pub fn with_step(mut self, step: TimeDelta) -> Self {
65 self.step = Some(step);
66 self
67 }
68
69 pub fn time(&self) -> Time<Tai> {
71 self.time
72 }
73
74 pub fn state_at(&self, time: Time<Tai>) -> Result<CartesianOrbit<Tai, Earth, Teme>, Sgp4Error> {
76 let dt = (time - self.time).to_seconds().to_f64() / SECONDS_PER_MINUTE;
77 let prediction = self.constants.propagate(MinutesSinceEpoch(dt))?;
78 let position = DVec3::from_array(prediction.position) * 1e3;
80 let velocity = DVec3::from_array(prediction.velocity) * 1e3;
81 Ok(CartesianOrbit::new(
82 Cartesian::from_vecs(position, velocity),
83 time,
84 Earth,
85 Teme,
86 ))
87 }
88}
89
90impl Propagator<Tai, Earth> for Sgp4 {
91 type Frame = Teme;
92 type Error = Sgp4Error;
93
94 fn state_at(&self, time: Time<Tai>) -> Result<CartesianOrbit<Tai, Earth, Teme>, Sgp4Error> {
95 self.state_at(time)
96 }
97
98 fn propagate(
99 &self,
100 interval: TimeInterval<Tai>,
101 ) -> Result<Trajectory<Tai, Earth, Teme>, Sgp4Error> {
102 let step = self.step.unwrap_or(TimeDelta::from_seconds(60));
103 let states = interval
104 .step_by(step)
105 .map(|t| self.state_at(t))
106 .collect::<Result<Vec<_>, _>>()?;
107 Ok(Trajectory::try_new(states)?)
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use lox_frames::Icrf;
114 use lox_frames::providers::DefaultRotationProvider;
115 use lox_test_utils::assert_approx_eq;
116 use lox_time::deltas::TimeDelta;
117 use lox_time::intervals::Interval;
118
119 use super::*;
120
121 #[test]
122 fn test_sgp4_state_at() {
123 let tle = Elements::from_tle(
124 Some("ISS (ZARYA)".to_string()),
125 "1 25544U 98067A 24170.37528350 .00016566 00000+0 30244-3 0 9996".as_bytes(),
126 "2 25544 51.6410 309.3890 0010444 339.5369 107.8830 15.49495945458731".as_bytes(),
127 )
128 .unwrap();
129 let sgp4 = Sgp4::new(tle).unwrap();
130 let orbital_period = 92.821;
131 let t1 = sgp4.time() + TimeDelta::from_minutes_f64(orbital_period);
132 let s1 = sgp4.state_at(t1).unwrap();
133 let s1_icrf = s1.try_to_frame(Icrf, &DefaultRotationProvider).unwrap();
134 let k1 = s1_icrf.to_keplerian();
135 assert_approx_eq!(
136 k1.orbital_period().unwrap().to_seconds().to_f64() / SECONDS_PER_MINUTE,
137 orbital_period,
138 rtol <= 1e-4
139 );
140 }
141
142 #[test]
143 fn test_sgp4_propagate() {
144 let tle = Elements::from_tle(
145 Some("ISS (ZARYA)".to_string()),
146 "1 25544U 98067A 24170.37528350 .00016566 00000+0 30244-3 0 9996".as_bytes(),
147 "2 25544 51.6410 309.3890 0010444 339.5369 107.8830 15.49495945458731".as_bytes(),
148 )
149 .unwrap();
150 let sgp4 = Sgp4::new(tle).unwrap();
151 let t0 = sgp4.time();
152 let t1 = t0 + TimeDelta::from_minutes_f64(92.821);
153 let interval = Interval::new(t0, t1);
154 let traj = sgp4.propagate(interval).unwrap();
155 assert!(traj.states().len() > 90);
157 }
158
159 #[test]
160 fn test_sgp4_propagate_in_frame() {
161 let tle = Elements::from_tle(
162 Some("ISS (ZARYA)".to_string()),
163 "1 25544U 98067A 24170.37528350 .00016566 00000+0 30244-3 0 9996".as_bytes(),
164 "2 25544 51.6410 309.3890 0010444 339.5369 107.8830 15.49495945458731".as_bytes(),
165 )
166 .unwrap();
167 let sgp4 = Sgp4::new(tle).unwrap();
168 let t0 = sgp4.time();
169 let t1 = t0 + TimeDelta::from_minutes(10);
170 let interval = Interval::new(t0, t1);
171 let traj = sgp4
172 .propagate(interval)
173 .unwrap()
174 .into_frame(Icrf, &DefaultRotationProvider)
175 .unwrap();
176 assert!(traj.states().len() > 5);
177 assert_eq!(traj.reference_frame(), Icrf);
178 }
179}