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::{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        // Check that the last statement is a terminate
62        if let Some((_, Phase::Activity { .. })) = self.seq.iter().last() {
63            return Err("final phase must be a Terminate".into());
64        }
65
66        // Check that all of the thruster set indexes reference an available thruster
67        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                // Check that the propagator exists
77                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    /// Set up the propagators that are used in the timeline
93    pub fn setup(&mut self, almanac: Arc<Almanac>) -> Result<(), String> {
94        // Don't set up anything if this is not a valid timeline
95        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                // Set up the propagator -- fetch the config first
109                // We know the config exists because validate would catch missing names.
110                let cfg = &self.propagators[propagator];
111                // Build the orbital dynamics
112                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                // Build the spacecraft dynamics
125                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                // And set it all up!
136                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    /// Propagate this plan starting the relevant phase for the probided state, and propagating until the end of the plan
147    /// or until the provided phase name. Returns the trajectory for each phase, allowing for each phase to be in its own central body.
148    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                    // Check stop condition
174                    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                        // Include the guidance if any is available
265                        let (next_state, mut phase_traj) = if let Some(guid_cfg) = guidance {
266                            // Clone the propagator to add the dynamics
267                            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        // seq maps to "sequence" in Dhall
323        // Serialized as List { _1: Text, _2: Phase }
324        let mut seq_entry = HashMap::new();
325        seq_entry.insert("_1".to_string(), SimpleType::Text); // Epoch serializes to Text
326        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        // thruster_sets maps to "thruster_set" in Dhall (matches your serialization name)
334        // Serialized as List { _1: Text, _2: Thruster }
335        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(), // Keep as "thruster_set" if that's your Dhall preference
341            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(), // Keep as "thruster_set" if that's your Dhall preference
350            SimpleType::List(Box::new(SimpleType::Record(propagators))),
351        );
352
353        SimpleType::Record(repr)
354    }
355}
356
357/* serialization helper functions */
358
359fn 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    // This turns the map into a sequence of (K, V) which Serde-Dhall sees as {_1, _2}
366    serializer.collect_seq(map.iter())
367}
368
369// You'll need a symmetric deserializer if you plan to read these back
370fn 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;