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};
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        // Check that the last statement is a terminate
60        if let Some((_, Phase::Activity { .. })) = self.seq.iter().last() {
61            return Err("final phase must be a Terminate".into());
62        }
63
64        // Check that all of the thruster set indexes reference an available thruster
65        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                // Check that the propagator exists
75                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    /// Set up the propagators that are used in the timeline
91    pub fn setup(&mut self, almanac: Arc<Almanac>) -> Result<(), String> {
92        // Don't set up anything if this is not a valid timeline
93        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                // 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                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    /// Propagate this plan starting the relevant phase for the probided state, and propagating until the end of the plan
120    /// or until the provided phase name. Returns the trajectory for each phase, allowing for each phase to be in its own central body.
121    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                    // Check stop condition
147                    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                        // Include the guidance if any is available
238                        let (next_state, mut phase_traj) = if let Some(guid_cfg) = guidance {
239                            // Clone the propagator to add the dynamics
240                            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        // seq maps to "sequence" in Dhall
296        // Serialized as List { _1: Text, _2: Phase }
297        let mut seq_entry = HashMap::new();
298        seq_entry.insert("_1".to_string(), SimpleType::Text); // Epoch serializes to Text
299        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        // thruster_sets maps to "thruster_set" in Dhall (matches your serialization name)
307        // Serialized as List { _1: Text, _2: Thruster }
308        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(), // Keep as "thruster_set" if that's your Dhall preference
314            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(), // Keep as "thruster_set" if that's your Dhall preference
323            SimpleType::List(Box::new(SimpleType::Record(propagators))),
324        );
325
326        SimpleType::Record(repr)
327    }
328}
329
330/* serialization helper functions */
331
332fn 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    // This turns the map into a sequence of (K, V) which Serde-Dhall sees as {_1, _2}
339    serializer.collect_seq(map.iter())
340}
341
342// You'll need a symmetric deserializer if you plan to read these back
343fn 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;