1use 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#[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 let mut d_x = OVector::<f64, Const<90>>::zeros();
201
202 match ctx.stm {
204 Some(stm) => {
205 let (state, grad) = self.dual_eom(delta_t_s, &osc_sc, almanac)?;
207
208 let stm_dt = stm * grad;
210
211 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 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 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 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 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 let total_thrust = (thrust_throttle_lvl * thruster.thrust_N) * 1e-3; (
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 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 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 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 d_x[i + 3] += model_frc[i] / total_mass;
346 for j in 0..3 {
348 grad[(i + 3, j)] += model_grad[(i, j)] / total_mass;
349 }
350 }
351 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}