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    #[serde(with = "indexmap::map::serde_seq")]
54    pub thruster_sets: IndexMap<String, Thruster>,
55    #[serde(with = "indexmap::map::serde_seq")]
56    pub propagators: IndexMap<String, PropagatorConfig>,
57    #[serde(skip)]
58    prop_setups: IndexMap<String, Propagator<SpacecraftDynamics>>,
59}
60
61impl SpacecraftSequence {
62    pub fn validate(&self) -> Result<(), String> {
63        // Check that the last statement is a terminate
64        if let Some((_, Phase::Activity { .. })) = self.seq.iter().last() {
65            return Err("final phase must be a Terminate".into());
66        }
67
68        // Check that all of the thruster set indexes reference an available thruster
69        for (epoch, phase) in &self.seq {
70            if let Phase::Activity {
71                name: _,
72                propagator,
73                guidance,
74                on_entry: _,
75                disabled: _,
76            } = phase
77            {
78                // Check that the propagator exists
79                if self.propagators.get(propagator).is_none() {
80                    return Err(format!("{epoch}: no propagator named `{propagator}`"));
81                }
82                if let Some(guidance) = guidance {
83                    let thruster = &guidance.thruster_model;
84                    if self.thruster_sets.get(thruster).is_none() {
85                        return Err(format!("{epoch}: no thruster set named {thruster}"));
86                    }
87                }
88            }
89        }
90
91        Ok(())
92    }
93
94    /// Set up the propagators that are used in the timeline
95    pub fn setup(&mut self, almanac: Arc<Almanac>) -> Result<(), String> {
96        // Don't set up anything if this is not a valid timeline
97        self.validate()?;
98
99        for phase in self.seq.values() {
100            if let Phase::Activity {
101                name: _,
102                propagator,
103                guidance: _,
104                on_entry: _,
105                disabled,
106            } = phase
107                && !disabled
108                && self.prop_setups.get(propagator).is_none()
109            {
110                // Set up the propagator -- fetch the config first
111                // We know the config exists because validate would catch missing names.
112                let cfg = &self.propagators[propagator];
113                // Build the orbital dynamics
114                let mut orbital_dyn = OrbitalDynamics::two_body();
115                if let Some(point_masses) = &cfg.accel_models.point_masses {
116                    orbital_dyn.accel_models.push(point_masses.clone());
117                }
118                if let Some((gravity_cfg, frame_uid)) = &cfg.accel_models.gravity_field {
119                    let grav_data = GravityFieldData::from_config(gravity_cfg.clone())
120                        .map_err(|e| e.to_string())?;
121                    let compute_frame =
122                        almanac.frame_info(*frame_uid).map_err(|e| e.to_string())?;
123                    let gravity_field = GravityField::from_stor(compute_frame, grav_data);
124                    orbital_dyn.accel_models.push(gravity_field);
125                }
126                // Build the spacecraft dynamics
127                let mut sc_dyn = SpacecraftDynamics::new(orbital_dyn);
128
129                if let Some(srp) = &cfg.force_models.solar_pressure {
130                    sc_dyn.force_models.push(srp.clone());
131                }
132
133                if let Some(drag) = &cfg.force_models.drag {
134                    sc_dyn.force_models.push(drag.clone());
135                }
136
137                // And set it all up!
138                let setup = Propagator::new(sc_dyn, cfg.method, cfg.options);
139
140                self.prop_setups.insert(propagator.clone(), setup);
141                debug!("built `{propagator}`");
142            }
143        }
144
145        Ok(())
146    }
147
148    /// Propagate this plan starting the relevant phase for the probided state, and propagating until the end of the plan
149    /// or until the provided phase name. Returns the trajectory for each phase, allowing for each phase to be in its own central body.
150    pub fn propagate(
151        &self,
152        mut state: Spacecraft,
153        until_phase: Option<String>,
154        almanac: Arc<Almanac>,
155    ) -> Result<Vec<Trajectory>, NyxError> {
156        let tick = Epoch::now().unwrap();
157        let mut phase_iterator = self.seq.range(state.epoch()..).peekable();
158
159        let mut trajs = Vec::with_capacity(self.seq.len());
160
161        while let Some((epoch, phase)) = phase_iterator.next() {
162            match phase {
163                Phase::Terminate => {
164                    let tock = (Epoch::now().unwrap() - tick).round(Unit::Millisecond * 1);
165                    info!("[{epoch}] plan completed in {tock}");
166                    return Ok(trajs);
167                }
168                Phase::Activity {
169                    name,
170                    propagator,
171                    guidance,
172                    on_entry,
173                    disabled,
174                } => {
175                    // Check stop condition
176                    if let Some(ref target) = until_phase
177                        && target == name
178                    {
179                        return Ok(trajs);
180                    }
181
182                    if *disabled {
183                        info!("[{epoch}] skipping disabled {name}");
184                    } else {
185                        info!("[{epoch}] executing {name}");
186                        if let Some(discrete_event) = on_entry {
187                            match &**discrete_event {
188                                DiscreteEvent::FrameSwap { new_frame } => {
189                                    if !new_frame.orient_origin_match(state.orbit.frame)
190                                        || !new_frame.ephem_origin_match(state.orbit.frame)
191                                    {
192                                        state = state.with_orbit(
193                                            almanac
194                                                .translate_to(state.orbit, *new_frame, None)
195                                                .map_err(|source| {
196                                                    anise::errors::AlmanacError::Ephemeris {
197                                                        action: "central body swap",
198                                                        source: Box::new(source),
199                                                    }
200                                                })
201                                                .context(FromAlmanacSnafu {
202                                                    action: "central body swap",
203                                                })?,
204                                        );
205                                        info!("[{epoch}] central body swapped to {new_frame}");
206                                    }
207                                }
208                                DiscreteEvent::Staging {
209                                    impulsive_maneuver,
210                                    decrement_properties,
211                                } => {
212                                    if let Some(mnvr) = impulsive_maneuver {
213                                        info!("[{epoch}] staging, with maneuver {mnvr}");
214                                        state = state
215                                            .with_orbit(state.orbit.with_dv_km_s(mnvr.dv_km_s));
216                                    }
217                                    if let Some(decr) = decrement_properties {
218                                        if let Some(mass) = decr.mass {
219                                            state.mass.dry_mass_kg -= mass.dry_mass_kg;
220                                            state.mass.prop_mass_kg -= mass.prop_mass_kg;
221                                            state.mass.extra_mass_kg -= mass.extra_mass_kg;
222                                        }
223                                        if let Some(srp) = decr.srp {
224                                            state.srp.area_m2 -= srp.area_m2;
225                                            state.srp.coeff_reflectivity -= srp.coeff_reflectivity;
226                                        }
227                                        if let Some(drag) = decr.drag {
228                                            state.drag.area_m2 -= drag.area_m2;
229                                            state.drag.coeff_drag -= drag.coeff_drag;
230                                        }
231                                    }
232                                }
233                                DiscreteEvent::Docking {
234                                    impulsive_maneuver,
235                                    increment_properties,
236                                } => {
237                                    if let Some(mnvr) = impulsive_maneuver {
238                                        info!("[{epoch}] docking, with maneuver {mnvr}");
239                                        state = state
240                                            .with_orbit(state.orbit.with_dv_km_s(mnvr.dv_km_s));
241                                    }
242                                    if let Some(incr) = increment_properties {
243                                        if let Some(mass) = incr.mass {
244                                            state.mass.dry_mass_kg += mass.dry_mass_kg;
245                                            state.mass.prop_mass_kg += mass.prop_mass_kg;
246                                            state.mass.extra_mass_kg += mass.extra_mass_kg;
247                                        }
248                                        if let Some(srp) = incr.srp {
249                                            state.srp.area_m2 += srp.area_m2;
250                                            state.srp.coeff_reflectivity += srp.coeff_reflectivity;
251                                        }
252                                        if let Some(drag) = incr.drag {
253                                            state.drag.area_m2 += drag.area_m2;
254                                            state.drag.coeff_drag += drag.coeff_drag;
255                                        }
256                                    }
257                                }
258                            }
259                        }
260
261                        let end_time = phase_iterator
262                            .peek()
263                            .expect("validate did not catch missing terminate")
264                            .0;
265
266                        // Include the guidance if any is available
267                        let (next_state, mut phase_traj) = if let Some(guid_cfg) = guidance {
268                            // Clone the propagator to add the dynamics
269                            let mut setup = self.prop_setups[propagator].clone();
270                            setup.dynamics.decrement_mass = !guid_cfg.disable_prop_mass;
271                            state.thruster = Some(self.thruster_sets[&guid_cfg.thruster_model]);
272                            match &guid_cfg.law {
273                                SteeringLaw::FiniteBurn(maneuver) => {
274                                    setup.dynamics.guid_law = Some(Arc::new(*maneuver));
275                                }
276                                SteeringLaw::Ruggiero {
277                                    objectives,
278                                    max_eclipse_prct,
279                                } => {
280                                    let guid = Ruggiero {
281                                        objectives: objectives.clone(),
282                                        max_eclipse_prct: *max_eclipse_prct,
283                                        init_state: state,
284                                    };
285                                    setup.dynamics.guid_law = Some(Arc::new(guid));
286                                }
287                                SteeringLaw::Kluever {
288                                    objectives,
289                                    max_eclipse_prct,
290                                } => {
291                                    let guid = Kluever {
292                                        objectives: objectives.clone(),
293                                        max_eclipse_prct: *max_eclipse_prct,
294                                    };
295                                    setup.dynamics.guid_law = Some(Arc::new(guid));
296                                }
297                            }
298                            setup
299                                .with(state, almanac.clone())
300                                .until_epoch_with_traj(*end_time)
301                                .context(FromPropSnafu)?
302                        } else {
303                            self.prop_setups[propagator]
304                                .with(state, almanac.clone())
305                                .until_epoch_with_traj(*end_time)
306                                .context(FromPropSnafu)?
307                        };
308                        info!("[{epoch}] {name} completed: {next_state:x}");
309                        state = next_state;
310                        phase_traj.name = Some(name.clone());
311                        trajs.push(phase_traj);
312                    }
313                }
314            }
315        }
316        unreachable!("spacecraft plan never finished?!")
317    }
318}
319
320impl StaticType for SpacecraftSequence {
321    fn static_type() -> serde_dhall::SimpleType {
322        let mut repr = HashMap::new();
323
324        // seq maps to "sequence" in Dhall
325        // Serialized as List { _1: Text, _2: Phase }
326        let mut seq_entry = HashMap::new();
327        seq_entry.insert("_1".to_string(), SimpleType::Text); // Epoch serializes to Text
328        seq_entry.insert("_2".to_string(), Phase::static_type());
329
330        repr.insert(
331            "seq".to_string(),
332            SimpleType::List(Box::new(SimpleType::Record(seq_entry))),
333        );
334
335        // thruster_sets maps to "thruster_set" in Dhall (matches your serialization name)
336        // Serialized as List { _1: Text, _2: Thruster }
337        let mut thruster_sets = HashMap::new();
338        thruster_sets.insert("_1".to_string(), SimpleType::Text);
339        thruster_sets.insert("_2".to_string(), Thruster::static_type());
340
341        repr.insert(
342            "thruster_sets".to_string(), // Keep as "thruster_set" if that's your Dhall preference
343            SimpleType::List(Box::new(SimpleType::Record(thruster_sets))),
344        );
345
346        let mut propagators = HashMap::new();
347        propagators.insert("_1".to_string(), SimpleType::Text);
348        propagators.insert("_2".to_string(), PropagatorConfig::static_type());
349
350        repr.insert(
351            "propagators".to_string(), // Keep as "thruster_set" if that's your Dhall preference
352            SimpleType::List(Box::new(SimpleType::Record(propagators))),
353        );
354
355        SimpleType::Record(repr)
356    }
357}
358
359/* serialization helper functions */
360
361fn map_as_pairs<S, K, V>(map: &BTreeMap<K, V>, serializer: S) -> Result<S::Ok, S::Error>
362where
363    S: serde::Serializer,
364    K: Serialize + Clone,
365    V: Serialize + Clone,
366{
367    // This turns the map into a sequence of (K, V) which Serde-Dhall sees as {_1, _2}
368    serializer.collect_seq(map.iter())
369}
370
371// You'll need a symmetric deserializer if you plan to read these back
372fn pairs_as_map<'de, D, K, V>(deserializer: D) -> Result<BTreeMap<K, V>, D::Error>
373where
374    D: serde::Deserializer<'de>,
375    K: Deserialize<'de> + Ord,
376    V: Deserialize<'de>,
377{
378    let pairs: Vec<(K, V)> = Vec::deserialize(deserializer)?;
379    Ok(pairs.into_iter().collect())
380}
381#[cfg(feature = "python")]
382pub mod python;