nyx_space/dynamics/
spacecraft.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 anise::prelude::Almanac;
20use log::{error, warn};
21use snafu::ResultExt;
22
23use super::guidance::{ra_dec_from_unit_vector, GuidanceError, GuidanceLaw};
24use super::orbital::OrbitalDynamics;
25use super::{Dynamics, DynamicsGuidanceSnafu, ForceModel};
26pub use crate::cosmic::{GuidanceMode, Spacecraft, STD_GRAVITY};
27use crate::dynamics::DynamicsError;
28
29use crate::linalg::{Const, DimName, OMatrix, OVector, Vector3};
30pub use crate::md::prelude::SolarPressure;
31use crate::State;
32
33use std::fmt::{self, Write};
34use std::sync::Arc;
35
36use crate::cosmic::AstroError;
37
38const NORM_ERR: f64 = 1e-4;
39
40/// A generic spacecraft dynamics with associated force models, guidance law, and flag specifying whether to decrement the prop mass or not.
41/// Note: when developing new guidance laws, it is recommended to _not_ enable prop decrement until the guidance law seems to work without proper physics.
42/// Note: if the spacecraft runs out of prop, the propagation segment will return an error.
43#[derive(Clone)]
44pub struct SpacecraftDynamics {
45    pub orbital_dyn: OrbitalDynamics,
46    // TODO: https://github.com/nyx-space/nyx/issues/214
47    pub force_models: Vec<Arc<dyn ForceModel>>,
48    pub guid_law: Option<Arc<dyn GuidanceLaw>>,
49    pub decrement_mass: bool,
50}
51
52impl SpacecraftDynamics {
53    /// Initialize a Spacecraft with a set of orbital dynamics and a propulsion subsystem.
54    /// By default, the mass of the vehicle will be decremented as propellant is consumed.
55    pub fn from_guidance_law(orbital_dyn: OrbitalDynamics, guid_law: Arc<dyn GuidanceLaw>) -> Self {
56        Self {
57            orbital_dyn,
58            guid_law: Some(guid_law),
59            force_models: Vec::new(),
60            decrement_mass: true,
61        }
62    }
63
64    /// Initialize a Spacecraft with a set of orbital dynamics and a propulsion subsystem.
65    /// Will _not_ decrement the prop mass as propellant is consumed.
66    pub fn from_guidance_law_no_decr(
67        orbital_dyn: OrbitalDynamics,
68        guid_law: Arc<dyn GuidanceLaw>,
69    ) -> Self {
70        Self {
71            orbital_dyn,
72            guid_law: Some(guid_law),
73            force_models: Vec::new(),
74            decrement_mass: false,
75        }
76    }
77
78    /// Initialize a Spacecraft with a set of orbital dynamics and with SRP enabled.
79    pub fn new(orbital_dyn: OrbitalDynamics) -> Self {
80        Self {
81            orbital_dyn,
82            guid_law: None,
83            force_models: Vec::new(),
84            decrement_mass: true,
85        }
86    }
87
88    /// Initialize new spacecraft dynamics with the provided orbital mechanics and with the provided force model.
89    pub fn from_model(orbital_dyn: OrbitalDynamics, force_model: Arc<dyn ForceModel>) -> Self {
90        Self {
91            orbital_dyn,
92            guid_law: None,
93            force_models: vec![force_model],
94            decrement_mass: true,
95        }
96    }
97
98    /// Initialize new spacecraft dynamics with a vector of force models.
99    pub fn from_models(
100        orbital_dyn: OrbitalDynamics,
101        force_models: Vec<Arc<dyn ForceModel>>,
102    ) -> Self {
103        let mut me = Self::new(orbital_dyn);
104        me.force_models = force_models;
105        me
106    }
107
108    /// A shortcut to spacecraft.guid_law if a guidance law is defined for these dynamics
109    pub fn guidance_achieved(&self, state: &Spacecraft) -> Result<bool, GuidanceError> {
110        match &self.guid_law {
111            Some(guid_law) => guid_law.achieved(state),
112            None => Err(GuidanceError::NoGuidanceObjectiveDefined),
113        }
114    }
115
116    /// Clone these spacecraft dynamics and update the control to the one provided.
117    pub fn with_guidance_law(&self, guid_law: Arc<dyn GuidanceLaw>) -> Self {
118        Self {
119            orbital_dyn: self.orbital_dyn.clone(),
120            guid_law: Some(guid_law),
121            force_models: self.force_models.clone(),
122            decrement_mass: self.decrement_mass,
123        }
124    }
125}
126
127impl fmt::Display for SpacecraftDynamics {
128    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
129        let force_models: String = if self.force_models.is_empty() {
130            "No force models;".to_string()
131        } else {
132            self.force_models
133                .iter()
134                .fold(String::new(), |mut output, x| {
135                    let _ = write!(output, "{x}; ");
136                    output
137                })
138        };
139        write!(
140            f,
141            "Spacecraft dynamics (with guidance = {}): {} {}",
142            self.guid_law.is_some(),
143            force_models,
144            self.orbital_dyn
145        )
146    }
147}
148
149impl Dynamics for SpacecraftDynamics {
150    type HyperdualSize = Const<9>;
151    type StateType = Spacecraft;
152
153    fn finally(
154        &self,
155        next_state: Self::StateType,
156        almanac: Arc<Almanac>,
157    ) -> Result<Self::StateType, DynamicsError> {
158        if next_state.mass.prop_mass_kg < 0.0 {
159            error!("negative prop mass at {}", next_state.epoch());
160            return Err(DynamicsError::FuelExhausted {
161                sc: Box::new(next_state),
162            });
163        }
164
165        if let Some(guid_law) = &self.guid_law {
166            let mut state = next_state;
167            // Update the control mode
168            guid_law.next(&mut state, almanac.clone());
169            Ok(state)
170        } else {
171            Ok(next_state)
172        }
173    }
174
175    fn eom(
176        &self,
177        delta_t_s: f64,
178        state: &OVector<f64, Const<90>>,
179        ctx: &Self::StateType,
180        almanac: Arc<Almanac>,
181    ) -> Result<OVector<f64, Const<90>>, DynamicsError> {
182        // Rebuild the osculating state for the EOM context.
183        let osc_sc = ctx.set_with_delta_seconds(delta_t_s, state);
184        let mut d_x = OVector::<f64, Const<90>>::zeros();
185
186        // Maybe I use this only when estimating the orbit state from a spacecraft, but that functionality will soon disappear.
187        match ctx.stm {
188            Some(stm) => {
189                // Call the gradient (also called the dual EOM function of the force models)
190                let (state, grad) = self.dual_eom(delta_t_s, &osc_sc, almanac)?;
191
192                // Apply the gradient to the STM
193                let stm_dt = stm * grad;
194
195                // Rebuild the state vector
196                for (i, val) in state.iter().enumerate() {
197                    d_x[i] = *val;
198                }
199
200                for (i, val) in stm_dt.iter().copied().enumerate() {
201                    d_x[i + <Spacecraft as State>::Size::dim()] = val;
202                }
203            }
204            None => {
205                // Compute the orbital dynamics
206                for (i, val) in self
207                    .orbital_dyn
208                    .eom(&osc_sc.orbit, almanac.clone())?
209                    .iter()
210                    .copied()
211                    .enumerate()
212                {
213                    d_x[i] = val;
214                }
215
216                // Apply the force models for non STM propagation
217                for model in &self.force_models {
218                    let model_frc = model.eom(&osc_sc, almanac.clone())? / osc_sc.mass_kg();
219                    for i in 0..3 {
220                        d_x[i + 3] += model_frc[i];
221                    }
222                }
223            }
224        };
225
226        // Now include the control as needed.
227        if let Some(guid_law) = &self.guid_law {
228            let (thrust_force, prop_rate) = {
229                if osc_sc.thruster.is_none() {
230                    return Err(DynamicsError::DynamicsGuidance {
231                        source: GuidanceError::NoThrustersDefined,
232                    });
233                }
234                let thruster = osc_sc.thruster.unwrap();
235                let thrust_throttle_lvl =
236                    guid_law.throttle(&osc_sc).context(DynamicsGuidanceSnafu)?;
237                if !(0.0..=1.0).contains(&thrust_throttle_lvl) {
238                    return Err(DynamicsError::DynamicsGuidance {
239                        source: GuidanceError::ThrottleRatio {
240                            ratio: thrust_throttle_lvl,
241                        },
242                    });
243                } else if thrust_throttle_lvl > 0.0 {
244                    // Thrust arc
245                    let thrust_inertial =
246                        guid_law.direction(&osc_sc).context(DynamicsGuidanceSnafu)?;
247                    if (thrust_inertial.norm() - 1.0).abs() > NORM_ERR {
248                        let (alpha, delta) = ra_dec_from_unit_vector(thrust_inertial);
249                        return Err(DynamicsError::DynamicsGuidance {
250                            source: GuidanceError::InvalidDirection {
251                                x: thrust_inertial[0],
252                                y: thrust_inertial[1],
253                                z: thrust_inertial[2],
254                                in_plane_deg: alpha.to_degrees(),
255                                out_of_plane_deg: delta.to_degrees(),
256                            },
257                        });
258                    } else if thrust_inertial.norm().is_normal() {
259                        // Compute the thrust in Newtons and Isp
260                        let total_thrust = (thrust_throttle_lvl * thruster.thrust_N) * 1e-3; // Convert m/s^-2 to km/s^-2
261                        (
262                            thrust_inertial * total_thrust,
263                            if self.decrement_mass {
264                                let prop_usage = thrust_throttle_lvl * thruster.thrust_N
265                                    / (thruster.isp_s * STD_GRAVITY);
266                                -prop_usage
267                            } else {
268                                0.0
269                            },
270                        )
271                    } else {
272                        warn!(
273                            "Abnormal thrust direction vector\t|u| = {}",
274                            thrust_inertial.norm()
275                        );
276                        (Vector3::zeros(), 0.0)
277                    }
278                } else {
279                    (Vector3::zeros(), 0.0)
280                }
281            };
282
283            for i in 0..3 {
284                d_x[i + 3] += thrust_force[i] / osc_sc.mass_kg();
285            }
286            d_x[8] += prop_rate;
287        }
288        Ok(d_x)
289    }
290
291    fn dual_eom(
292        &self,
293        delta_t_s: f64,
294        ctx: &Self::StateType,
295        almanac: Arc<Almanac>,
296    ) -> Result<(OVector<f64, Const<9>>, OMatrix<f64, Const<9>, Const<9>>), DynamicsError> {
297        // Rebuild the appropriately sized state and STM.
298        // This is the orbital state followed by Cr and Cd
299        let mut d_x = OVector::<f64, Const<9>>::zeros();
300        let mut grad = OMatrix::<f64, Const<9>, Const<9>>::zeros();
301
302        let (orb_state, orb_grad) =
303            self.orbital_dyn
304                .dual_eom(delta_t_s, &ctx.orbit, almanac.clone())?;
305
306        // Copy the d orbit dt data
307        for (i, val) in orb_state.iter().enumerate() {
308            d_x[i] = *val;
309        }
310
311        for i in 0..6 {
312            for j in 0..6 {
313                grad[(i, j)] = orb_grad[(i, j)];
314            }
315        }
316
317        if self.guid_law.is_some() {
318            return Err(DynamicsError::DynamicsAstro {
319                source: AstroError::PartialsUndefined,
320            });
321        }
322
323        // Call the EOMs
324        let total_mass = ctx.mass_kg();
325        for model in &self.force_models {
326            let (model_frc, model_grad) = model.dual_eom(ctx, almanac.clone())?;
327            for i in 0..3 {
328                // Add the velocity changes
329                d_x[i + 3] += model_frc[i] / total_mass;
330                // Add the velocity partials
331                for j in 0..3 {
332                    grad[(i + 3, j)] += model_grad[(i, j)] / total_mass;
333                }
334            }
335            // Add this force model's estimation if applicable.
336            if let Some(idx) = model.estimation_index() {
337                for j in 0..3 {
338                    grad[(j + 3, idx)] += model_grad[(3, j)] / total_mass;
339                }
340            }
341        }
342
343        Ok((d_x, grad))
344    }
345}