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