Skip to main content

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