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