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 snafu::ResultExt;
21
22use super::guidance::{ra_dec_from_unit_vector, GuidanceError, GuidanceLaw};
23use super::orbital::OrbitalDynamics;
24use super::{Dynamics, DynamicsGuidanceSnafu, ForceModel};
25pub use crate::cosmic::{GuidanceMode, Spacecraft, STD_GRAVITY};
26use crate::dynamics::DynamicsError;
27
28use crate::linalg::{Const, DimName, OMatrix, OVector, Vector3};
29pub use crate::md::prelude::SolarPressure;
30use crate::State;
31
32use std::fmt::{self, Write};
33use std::sync::Arc;
34
35use crate::cosmic::AstroError;
36
37const NORM_ERR: f64 = 1e-4;
38
39/// A generic spacecraft dynamics with associated force models, guidance law, and flag specifying whether to decrement the prop mass or not.
40/// Note: when developing new guidance laws, it is recommended to _not_ enable prop decrement until the guidance law seems to work without proper physics.
41/// Note: if the spacecraft runs out of prop, the propagation segment will return an error.
42#[derive(Clone)]
43pub struct SpacecraftDynamics {
44    pub orbital_dyn: OrbitalDynamics,
45    // TODO: https://github.com/nyx-space/nyx/issues/214
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 Dynamics for SpacecraftDynamics {
149    type HyperdualSize = Const<9>;
150    type StateType = Spacecraft;
151
152    fn finally(
153        &self,
154        next_state: Self::StateType,
155        almanac: Arc<Almanac>,
156    ) -> Result<Self::StateType, DynamicsError> {
157        if next_state.mass.prop_mass_kg < 0.0 {
158            error!("negative prop mass at {}", next_state.epoch());
159            return Err(DynamicsError::FuelExhausted {
160                sc: Box::new(next_state),
161            });
162        }
163
164        if let Some(guid_law) = &self.guid_law {
165            let mut state = next_state;
166            // Update the control mode
167            guid_law.next(&mut state, almanac.clone());
168            Ok(state)
169        } else {
170            Ok(next_state)
171        }
172    }
173
174    fn eom(
175        &self,
176        delta_t_s: f64,
177        state: &OVector<f64, Const<90>>,
178        ctx: &Self::StateType,
179        almanac: Arc<Almanac>,
180    ) -> Result<OVector<f64, Const<90>>, DynamicsError> {
181        // Rebuild the osculating state for the EOM context.
182        let osc_sc = ctx.set_with_delta_seconds(delta_t_s, state);
183        let mut d_x = OVector::<f64, Const<90>>::zeros();
184
185        // Maybe I use this only when estimating the orbit state from a spacecraft, but that functionality will soon disappear.
186        match ctx.stm {
187            Some(stm) => {
188                // Call the gradient (also called the dual EOM function of the force models)
189                let (state, grad) = self.dual_eom(delta_t_s, &osc_sc, almanac)?;
190
191                // Apply the gradient to the STM
192                let stm_dt = stm * grad;
193
194                // Rebuild the state vector
195                for (i, val) in state.iter().enumerate() {
196                    d_x[i] = *val;
197                }
198
199                for (i, val) in stm_dt.iter().copied().enumerate() {
200                    d_x[i + <Spacecraft as State>::Size::dim()] = val;
201                }
202            }
203            None => {
204                // Compute the orbital dynamics
205                for (i, val) in self
206                    .orbital_dyn
207                    .eom(&osc_sc.orbit, almanac.clone())?
208                    .iter()
209                    .copied()
210                    .enumerate()
211                {
212                    d_x[i] = val;
213                }
214
215                // Apply the force models for non STM propagation
216                for model in &self.force_models {
217                    let model_frc = model.eom(&osc_sc, almanac.clone())? / osc_sc.mass_kg();
218                    for i in 0..3 {
219                        d_x[i + 3] += model_frc[i];
220                    }
221                }
222            }
223        };
224
225        // Now include the control as needed.
226        if let Some(guid_law) = &self.guid_law {
227            let (thrust_force, prop_rate) = {
228                if osc_sc.thruster.is_none() {
229                    return Err(DynamicsError::DynamicsGuidance {
230                        source: GuidanceError::NoThrustersDefined,
231                    });
232                }
233                let thruster = osc_sc.thruster.unwrap();
234                let thrust_throttle_lvl =
235                    guid_law.throttle(&osc_sc).context(DynamicsGuidanceSnafu)?;
236                if !(0.0..=1.0).contains(&thrust_throttle_lvl) {
237                    return Err(DynamicsError::DynamicsGuidance {
238                        source: GuidanceError::ThrottleRatio {
239                            ratio: thrust_throttle_lvl,
240                        },
241                    });
242                } else if thrust_throttle_lvl > 0.0 {
243                    // Thrust arc
244                    let thrust_inertial =
245                        guid_law.direction(&osc_sc).context(DynamicsGuidanceSnafu)?;
246                    if (thrust_inertial.norm() - 1.0).abs() > NORM_ERR {
247                        let (alpha, delta) = ra_dec_from_unit_vector(thrust_inertial);
248                        return Err(DynamicsError::DynamicsGuidance {
249                            source: GuidanceError::InvalidDirection {
250                                x: thrust_inertial[0],
251                                y: thrust_inertial[1],
252                                z: thrust_inertial[2],
253                                in_plane_deg: alpha.to_degrees(),
254                                out_of_plane_deg: delta.to_degrees(),
255                            },
256                        });
257                    } else if thrust_inertial.norm().is_normal() {
258                        // Compute the thrust in Newtons and Isp
259                        let total_thrust = (thrust_throttle_lvl * thruster.thrust_N) * 1e-3; // Convert m/s^-2 to km/s^-2
260                        (
261                            thrust_inertial * total_thrust,
262                            if self.decrement_mass {
263                                let prop_usage = thrust_throttle_lvl * thruster.thrust_N
264                                    / (thruster.isp_s * STD_GRAVITY);
265                                -prop_usage
266                            } else {
267                                0.0
268                            },
269                        )
270                    } else {
271                        warn!(
272                            "Abnormal thrust direction vector\t|u| = {}",
273                            thrust_inertial.norm()
274                        );
275                        (Vector3::zeros(), 0.0)
276                    }
277                } else {
278                    (Vector3::zeros(), 0.0)
279                }
280            };
281
282            for i in 0..3 {
283                d_x[i + 3] += thrust_force[i] / osc_sc.mass_kg();
284            }
285            d_x[8] += prop_rate;
286        }
287        Ok(d_x)
288    }
289
290    fn dual_eom(
291        &self,
292        delta_t_s: f64,
293        ctx: &Self::StateType,
294        almanac: Arc<Almanac>,
295    ) -> Result<(OVector<f64, Const<9>>, OMatrix<f64, Const<9>, Const<9>>), DynamicsError> {
296        // Rebuild the appropriately sized state and STM.
297        // This is the orbital state followed by Cr and Cd
298        let mut d_x = OVector::<f64, Const<9>>::zeros();
299        let mut grad = OMatrix::<f64, Const<9>, Const<9>>::zeros();
300
301        let (orb_state, orb_grad) =
302            self.orbital_dyn
303                .dual_eom(delta_t_s, &ctx.orbit, almanac.clone())?;
304
305        // Copy the d orbit dt data
306        for (i, val) in orb_state.iter().enumerate() {
307            d_x[i] = *val;
308        }
309
310        for i in 0..6 {
311            for j in 0..6 {
312                grad[(i, j)] = orb_grad[(i, j)];
313            }
314        }
315
316        if self.guid_law.is_some() {
317            return Err(DynamicsError::DynamicsAstro {
318                source: AstroError::PartialsUndefined,
319            });
320        }
321
322        // Call the EOMs
323        let total_mass = ctx.mass_kg();
324        for model in &self.force_models {
325            let (model_frc, model_grad) = model.dual_eom(ctx, almanac.clone())?;
326            for i in 0..3 {
327                // Add the velocity changes
328                d_x[i + 3] += model_frc[i] / total_mass;
329                // Add the velocity partials
330                for j in 0..3 {
331                    grad[(i + 3, j)] += model_grad[(i, j)] / total_mass;
332                }
333            }
334            // Add this force model's estimation if applicable.
335            if let Some(idx) = model.estimation_index() {
336                for j in 0..3 {
337                    grad[(j + 3, idx)] += model_grad[(3, j)] / total_mass;
338                }
339            }
340        }
341
342        Ok((d_x, grad))
343    }
344}