Skip to main content

nyx_space/dynamics/sequence/
mod.rs

1/*
2    Nyx, blazing fast astrodynamics
3    Copyright (C) 2018-onwards Christopher Rabotin <christopher.rabotin@gmail.com>
4
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU Affero General Public License as published
7    by the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9
10    This program is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU Affero General Public License for more details.
14
15    You should have received a copy of the GNU Affero General Public License
16    along with this program.  If not, see <https://www.gnu.org/licenses/>.
17*/
18
19use 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        // Check that the last statement is a terminate
61        if let Some((_, Phase::Activity { .. })) = self.seq.iter().last() {
62            return Err("final phase must be a Terminate".into());
63        }
64
65        // Check that all of the thruster set indexes reference an available thruster
66        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                // Check that the propagator exists
76                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    /// Set up the propagators that are used in the timeline
92    pub fn setup(&mut self, almanac: Arc<Almanac>) -> Result<(), String> {
93        // Don't set up anything if this is not a valid timeline
94        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                    // Set up the propagator -- fetch the config first
107                    // We know the config exists because validate would catch missing names.
108                    let cfg = &self.propagators[propagator];
109                    // Build the orbital dynamics
110                    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                    // Build the spacecraft dynamics
123                    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                    // And set it all up!
134                    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    /// Propagate this plan starting the relevant phase for the probided state, and propagating until the end of the plan
146    /// or until the provided phase name. Returns the trajectory for each phase, allowing for each phase to be in its own central body.
147    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                    // Check stop condition
173                    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                        // Include the guidance if any is available
264                        let (next_state, mut phase_traj) = if let Some(guid_cfg) = guidance {
265                            // Clone the propagator to add the dynamics
266                            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        // seq maps to "sequence" in Dhall
322        // Serialized as List { _1: Text, _2: Phase }
323        let mut seq_entry = HashMap::new();
324        seq_entry.insert("_1".to_string(), SimpleType::Text); // Epoch serializes to Text
325        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        // thruster_sets maps to "thruster_set" in Dhall (matches your serialization name)
333        // Serialized as List { _1: Text, _2: Thruster }
334        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(), // Keep as "thruster_set" if that's your Dhall preference
340            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(), // Keep as "thruster_set" if that's your Dhall preference
349            SimpleType::List(Box::new(SimpleType::Record(propagators))),
350        );
351
352        SimpleType::Record(repr)
353    }
354}
355
356/* serialization helper functions */
357
358fn 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    // This turns the map into a sequence of (K, V) which Serde-Dhall sees as {_1, _2}
365    serializer.collect_seq(map.iter())
366}
367
368// You'll need a symmetric deserializer if you plan to read these back
369fn 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}