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