nyx_space/dynamics/sequence/
mod.rs1use std::collections::BTreeMap;
20use std::sync::Arc;
21
22use anise::prelude::Almanac;
23use hifitime::{Epoch, Unit};
24use indexmap::IndexMap;
25use log::{debug, info};
26#[cfg(feature = "python")]
27use pyo3::prelude::*;
28use serde::{Deserialize, Serialize};
29use serde_dhall::{SimpleType, StaticType};
30use snafu::ResultExt;
31use std::collections::HashMap;
32
33use crate::dynamics::guidance::{Kluever, Ruggiero};
34use crate::dynamics::{GravityField, OrbitalDynamics};
35use crate::dynamics::{SpacecraftDynamics, guidance::Thruster};
36use crate::errors::{FromAlmanacSnafu, FromPropSnafu};
37use crate::io::gravity::GravityFieldData;
38use crate::md::Trajectory;
39use crate::propagators::Propagator;
40use crate::{NyxError, Spacecraft, State};
41
42mod config;
43mod discrete_event;
44
45pub use config::*;
46pub use discrete_event::*;
47
48#[derive(Clone, Debug, Default, Serialize, Deserialize)]
49#[cfg_attr(feature = "python", pyclass(from_py_object))]
50pub struct SpacecraftSequence {
51 #[serde(serialize_with = "map_as_pairs", deserialize_with = "pairs_as_map")]
52 pub seq: BTreeMap<Epoch, Phase>,
53 #[serde(with = "indexmap::map::serde_seq")]
54 pub thruster_sets: IndexMap<String, Thruster>,
55 #[serde(with = "indexmap::map::serde_seq")]
56 pub propagators: IndexMap<String, PropagatorConfig>,
57 #[serde(skip)]
58 prop_setups: IndexMap<String, Propagator<SpacecraftDynamics>>,
59}
60
61impl SpacecraftSequence {
62 pub fn validate(&self) -> Result<(), String> {
63 if let Some((_, Phase::Activity { .. })) = self.seq.iter().last() {
65 return Err("final phase must be a Terminate".into());
66 }
67
68 for (epoch, phase) in &self.seq {
70 if let Phase::Activity {
71 name: _,
72 propagator,
73 guidance,
74 on_entry: _,
75 disabled: _,
76 } = phase
77 {
78 if self.propagators.get(propagator).is_none() {
80 return Err(format!("{epoch}: no propagator named `{propagator}`"));
81 }
82 if let Some(guidance) = guidance {
83 let thruster = &guidance.thruster_model;
84 if self.thruster_sets.get(thruster).is_none() {
85 return Err(format!("{epoch}: no thruster set named {thruster}"));
86 }
87 }
88 }
89 }
90
91 Ok(())
92 }
93
94 pub fn setup(&mut self, almanac: Arc<Almanac>) -> Result<(), String> {
96 self.validate()?;
98
99 for phase in self.seq.values() {
100 if let Phase::Activity {
101 name: _,
102 propagator,
103 guidance: _,
104 on_entry: _,
105 disabled,
106 } = phase
107 && !disabled
108 && self.prop_setups.get(propagator).is_none()
109 {
110 let cfg = &self.propagators[propagator];
113 let mut orbital_dyn = OrbitalDynamics::two_body();
115 if let Some(point_masses) = &cfg.accel_models.point_masses {
116 orbital_dyn.accel_models.push(point_masses.clone());
117 }
118 if let Some((gravity_cfg, frame_uid)) = &cfg.accel_models.gravity_field {
119 let grav_data = GravityFieldData::from_config(gravity_cfg.clone())
120 .map_err(|e| e.to_string())?;
121 let compute_frame =
122 almanac.frame_info(*frame_uid).map_err(|e| e.to_string())?;
123 let gravity_field = GravityField::from_stor(compute_frame, grav_data);
124 orbital_dyn.accel_models.push(gravity_field);
125 }
126 let mut sc_dyn = SpacecraftDynamics::new(orbital_dyn);
128
129 if let Some(srp) = &cfg.force_models.solar_pressure {
130 sc_dyn.force_models.push(srp.clone());
131 }
132
133 if let Some(drag) = &cfg.force_models.drag {
134 sc_dyn.force_models.push(drag.clone());
135 }
136
137 let setup = Propagator::new(sc_dyn, cfg.method, cfg.options);
139
140 self.prop_setups.insert(propagator.clone(), setup);
141 debug!("built `{propagator}`");
142 }
143 }
144
145 Ok(())
146 }
147
148 pub fn propagate(
151 &self,
152 mut state: Spacecraft,
153 until_phase: Option<String>,
154 almanac: Arc<Almanac>,
155 ) -> Result<Vec<Trajectory>, NyxError> {
156 let tick = Epoch::now().unwrap();
157 let mut phase_iterator = self.seq.range(state.epoch()..).peekable();
158
159 let mut trajs = Vec::with_capacity(self.seq.len());
160
161 while let Some((epoch, phase)) = phase_iterator.next() {
162 match phase {
163 Phase::Terminate => {
164 let tock = (Epoch::now().unwrap() - tick).round(Unit::Millisecond * 1);
165 info!("[{epoch}] plan completed in {tock}");
166 return Ok(trajs);
167 }
168 Phase::Activity {
169 name,
170 propagator,
171 guidance,
172 on_entry,
173 disabled,
174 } => {
175 if let Some(ref target) = until_phase
177 && target == name
178 {
179 return Ok(trajs);
180 }
181
182 if *disabled {
183 info!("[{epoch}] skipping disabled {name}");
184 } else {
185 info!("[{epoch}] executing {name}");
186 if let Some(discrete_event) = on_entry {
187 match &**discrete_event {
188 DiscreteEvent::FrameSwap { new_frame } => {
189 if !new_frame.orient_origin_match(state.orbit.frame)
190 || !new_frame.ephem_origin_match(state.orbit.frame)
191 {
192 state = state.with_orbit(
193 almanac
194 .translate_to(state.orbit, *new_frame, None)
195 .map_err(|source| {
196 anise::errors::AlmanacError::Ephemeris {
197 action: "central body swap",
198 source: Box::new(source),
199 }
200 })
201 .context(FromAlmanacSnafu {
202 action: "central body swap",
203 })?,
204 );
205 info!("[{epoch}] central body swapped to {new_frame}");
206 }
207 }
208 DiscreteEvent::Staging {
209 impulsive_maneuver,
210 decrement_properties,
211 } => {
212 if let Some(mnvr) = impulsive_maneuver {
213 info!("[{epoch}] staging, with maneuver {mnvr}");
214 state = state
215 .with_orbit(state.orbit.with_dv_km_s(mnvr.dv_km_s));
216 }
217 if let Some(decr) = decrement_properties {
218 if let Some(mass) = decr.mass {
219 state.mass.dry_mass_kg -= mass.dry_mass_kg;
220 state.mass.prop_mass_kg -= mass.prop_mass_kg;
221 state.mass.extra_mass_kg -= mass.extra_mass_kg;
222 }
223 if let Some(srp) = decr.srp {
224 state.srp.area_m2 -= srp.area_m2;
225 state.srp.coeff_reflectivity -= srp.coeff_reflectivity;
226 }
227 if let Some(drag) = decr.drag {
228 state.drag.area_m2 -= drag.area_m2;
229 state.drag.coeff_drag -= drag.coeff_drag;
230 }
231 }
232 }
233 DiscreteEvent::Docking {
234 impulsive_maneuver,
235 increment_properties,
236 } => {
237 if let Some(mnvr) = impulsive_maneuver {
238 info!("[{epoch}] docking, with maneuver {mnvr}");
239 state = state
240 .with_orbit(state.orbit.with_dv_km_s(mnvr.dv_km_s));
241 }
242 if let Some(incr) = increment_properties {
243 if let Some(mass) = incr.mass {
244 state.mass.dry_mass_kg += mass.dry_mass_kg;
245 state.mass.prop_mass_kg += mass.prop_mass_kg;
246 state.mass.extra_mass_kg += mass.extra_mass_kg;
247 }
248 if let Some(srp) = incr.srp {
249 state.srp.area_m2 += srp.area_m2;
250 state.srp.coeff_reflectivity += srp.coeff_reflectivity;
251 }
252 if let Some(drag) = incr.drag {
253 state.drag.area_m2 += drag.area_m2;
254 state.drag.coeff_drag += drag.coeff_drag;
255 }
256 }
257 }
258 }
259 }
260
261 let end_time = phase_iterator
262 .peek()
263 .expect("validate did not catch missing terminate")
264 .0;
265
266 let (next_state, mut phase_traj) = if let Some(guid_cfg) = guidance {
268 let mut setup = self.prop_setups[propagator].clone();
270 setup.dynamics.decrement_mass = !guid_cfg.disable_prop_mass;
271 state.thruster = Some(self.thruster_sets[&guid_cfg.thruster_model]);
272 match &guid_cfg.law {
273 SteeringLaw::FiniteBurn(maneuver) => {
274 setup.dynamics.guid_law = Some(Arc::new(*maneuver));
275 }
276 SteeringLaw::Ruggiero {
277 objectives,
278 max_eclipse_prct,
279 } => {
280 let guid = Ruggiero {
281 objectives: objectives.clone(),
282 max_eclipse_prct: *max_eclipse_prct,
283 init_state: state,
284 };
285 setup.dynamics.guid_law = Some(Arc::new(guid));
286 }
287 SteeringLaw::Kluever {
288 objectives,
289 max_eclipse_prct,
290 } => {
291 let guid = Kluever {
292 objectives: objectives.clone(),
293 max_eclipse_prct: *max_eclipse_prct,
294 };
295 setup.dynamics.guid_law = Some(Arc::new(guid));
296 }
297 }
298 setup
299 .with(state, almanac.clone())
300 .until_epoch_with_traj(*end_time)
301 .context(FromPropSnafu)?
302 } else {
303 self.prop_setups[propagator]
304 .with(state, almanac.clone())
305 .until_epoch_with_traj(*end_time)
306 .context(FromPropSnafu)?
307 };
308 info!("[{epoch}] {name} completed: {next_state:x}");
309 state = next_state;
310 phase_traj.name = Some(name.clone());
311 trajs.push(phase_traj);
312 }
313 }
314 }
315 }
316 unreachable!("spacecraft plan never finished?!")
317 }
318}
319
320impl StaticType for SpacecraftSequence {
321 fn static_type() -> serde_dhall::SimpleType {
322 let mut repr = HashMap::new();
323
324 let mut seq_entry = HashMap::new();
327 seq_entry.insert("_1".to_string(), SimpleType::Text); seq_entry.insert("_2".to_string(), Phase::static_type());
329
330 repr.insert(
331 "seq".to_string(),
332 SimpleType::List(Box::new(SimpleType::Record(seq_entry))),
333 );
334
335 let mut thruster_sets = HashMap::new();
338 thruster_sets.insert("_1".to_string(), SimpleType::Text);
339 thruster_sets.insert("_2".to_string(), Thruster::static_type());
340
341 repr.insert(
342 "thruster_sets".to_string(), SimpleType::List(Box::new(SimpleType::Record(thruster_sets))),
344 );
345
346 let mut propagators = HashMap::new();
347 propagators.insert("_1".to_string(), SimpleType::Text);
348 propagators.insert("_2".to_string(), PropagatorConfig::static_type());
349
350 repr.insert(
351 "propagators".to_string(), SimpleType::List(Box::new(SimpleType::Record(propagators))),
353 );
354
355 SimpleType::Record(repr)
356 }
357}
358
359fn map_as_pairs<S, K, V>(map: &BTreeMap<K, V>, serializer: S) -> Result<S::Ok, S::Error>
362where
363 S: serde::Serializer,
364 K: Serialize + Clone,
365 V: Serialize + Clone,
366{
367 serializer.collect_seq(map.iter())
369}
370
371fn pairs_as_map<'de, D, K, V>(deserializer: D) -> Result<BTreeMap<K, V>, D::Error>
373where
374 D: serde::Deserializer<'de>,
375 K: Deserialize<'de> + Ord,
376 V: Deserialize<'de>,
377{
378 let pairs: Vec<(K, V)> = Vec::deserialize(deserializer)?;
379 Ok(pairs.into_iter().collect())
380}
381#[cfg(feature = "python")]
382pub mod python;