1use 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#[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 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 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 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 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 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 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 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 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 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 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 match ctx.stm {
209 Some(stm) => {
210 let (state, grad) = self.dual_eom(delta_t_s, &osc_sc, almanac)?;
212
213 let stm_dt = stm * grad;
215
216 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 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 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 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 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 let total_thrust = (thrust_throttle_lvl * thruster.thrust_N) * 1e-3; (
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 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 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 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 d_x[i + 3] += model_frc[i] / total_mass;
351 for j in 0..3 {
353 grad[(i + 3, j)] += model_grad[(i, j)] / total_mass;
354 }
355 }
356 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}