sidereon_core/astro/propagator/
driver.rs1use crate::astro::constants::{J2_EARTH, MU_EARTH, RE_EARTH};
18use crate::astro::error::PropagationError;
19use crate::astro::forces::DragParameters;
20use crate::astro::propagator::api::{IntegratorOptions, PropagationContext};
21use crate::astro::propagator::numerical::{ForceModelKind, IntegratorKind, StatePropagator};
22use crate::astro::state::CartesianState;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32pub enum PropagationForceModel {
33 #[default]
35 TwoBody,
36 TwoBodyJ2,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq)]
47pub struct PropagationConfig {
48 pub initial: CartesianState,
50 pub force_model: PropagationForceModel,
52 pub mu_km3_s2: Option<f64>,
55 pub integrator: IntegratorKind,
57 pub options: IntegratorOptions,
59 pub drag: Option<DragParameters>,
61}
62
63impl PropagationConfig {
64 pub fn new(epoch_tdb_seconds: f64, position_km: [f64; 3], velocity_km_s: [f64; 3]) -> Self {
69 Self {
70 initial: CartesianState::new(epoch_tdb_seconds, position_km, velocity_km_s),
71 force_model: PropagationForceModel::TwoBody,
72 mu_km3_s2: None,
73 integrator: IntegratorKind::Dp54,
74 options: IntegratorOptions::default(),
75 drag: None,
76 }
77 }
78
79 pub fn with_drag(mut self, drag: DragParameters) -> Self {
81 self.drag = Some(drag);
82 self
83 }
84
85 pub fn gravitational_parameter(&self) -> f64 {
88 self.mu_km3_s2.unwrap_or(MU_EARTH)
89 }
90
91 pub fn force_model_kind(&self) -> ForceModelKind {
96 let mu_km3_s2 = self.gravitational_parameter();
97 match self.force_model {
98 PropagationForceModel::TwoBody => ForceModelKind::TwoBody { mu_km3_s2 },
99 PropagationForceModel::TwoBodyJ2 => ForceModelKind::TwoBodyJ2 {
100 mu_km3_s2,
101 re_km: RE_EARTH,
102 j2: J2_EARTH,
103 },
104 }
105 }
106
107 pub fn to_propagator(&self) -> StatePropagator {
111 StatePropagator {
112 initial: self.initial,
113 force_model: self.force_model_kind(),
114 integrator: self.integrator,
115 options: self.options,
116 drag: self.drag,
117 space_weather: None,
118 }
119 }
120}
121
122pub fn propagate_states(
131 config: &PropagationConfig,
132 epochs_tdb_seconds: &[f64],
133) -> Result<Vec<CartesianState>, PropagationError> {
134 config.to_propagator().ephemeris(epochs_tdb_seconds)
135}
136
137pub fn propagate_states_with_context(
143 config: &PropagationConfig,
144 epochs_tdb_seconds: &[f64],
145 ctx: &PropagationContext,
146) -> Result<Vec<CartesianState>, PropagationError> {
147 config
148 .to_propagator()
149 .ephemeris_with_context(epochs_tdb_seconds, ctx)
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 fn circular_state() -> (f64, [f64; 3], [f64; 3]) {
157 let r: f64 = 7000.0;
158 let v = (MU_EARTH / r).sqrt();
159 (0.0, [r, 0.0, 0.0], [0.0, v, 0.0])
160 }
161
162 fn assert_states_bit_for_bit(left: &[CartesianState], right: &[CartesianState]) {
163 assert_eq!(left.len(), right.len(), "state-count mismatch");
164 for (a, b) in left.iter().zip(right.iter()) {
165 assert_eq!(a.epoch_tdb_seconds.to_bits(), b.epoch_tdb_seconds.to_bits());
166 for axis in 0..3 {
167 assert_eq!(
168 a.position_array()[axis].to_bits(),
169 b.position_array()[axis].to_bits(),
170 "position axis {axis}"
171 );
172 assert_eq!(
173 a.velocity_array()[axis].to_bits(),
174 b.velocity_array()[axis].to_bits(),
175 "velocity axis {axis}"
176 );
177 }
178 }
179 }
180
181 #[test]
182 fn config_defaults_come_from_core_constants() {
183 let (epoch, pos, vel) = circular_state();
184 let config = PropagationConfig::new(epoch, pos, vel);
185
186 assert_eq!(config.force_model, PropagationForceModel::TwoBody);
187 assert_eq!(config.mu_km3_s2, None);
188 assert_eq!(
189 config.gravitational_parameter().to_bits(),
190 MU_EARTH.to_bits()
191 );
192 assert_eq!(config.integrator, IntegratorKind::Dp54);
193 assert_eq!(config.options, IntegratorOptions::default());
194 assert_eq!(
195 config.force_model_kind(),
196 ForceModelKind::TwoBody {
197 mu_km3_s2: MU_EARTH
198 }
199 );
200 }
201
202 #[test]
203 fn force_model_kind_composes_j2_from_canonical_constants() {
204 let (epoch, pos, vel) = circular_state();
205 let config = PropagationConfig {
206 force_model: PropagationForceModel::TwoBodyJ2,
207 ..PropagationConfig::new(epoch, pos, vel)
208 };
209
210 assert_eq!(
211 config.force_model_kind(),
212 ForceModelKind::TwoBodyJ2 {
213 mu_km3_s2: MU_EARTH,
214 re_km: RE_EARTH,
215 j2: J2_EARTH,
216 }
217 );
218 }
219
220 #[test]
221 fn driver_two_body_default_matches_manual_composition_bit_for_bit() {
222 let (epoch, pos, vel) = circular_state();
224 let config = PropagationConfig::new(epoch, pos, vel);
225 let epochs = [0.0, 600.0, 1800.0, crate::constants::SECONDS_PER_HOUR];
226 let via_driver = propagate_states(&config, &epochs).expect("driver propagation");
227
228 let via_manual = StatePropagator {
231 initial: CartesianState::new(epoch, pos, vel),
232 force_model: ForceModelKind::TwoBody {
233 mu_km3_s2: MU_EARTH,
234 },
235 integrator: IntegratorKind::Dp54,
236 options: IntegratorOptions {
237 abs_tol: 1.0e-9,
238 rel_tol: 1.0e-12,
239 initial_step: 60.0,
240 min_step: 1.0e-6,
241 max_step: crate::constants::SECONDS_PER_HOUR,
242 max_steps: 1_000_000,
243 dense_output: false,
244 },
245 drag: None,
246 space_weather: None,
247 }
248 .ephemeris(&epochs)
249 .expect("manual propagation");
250
251 assert_states_bit_for_bit(&via_driver, &via_manual);
252 }
253
254 #[test]
255 fn driver_two_body_j2_custom_mu_rk4_matches_manual_composition_bit_for_bit() {
256 let (epoch, pos, vel) = circular_state();
257 let mu = 398_600.5;
258 let options = IntegratorOptions {
259 abs_tol: 1.0e-11,
260 rel_tol: 1.0e-13,
261 initial_step: 30.0,
262 min_step: 1.0e-5,
263 max_step: 120.0,
264 max_steps: 500_000,
265 dense_output: false,
266 };
267 let config = PropagationConfig {
268 force_model: PropagationForceModel::TwoBodyJ2,
269 mu_km3_s2: Some(mu),
270 integrator: IntegratorKind::Rk4,
271 options,
272 ..PropagationConfig::new(epoch, pos, vel)
273 };
274 let epochs = [0.0, 300.0, 900.0];
275 let via_driver = propagate_states(&config, &epochs).expect("driver propagation");
276
277 let via_manual = StatePropagator {
278 initial: CartesianState::new(epoch, pos, vel),
279 force_model: ForceModelKind::TwoBodyJ2 {
280 mu_km3_s2: mu,
281 re_km: RE_EARTH,
282 j2: J2_EARTH,
283 },
284 integrator: IntegratorKind::Rk4,
285 options,
286 drag: None,
287 space_weather: None,
288 }
289 .ephemeris(&epochs)
290 .expect("manual propagation");
291
292 assert_states_bit_for_bit(&via_driver, &via_manual);
293 }
294
295 #[test]
296 fn driver_surfaces_the_integrator_error_unchanged() {
297 let (epoch, pos, vel) = circular_state();
300 let config = PropagationConfig {
301 options: IntegratorOptions {
302 initial_step: 0.0,
303 ..IntegratorOptions::default()
304 },
305 ..PropagationConfig::new(epoch, pos, vel)
306 };
307
308 let err = propagate_states(&config, &[0.0, 60.0]).expect_err("non-positive step rejected");
309 match err {
310 PropagationError::InvalidInput(message) => {
311 assert!(message.contains("initial_step"), "{message}");
312 assert!(message.contains("not positive"), "{message}");
313 }
314 other => panic!("expected invalid-input error, got {other:?}"),
315 }
316 }
317}