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;
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    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        let mut d_x = OVector::<f64, Const<90>>::zeros();
201
202        // Maybe I use this only when estimating the orbit state from a spacecraft, but that functionality will soon disappear.
203        match ctx.stm {
204            Some(stm) => {
205                // Call the gradient (also called the dual EOM function of the force models)
206                let (state, grad) = self.dual_eom(delta_t_s, &osc_sc, almanac)?;
207
208                // Apply the gradient to the STM
209                let stm_dt = stm * grad;
210
211                // Rebuild the state vector
212                for (i, val) in state.iter().enumerate() {
213                    d_x[i] = *val;
214                }
215
216                for (i, val) in stm_dt.iter().copied().enumerate() {
217                    d_x[i + <Spacecraft as State>::Size::dim()] = val;
218                }
219            }
220            None => {
221                // Compute the orbital dynamics
222                for (i, val) in self
223                    .orbital_dyn
224                    .eom(&osc_sc.orbit, almanac.clone())?
225                    .iter()
226                    .copied()
227                    .enumerate()
228                {
229                    d_x[i] = val;
230                }
231
232                // Apply the force models for non STM propagation
233                for model in &self.force_models {
234                    let model_frc = model.eom(&osc_sc, almanac.clone())? / osc_sc.mass_kg();
235                    for i in 0..3 {
236                        d_x[i + 3] += model_frc[i];
237                    }
238                }
239            }
240        };
241
242        // Now include the control as needed.
243        if let Some(guid_law) = &self.guid_law {
244            let (thrust_force, prop_rate) = {
245                if osc_sc.thruster.is_none() {
246                    return Err(DynamicsError::DynamicsGuidance {
247                        source: GuidanceError::NoThrustersDefined,
248                    });
249                }
250                let thruster = osc_sc.thruster.unwrap();
251                let thrust_throttle_lvl =
252                    guid_law.throttle(&osc_sc).context(DynamicsGuidanceSnafu)?;
253                if !(0.0..=1.0).contains(&thrust_throttle_lvl) {
254                    return Err(DynamicsError::DynamicsGuidance {
255                        source: GuidanceError::ThrottleRatio {
256                            ratio: thrust_throttle_lvl,
257                        },
258                    });
259                } else if thrust_throttle_lvl > 0.0 {
260                    // Thrust arc
261                    let thrust_inertial =
262                        guid_law.direction(&osc_sc).context(DynamicsGuidanceSnafu)?;
263                    if (thrust_inertial.norm() - 1.0).abs() > NORM_ERR {
264                        let (alpha, delta) = ra_dec_from_unit_vector(thrust_inertial);
265                        return Err(DynamicsError::DynamicsGuidance {
266                            source: GuidanceError::InvalidDirection {
267                                x: thrust_inertial[0],
268                                y: thrust_inertial[1],
269                                z: thrust_inertial[2],
270                                in_plane_deg: alpha.to_degrees(),
271                                out_of_plane_deg: delta.to_degrees(),
272                            },
273                        });
274                    } else if thrust_inertial.norm().is_normal() {
275                        // Compute the thrust in Newtons and Isp
276                        let total_thrust = (thrust_throttle_lvl * thruster.thrust_N) * 1e-3; // Convert m/s^-2 to km/s^-2
277                        (
278                            thrust_inertial * total_thrust,
279                            if self.decrement_mass {
280                                let prop_usage = thrust_throttle_lvl * thruster.thrust_N
281                                    / (thruster.isp_s * STD_GRAVITY);
282                                -prop_usage
283                            } else {
284                                0.0
285                            },
286                        )
287                    } else {
288                        warn!(
289                            "Abnormal thrust direction vector\t|u| = {}",
290                            thrust_inertial.norm()
291                        );
292                        (Vector3::zeros(), 0.0)
293                    }
294                } else {
295                    (Vector3::zeros(), 0.0)
296                }
297            };
298
299            for i in 0..3 {
300                d_x[i + 3] += thrust_force[i] / osc_sc.mass_kg();
301            }
302            d_x[8] += prop_rate;
303        }
304        Ok(d_x)
305    }
306
307    fn dual_eom(
308        &self,
309        delta_t_s: f64,
310        ctx: &Self::StateType,
311        almanac: Arc<Almanac>,
312    ) -> Result<(OVector<f64, Const<9>>, OMatrix<f64, Const<9>, Const<9>>), DynamicsError> {
313        // Rebuild the appropriately sized state and STM.
314        // This is the orbital state followed by Cr and Cd
315        let mut d_x = OVector::<f64, Const<9>>::zeros();
316        let mut grad = OMatrix::<f64, Const<9>, Const<9>>::zeros();
317
318        let (orb_state, orb_grad) =
319            self.orbital_dyn
320                .dual_eom(delta_t_s, &ctx.orbit, almanac.clone())?;
321
322        // Copy the d orbit dt data
323        for (i, val) in orb_state.iter().enumerate() {
324            d_x[i] = *val;
325        }
326
327        for i in 0..6 {
328            for j in 0..6 {
329                grad[(i, j)] = orb_grad[(i, j)];
330            }
331        }
332
333        if self.guid_law.is_some() {
334            return Err(DynamicsError::DynamicsAstro {
335                source: AstroError::PartialsUndefined,
336            });
337        }
338
339        // Call the EOMs
340        let total_mass = ctx.mass_kg();
341        for model in &self.force_models {
342            let (model_frc, model_grad) = model.gradient(ctx, almanac.clone())?;
343            for i in 0..3 {
344                // Add the velocity changes
345                d_x[i + 3] += model_frc[i] / total_mass;
346                // Add the velocity partials
347                for j in 0..3 {
348                    grad[(i + 3, j)] += model_grad[(i, j)] / total_mass;
349                }
350            }
351            // Add this force model's estimation if applicable.
352            if let Some(idx) = model.estimation_index() {
353                for j in 0..3 {
354                    grad[(j + 3, idx)] += model_grad[(3, j)] / total_mass;
355                }
356            }
357        }
358
359        Ok((d_x, grad))
360    }
361}